Compare commits

...
143 Commits
Author SHA1 Message Date
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
Matt Wright ba1758e5c7 Bump version number to 1.6.0 2013-03-13 14:06:34 -04:00
Matt Wright 36198c1993 Update CHANGE 2013-03-13 14:06:08 -04:00
Matt Wright 8708fd8514 Update form messaging to be more flexible. Fixes #80 2013-03-13 13:40:35 -04:00
Matt Wright 44a320ee74 Fix a failling test 2013-03-13 12:28:26 -04:00
Matt Wright 520b8ecef4 Show an invalid confirmation token message if a user attempts to confirm their account after it has been deleted. Fixes #93 2013-03-13 12:27:26 -04:00
Matt Wright 34aa43ead3 Merge pull request #100 from chrishaines/develop
Make subdomain configurable
2013-03-13 09:10:16 -07:00
Matt Wright 8ecc3b9a78 Add user to request context for http basic and token auth 2013-03-13 12:09:28 -04:00
Chris Haines c0d1d0566f Make subdomain configurable 2013-03-13 00:13:54 -04:00
Matt Wright 2e01cab3f8 polish 2013-03-07 15:38:41 -05:00
Matt Wright bbe99b5436 Fixes #98 2013-03-07 15:38:34 -05:00
Matt Wright 246ab41479 Merge pull request #96 from invernizzi/develop
NextFormMixin security bug fixed: open redirect
2013-03-05 13:58:08 -08:00
Luca Invernizzi 48dd3fa5bf NextFormMixin security bug fixed: open redirect
NextFormMixin was missing validations check on redirection [1]. Only internal redirections
are now allowed.
Attack Example: http://127.0.0.1:5000/login?next=http://google.com (it should not redirect to google.com)
wq
[1] https://www.owasp.org/index.php/Top_10_2010-A10-Unvalidated_Redirects_and_Forwards
2013-03-05 21:20:45 +00:00
Matt Wright 7db5fe32a8 Turn on testing flag for test app 2013-03-04 14:57:29 -05:00
Matt Wright 38a1dfa336 Merge pull request #85 from chrishaines/template_list
Template paths can be specified in config
2013-03-03 18:38:35 -08:00
Matt Wright 8b41b531a6 Merge pull request #91 from intonarumori/develop
added option to disable register email
2013-02-20 15:06:46 -08:00
rumori 4a048a4918 updated doc with new registration email option 2013-02-20 17:37:30 +01:00
rumori ae64370478 added option to disable register email 2013-02-20 17:04:47 +01:00
Matt Wright b03f355fae Merge pull request #88 from andreev-artem/develop
Password should be encoded as 'utf-8' before creating hmac to support passwords with non-latin symbols
2013-02-03 12:12:04 -08:00
Artem Andreev 8085e0031e Password should be encoded as 'utf-8' before creating hmac to support passwords with non-latin symbols 2013-02-03 22:14:32 +04:00
Chris Haines dbc2dcc625 Removed flask-script as a dependency in setup.py 2013-02-01 19:43:13 -05:00
Chris Haines ad6227006e Fixed lack of newline in requirements.txt 2013-02-01 19:41:42 -05:00
Chris Haines 4f414cf70f Merge branch 'develop' of git://github.com/mattupstate/flask-security into template_list
Conflicts:
	requirements.txt
2013-02-01 19:40:01 -05:00
Matt Wright adb2680289 Add change password endpoint 2013-02-01 18:21:43 -05:00
Matt Wright f1f621d178 Merge pull request #78 from eskil/change_password_form
Change password form
2013-02-01 15:16:45 -08:00
Matt Wright 996e162d11 Merge pull request #70 from eskil/formsdocs
Clarify user model/register form interaction in docs.
2013-02-01 14:59:40 -08:00
Matt Wright 840f72a589 Merge pull request #82 from maebert/flask-peewee
Flask-Peewee support
2013-02-01 14:44:52 -08:00
Matt Wright eca83f83ac Test configured login form messages better 2013-02-01 17:37:03 -05:00
Matt Wright 0f070b494a Merge branch 'feature/login_form_messages' into develop 2013-02-01 17:33:22 -05:00
Matt Wright 0ea1e0c03d Update CHANGES 2013-02-01 17:33:15 -05:00
Matt Wright c49d9b57ed Make login form messages configurable 2013-02-01 17:32:54 -05:00
Matt Wright 8d7e0f5190 Merge branch 'feature/csrf_testing' into develop 2013-02-01 17:25:36 -05:00
Matt Wright 012781103a Update CHANGES 2013-02-01 17:25:30 -05:00
Matt Wright 34b3bf9e80 Fix CSRF functionality for LoginForm
The login form was not respecting csrf validation. I've adjusted the tests as well to always send a CSRF token along. This now requires all requests to pass a csrf token. If performing plain AJAX requests the token will have to be extracted from the form in some way. Fixes #86
2013-02-01 17:23:18 -05:00
Chris Haines 4c203a4777 Removed extra line. 2013-01-29 22:29:31 -05:00
Chris Haines 02c49ee423 Paths for templates are now configurable 2013-01-29 22:24:11 -05:00
Chris Haines 3f243d1c11 Updated flask version and added flask-script to setup.py. 2013-01-29 19:51:25 -05:00
Chris Haines cf37d59d47 Updated requirements to reflect packages in setup.py 2013-01-29 19:27:05 -05:00
Chris Haines 75c10c75d9 Fixed typo in requirements.txt 2013-01-29 19:13:20 -05:00
Manuel Ebert 51e06bdbb0 Fixes typo in find_or_create_role 2013-01-29 15:46:59 -08:00
Manuel Ebert 462fb1ae7e Convenience method for finding or creating a role
`datastore. find_or_create_role("admin")` will now always return a role
with the name admin; useful for initialisation,
2013-01-28 18:58:11 -08:00
Manuel Ebert aea5b91649 Method stub parameters and docs for find_role didn't match implementations. 2013-01-28 18:57:19 -08:00
Manuel Ebert e3e96d546a Another small fix for the peewee docs 2013-01-25 16:59:48 -08:00
Manuel Ebert 46c2355a7e FIxes peewee description on quickstart 2013-01-25 16:58:21 -08:00
Manuel Ebert 423e430e04 Docs for flask-peewee 2013-01-25 16:54:18 -08:00
Manuel Ebert 70b11d9015 Unit-tests for flask-peewee 2013-01-25 16:53:01 -08:00
Manuel Ebert 5687f2f5a9 Adds support for flask-peewee 2013-01-25 16:52:50 -08:00
Matt Wright b82a8d681d Merge pull request #79 from maebert/develop
Fixes dependencies
2013-01-25 08:08:05 -08:00
Manuel Ebert 29af22bd6e Updated requirements.txt to reflect setup.py
Also fixes a typo (`passlib=1.5.3` is not a valid line)
2013-01-24 16:24:50 -08:00
Manuel Ebert 68f8127286 Updates dependencies to Flask>=0.9
after_this_request was introduced in 0.9-dev
2013-01-24 16:23:48 -08:00
Eskil Heyn Olsen 647e1a06d5 Add test to ensure it is disabled 2013-01-17 20:35:23 -08:00
Matt Wright 84759b5dbd Merge pull request #73 from apahomov/multiple-auth-mechanisms
Multiple auth mechanisms
2013-01-16 19:37:55 -08:00
apahomov 39f62374aa Added tests 2013-01-15 10:30:48 +04:00
Matt Wright 4de2cbcf9f Merge pull request #72 from eskil/please_login_msg
Fix passing category to login required message
2013-01-14 07:17:05 -08:00
apahomov 3f9ca423bd Calling auth methods 2013-01-14 16:11:09 +04:00
apahomov bbed019ca5 Add auth_required decorator that allows multiple auth mechanisms 2013-01-14 15:45:18 +04:00
Eskil Heyn Olsen 3081d76787 Fix passing category to login required message 2013-01-13 23:25:16 -08:00
apahomov 1a0ddff82b Get auth token from JSON request. 2013-01-14 10:54:48 +04:00
Matt Wright e9b40a12c8 Fix for Python 2.6 2013-01-14 00:26:46 -05:00
Eskil Heyn Olsen 3adb4afd60 Minor wording fix 2013-01-12 23:58:47 -08:00
Eskil Heyn Olsen c7af67f3a3 Fix signals test, following redirect is a trap 2013-01-12 23:53:31 -08:00
Eskil Heyn Olsen cca9298e74 Fix and test redir to configurable view post change 2013-01-12 19:56:50 -08:00
Eskil Heyn Olsen 4f9e23e0bc Fix email forms to have externally available links 2013-01-12 19:34:53 -08:00
Eskil Heyn Olsen ded62a556b Add a password-changed signal 2013-01-12 19:03:02 -08:00
Eskil Heyn Olsen 508f4d1b52 Fix change password form 2013-01-12 15:57:52 -08:00
Eskil Heyn Olsen 050ccb847a Forgot to add form 2013-01-12 14:55:30 -08:00
Eskil Heyn Olsen b67e61d625 Change password form 2013-01-12 14:40:42 -08:00
Eskil Heyn Olsen 9a47ec1ed9 Working on change password form 2013-01-11 22:35:54 -08:00
Eskil Heyn Olsen c5c27768f2 First pieces of change password form 2013-01-11 19:07:07 -08:00
Eskil Heyn Olsen 4dd944a8c1 Another fix to docs 2013-01-11 18:38:05 -08:00
Eskil Heyn Olsen e22aff51a4 Clarify user model/register form interaction in docs. 2013-01-10 07:57:44 -08:00
Matt Wright 6adc26a897 Get rid of strftime in favor of total_seconds. Fixes #67 2013-01-09 14:02:42 -05:00
Matt Wright 9726726e5a Merge pull request #68 from eskil/fix_tests
remove import that was left by accident
2013-01-09 07:35:07 -08:00
Eskil Heyn Olsen 6202abb75c remove import that was left by accident 2013-01-08 18:41:16 -08:00
Matt Wright 3c65923d33 Merge pull request #66 from eskil/registerform
Update documentation reg. configurable forms
2013-01-08 08:16:30 -08:00
Eskil Heyn Olsen daed6e1064 Update documentation reg. configurable forms 2013-01-08 07:45:13 -08:00
Matt Wright f566f41fb3 Merge pull request #58 from eskil/registerform
Configurable forms
2013-01-08 07:10:14 -08:00
Eskil Heyn Olsen e4190a0315 Add kwargs for configurable forms.
Specifically list out the kwargs so we'll get an interpreter error
on a bad name.
2013-01-07 21:43:33 -08:00
Eskil Heyn Olsen ca0d1d0b50 All unit-tests for configurable forms 2013-01-07 21:43:27 -08:00
Matt Wright 4e57226a1b Merge pull request #65 from KangOl/develop
take advantage of passlib CryptContext
2013-01-07 21:34:08 -08:00
Christophe Simonis a89b76d648 do not break API. add a new function to verify and update password 2013-01-08 01:01:02 +01:00
Christophe Simonis d0497fc886 update password automatically 2013-01-08 00:49:20 +01:00
Christophe Simonis a1c007599f allow change of hash scheme 2013-01-08 00:15:21 +01:00
Eskil Heyn Olsen 81040a57a6 Views get forms from _security 2013-01-06 20:20:06 -08:00
Eskil Heyn Olsen 09fe5a2cb7 Make test_app take kwargs 2013-01-06 20:00:13 -08:00
Matt Wright d760aa41c5 Merge branch 'master' into develop 2013-01-06 21:01:53 -05:00
Matt Wright 53576c6013 Set Flask-Login messages 2013-01-06 21:01:35 -05:00
Matt Wright 029466830d Bump version number to 1.5.4 2013-01-06 20:43:02 -05:00
Matt Wright 31595196fc Update CHANGES 2013-01-06 20:42:51 -05:00
Matt Wright 2a0b582911 Change csrf_enabled parameter in forms to check for incoming JSON data. Fix #63 2013-01-06 20:41:01 -05:00
Matt Wright 63fb30daf8 Merge pull request #64 from eskil/signals
Document and unit-test signals.
2013-01-06 17:38:44 -08:00
Eskil Heyn Olsen 675b29b4fe Minor style fixes 2013-01-06 14:40:09 -08:00
Eskil Heyn Olsen 075ccf6a82 Add some more tests of signals 2013-01-06 14:33:15 -08:00
Eskil Heyn Olsen ae6f3b6753 Document and unit-test the signals.
Adds description of signals to docmentation. Adds unit-tests of
signal behaviour and tests parameters.
2013-01-06 14:12:18 -08:00
Eskil Heyn Olsen 1a87a4cd0c Fix to RegisterForm.to_dict.
Only add fields that are also attributes on the
datastorage.user_model.
2013-01-03 23:29:50 -08:00
Eskil Heyn Olsen f83092865b Configurable forms, issue:49 2013-01-03 22:00:29 -08:00
Eskil Heyn Olsen b15736accd RegisterFormMixin can now to_dict all fields.
It adds a to_dict function that uses inspect to add all wtf Field
to the returned dict. This allows extensions to the register form
to easily add fields that will be passed to the datastore's
create_user function.
2013-01-03 19:07:00 -08:00
Matt Wright 79ce6f00d3 Merge pull request #56 from eskil/fix_add_role
Fix query for find_user
2013-01-01 15:14:04 -08:00
Eskil Heyn Olsen 7f43acc167 Fix query for find_user 2013-01-01 11:28:31 -08:00
Matt Wright f8fbd6cec8 Bump version number to 1.5.3 2012-12-23 16:47:20 -05:00
Matt Wright 9d11dd0787 Update dependency rules 2012-12-23 16:47:14 -05:00
Matt Wright 4c31b6b689 Merge pull request #51 from joshpurvis/mongo-quickstart
Reorganized Quickstart and added MongoDB example.
2012-12-19 08:26:42 -08:00
Josh Purvis d468fae0ea Reorganized Quickstart and added MongoDB example. 2012-12-19 11:17:06 -05:00
Matt Wright 3a5a1b4f52 Bump version number to 1.5.2 2012-12-11 15:15:01 -05:00
Matt Wright 0724bd12a5 Remove stray file 2012-12-11 15:13:35 -05:00
Matt Wright c7e3e642fa Update CHANGES 2012-12-11 15:11:51 -05:00
Matt Wright ee4c8f2a3f Fix login_user method to actually return a True or False value as mentioned in mattupstate/flask-social-example#8 2012-12-11 15:10:26 -05:00
Matt Wright 1092ffc9ea Fix #45 2012-11-26 16:21:05 -05:00
Matt Wright f4b6eb9869 Fix table 2012-11-26 15:03:18 -05:00
44 changed files with 1806 additions and 276 deletions
+6
View File
@@ -25,3 +25,9 @@ pip-log.txt
#Mr Developer #Mr Developer
.mr.developer.cfg .mr.developer.cfg
#Virtualenv
env/
#Editor temporaries
*~
+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-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;'
+88 -3
View File
@@ -3,6 +3,91 @@ 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.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
-------------
Released March 13th 2013
- Added Flask-Pewee support
- Password hashing is now more flexible and can be changed to a different type at will
- Flask-Login messages are configurable
- AJAX requests must now send a CSRF token for security reasons
- Form messages are now configurable
- Forms can now be extended with more fields
- Added change password endpoint
- Added the user to the request context when successfully authenticated via http basic and token auth
- The Flask-Security blueprint subdomain is now configurable
- Redirects to other domains are now not allowed during requests that may redirect
- Template paths can be configured
- The welcome/register email can now optionally be sent to the user
- Passwords can now contain non-latin characters
- Fixed a bug when confirming an account but the account has been deleted
Version 1.5.4
-------------
Released January 6th 2013
- Fix bug in forms with `csrf_enabled` parameter not accounting attempts to login using JSON data
Version 1.5.3
-------------
Released December 23rd 2012
- Change dependency requirement
Version 1.5.2
-------------
Released December 11th 2012
- Fix a small bug in `flask_security.utils.login_user` method
Version 1.5.1 Version 1.5.1
------------- -------------
@@ -49,10 +134,10 @@ Version 1.2.0
Released March 12th 2012 Released March 12th 2012
- Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a - Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a
boolean value to specify if Flask-Security should flash messages or not. boolean value to specify if Flask-Security should flash messages or not.
Version 1.1.0 Version 1.1.0
------------- -------------
Initial release Initial release
+52 -2
View File
@@ -49,9 +49,59 @@ Datastores
:members: :members:
:inherited-members: :inherited-members:
.. autoclass:: flask_security.datastore.PeeweeUserDatastore
:members:
:inherited-members:
Signals Signals
------- -------
See the `Flask documentation on signals`_ for information on how to use these
signals in your code.
See the documentation for the signals provided by the Flask-Login and See the documentation for the signals provided by the Flask-Login and
Flask-Principal extensions. Flask-Security does not provide any additional Flask-Principal extensions. In addition to those signals, Flask-Security
signals. sends the following signals.
.. data:: user_registered
Sent when a user registers on the site. It is passed a dict with
the `user` and `confirm_token`, the user being logged in and the
(if so configured) the confirmation token issued.
.. data:: user_confirmed
Sent when a user is confirmed. It is passed `user`, which is the
user being confirmed.
.. data:: confirm_instructions_sent
Sent when a user requests confirmation instructions. It is passed
the `user`.
.. data:: login_instructions_sent
Sent when passwordless login is used and user logs in. It is passed
a dict with the `user` and `login_token`, the user being logged in
and the (if so configured) the login token issued.
.. data:: password_reset
Sent when a user completes a password reset. It is passed the
`user`.
.. data:: password_changed
Sent when a user completes a password change. It is passed the
`user`.
.. data:: reset_password_instructions_sent
Sent when a user requests a password reset. It is passed a dict
with the `user` and `token`, the user being logged in and
the (if so configured) the reset token issued.
All signals are also passed a `app` keyword argument, which is the
current application.
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
+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.5.1' version = '1.6.5'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version
+39
View File
@@ -96,6 +96,35 @@ URLs and Views
=============================== ================================================ =============================== ================================================
Template Paths
--------------
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
======================================== =======================================
``SECURITY_FORGOT_PASSWORD_TEMPLATE`` Specifies the path to the template for
the forgot password page. Defaults to
``security/forgot_password.html``.
``SECURITY_LOGIN_USER_TEMPLATE`` Specifies the path to the template for
the user login page. Defaults to
``security/login_user.html``.
``SECURITY_REGISTER_USER_TEMPLATE`` Specifies the path to the template for
the user registration page. Defaults to
``security/register_user.html``.
``SECURITY_RESET_PASSWORD_TEMPLATE`` Specifies the path to the template for
the reset password page. Defaults to
``security/reset_password.html``.
``SECURITY_SEND_CONFIRMATION_TEMPLATE`` Specifies the path to the template for
the resend confirmation instructions
page. Defaults to
``security/send_confirmation.html``.
``SECURITY_SEND_LOGIN_TEMPLATE`` Specifies the path to the template for
the send login instructions page for
passwordless logins. Defaults to
``security/send_login.html``.
======================================== =======================================
Feature Flags Feature Flags
------------- -------------
@@ -127,6 +156,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
@@ -150,6 +183,7 @@ Email
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email ``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
confirmation message. Defaults to confirmation message. Defaults to
``Please confirm your email`` ``Please confirm your email``
=========================================== ====================================
Miscellaneous Miscellaneous
------------- -------------
@@ -157,6 +191,8 @@ Miscellaneous
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}| .. 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 ``SECURITY_CONFIRM_EMAIL_WITHIN`` Specifies the amount of time a user has
before their confirmation link expires. before their confirmation link expires.
Always pluralized the time unit for this Always pluralized the time unit for this
@@ -188,4 +224,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``.
======================================= ======================================== ======================================= ========================================
+48 -2
View File
@@ -18,6 +18,7 @@ following is a list of view templates:
* `security/login_user.html` * `security/login_user.html`
* `security/register_user.html` * `security/register_user.html`
* `security/reset_password.html` * `security/reset_password.html`
* `security/change_password.html`
* `security/send_confirmation.html` * `security/send_confirmation.html`
* `security/send_login.html` * `security/send_login.html`
@@ -26,9 +27,11 @@ Overriding these templates is simple:
1. Create a folder named ``security`` within your application's templates folder 1. Create a folder named ``security`` within your application's templates folder
2. Create a template with the same name for the template you wish to override 2. Create a template with the same name for the template you wish to override
You can also specify custom template file paths in the :doc:`configuration <configuration>`.
Each template is passed a template context object that includes the following, Each template is passed a template context object that includes the following,
including the objects/values that are passed to the template by the main including the objects/values that are passed to the template by the main
Flask application context processory: Flask application context processor:
* ``<template_name>_form``: A form object for the view * ``<template_name>_form``: A form object for the view
* ``security``: The Flask-Security extension object * ``security``: The Flask-Security extension object
@@ -55,10 +58,51 @@ The following is a list of all the available context processor decorators:
* ``login_context_processor``: Login view * ``login_context_processor``: Login view
* ``register_context_processor``: Register view * ``register_context_processor``: Register view
* ``reset_password_context_processor``: Reset password view * ``reset_password_context_processor``: Reset password view
* ``change_password_context_processor``: Reset password view
* ``send_confirmation_context_processor``: Send confirmation view * ``send_confirmation_context_processor``: Send confirmation view
* ``send_login_context_processor``: Send login view * ``send_login_context_processor``: Send login view
Forms
-----
All forms can be overridden. For each form used, you can specify a
replacement class. This allows you to add extra fields to the
register form or override validators::
from flask_security.forms import RegisterForm
class ExtendedRegisterForm(RegisterForm):
first_name = TextField('First Name', [Required()])
last_name = TextField('Last Name', [Required()])
security = Security(app, user_datastore,
register_form=ExtendedRegisterForm)
For the ``register_form`` and ``confirm_register_form``, each field is
passed to the user model (as kwargs) when a user is created. In the
above case, the ``first_name`` and ``last_name`` fields are passed
directly to the model, so the model should look like::
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
first_name = db.Column(db.String(255))
last_name = db.Column(db.String(255))
The following is a list of all the available form overrides:
* ``login_form``: Login form
* ``confirm_register_form``: Confirmable register form
* ``register_form``: Register form
* ``forgot_password_form``: Forgot password form
* ``reset_password_form``: Reset password form
* ``change_password_form``: Reset password form
* ``send_confirmation_form``: Send confirmation form
* ``passwordless_login_form``: Passwordless login form
Emails Emails
------ ------
@@ -73,6 +117,8 @@ The following is a list of email templates:
* `security/mail/reset_instructions.html` * `security/mail/reset_instructions.html`
* `security/mail/reset_instructions.txt` * `security/mail/reset_instructions.txt`
* `security/mail/reset_notice.html` * `security/mail/reset_notice.html`
* `security/mail/change_notice.txt`
* `security/mail/change_notice.html`
* `security/mail/reset_notice.txt` * `security/mail/reset_notice.txt`
* `security/mail/welcome.html` * `security/mail/welcome.html`
* `security/mail/welcome.txt` * `security/mail/welcome.txt`
@@ -93,4 +139,4 @@ templates you can specify an email context processor with the
# This processor is added to all emails # This processor is added to all emails
@security.email_context_processor @security.email_context_processor
def security_mail_processor(): def security_mail_processor():
return dict(hello="world") return dict(hello="world")
+7 -6
View File
@@ -10,7 +10,7 @@ Flask application. They include:
4. Basic HTTP authentication 4. Basic HTTP authentication
5. Token based authentication 5. Token based authentication
6. Token based account activation (optional) 6. Token based account activation (optional)
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)
@@ -21,16 +21,17 @@ 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/>`_
Additionally, it assumes you'll be using a common library for your database Additionally, it assumes you'll be using a common library for your database
connections and model definitions. Flask-Security supports the following Flask connections and model definitions. Flask-Security supports the following Flask
extensions out of the box for data persistance: extensions out of the box for data persistence:
1. `Flask-SQLAlchemy <http://packages.python.org/Flask-SQLAlchemy/>`_ 1. `Flask-SQLAlchemy <http://pypi.python.org/pypi/flask-sqlalchemy/>`_
2. `Flask-MongoEngine <http://packages.python.org/Flask-MongoEngine/>`_ 2. `Flask-MongoEngine <http://pypi.python.org/pypi/flask-mongoengine/>`_
3. `Flask-Peewee <http://pypi.python.org/pypi/flask-peewee/>`_
.. include:: contents.rst.inc .. include:: contents.rst.inc
+7 -7
View File
@@ -1,12 +1,12 @@
Models Models
====== ======
Flask-Security assumes you'll be using libraries such as SQLAlchemy or Flask-Security assumes you'll be using libraries such as SQLAlchemy,
MongoEngine to define a data model that includes a `User` and `Role` model. The MongoEngine or Peewee to define a data model that includes a `User` and
fields on your models must follow a particular convention depending on the `Role` model. The fields on your models must follow a particular convention
functionality your app requires. Aside from this, you're free to add any depending on the functionality your app requires. Aside from this, you're
additional fields to your model(s) if you want. At the bear minimum your `User` free to add any additional fields to your model(s) if you want. At the bear
and `Role` model should include the following fields: minimum your `User` and `Role` model should include the following fields:
**User** **User**
@@ -48,4 +48,4 @@ additional fields:
* ``current_login_at`` * ``current_login_at``
* ``last_login_ip`` * ``last_login_ip``
* ``current_login_ip`` * ``current_login_ip``
* ``login_count`` * ``login_count``
+176 -24
View File
@@ -1,26 +1,35 @@
Quick Start Quick Start
=========== ===========
- `Basic SQLAlchemy Application <#basic-sqlalchemy-application>`_
- `Basic MongoEngine Application <#basic-mongoengine-application>`_
- `Basic Peewee Application <#basic-peewee-application>`_
- `Mail Configuration <#mail-configuration>`_
Installation Basic SQLAlchemy Application
------------ =============================
Install requirements: SQLAlchemy Install requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ mkvirtualenv <your-app-name> ::
$ pip install flask-security, flask-sqlalchemy
$ mkvirtualenv <your-app-name>
$ pip install flask-security flask-sqlalchemy
Basic Application SQLAlchemy Application
----------------- ~~~~~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as possible The following code sample illustrates how to get started as quickly as
using SQLAlchemy.:: possible using SQLAlchemy:
::
from flask import Flask, render_template from flask import Flask, render_template
from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, SQLAlchemyUserDatastore, \ from flask.ext.security import Security, SQLAlchemyUserDatastore, \
UserMixin, RoleMixin UserMixin, RoleMixin, login_required
# Create app # Create app
app = Flask(__name__) app = Flask(__name__)
@@ -63,21 +72,169 @@ using SQLAlchemy.::
# Views # Views
@app.route('/') @app.route('/')
@login_required
def home(): def home():
return render_template('index.html') return render_template('index.html')
if __name__ == '__main__': if __name__ == '__main__':
app.run() app.run()
Basic MongoEngine Application
==============================
MongoEngine Install requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
$ mkvirtualenv <your-app-name>
$ pip install flask-security flask-mongoengine
MongoEngine Application
~~~~~~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as
possible using MongoEngine:
::
from flask import Flask, render_template
from flask.ext.mongoengine import MongoEngine
from flask.ext.security import Security, MongoEngineUserDatastore, \
UserMixin, RoleMixin, login_required
# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
# MongoDB Config
app.config['MONGODB_DB'] = 'mydatabase'
app.config['MONGODB_HOST'] = 'localhost'
app.config['MONGODB_PORT'] = 27017
# Create database connection object
db = MongoEngine(app)
class Role(db.Document, RoleMixin):
name = db.StringField(max_length=80, unique=True)
description = db.StringField(max_length=255)
class User(db.Document, UserMixin):
email = db.StringField(max_length=255)
password = db.StringField(max_length=255)
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
# Setup Flask-Security
user_datastore = MongoEngineUserDatastore(db, User, Role)
security = Security(app, user_datastore)
# Create a user to test with
@app.before_first_request
def create_user():
user_datastore.create_user(email='matt@nobien.net', password='password')
# Views
@app.route('/')
@login_required
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run()
Basic Peewee Application
========================
Peewee Install requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
$ mkvirtualenv <your-app-name>
$ pip install flask-security flask-peewee
Peewee Application
~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as
possible using Peewee:
::
from flask import Flask, render_template
from flask_peewee.db import Database
from peewee import *
from flask.ext.security import Security, PeeweeUserDatastore, \
UserMixin, RoleMixin, login_required
# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['DATABASE'] = {
'name': 'example.db',
'engine': 'peewee.SqliteDatabase',
}
# Create database connection object
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
description = TextField(null=True)
class User(db.Model, UserMixin):
email = TextField()
password = TextField()
active = BooleanField(default=True)
confirmed_at = DateTimeField(null=True)
class UserRoles(db.Model):
# Because peewee does not come with built-in many-to-many
# relationships, we need this intermediary class to link
# user to roles.
user = ForeignKeyField(User, related_name='roles')
role = ForeignKeyField(Role, related_name='users')
name = property(lambda self: self.role.name)
description = property(lambda self: self.role.description)
# Setup Flask-Security
user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles)
security = Security(app, user_datastore)
# Create a user to test with
@app.before_first_request
def create_user():
for Model in (Role, User, UserRoles):
Model.drop_table(fail_silently=True)
Model.create_table(fail_silently=True)
user_datastore.create_user(email='matt@nobien.net', password='password')
# Views
@app.route('/')
@login_required
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run()
Mail Configuration Mail Configuration
------------------ ===================
Flask-Security integrates with Flask-Mail to handle all email communications Flask-Security integrates with Flask-Mail to handle all email
between user and site, so it's important to configure Flask-Mail with your communications between user and site, so it's important to configure
email server details so Flask-Security can talk with Flask-Mail correctly. Flask-Mail with your email server details so Flask-Security can talk
with Flask-Mail correctly.
The following code illustrates a basic setup, which could be added to the The following code illustrates a basic setup, which could be added to
basic application code in the previous section:: the basic application code in the previous section::
# At top of file # At top of file
from flask_mail import Mail from flask_mail import Mail
@@ -90,11 +247,6 @@ basic application code in the previous section::
app.config['MAIL_PASSWORD'] = 'password' app.config['MAIL_PASSWORD'] = 'password'
mail = Mail(app) mail = Mail(app)
To learn more about the various Flask-Mail settings to configure it to work To learn more about the various Flask-Mail settings to configure it to
with your particular email server configuration, please see the work with your particular email server configuration, please see the
`Flask-Mail documentation <http://packages.python.org/Flask-Mail/>`_ `Flask-Mail documentation <http://packages.python.org/Flask-Mail/>`_.
+3 -3
View File
@@ -10,12 +10,12 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
__version__ = '1.5.1' __version__ = '1.6.5'
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore 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, \
+44
View File
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.changeable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security recoverable module
:copyright: (c) 2012 by Matt Wright.
:author: Eskil Heyn Olsen
:license: MIT, see LICENSE for more details.
"""
from flask import current_app as app, request
from werkzeug.local import LocalProxy
from .signals import password_changed
from .utils import send_mail, encrypt_password, config_value
# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
def send_password_changed_notice(user):
"""Sends the password changed notice email for the specified user.
:param user: The user to send the notice to
"""
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE'), user.email,
'change_notice', user=user)
def change_user_password(user, password):
"""Change the specified user's password
:param user: The user to change_password
:param password: The unencrypted new password
"""
user.password = encrypt_password(password)
_datastore.put(user)
send_password_changed_notice(user)
password_changed.send(user, app=app._get_current_object())
+5 -6
View File
@@ -15,7 +15,7 @@ from flask import current_app as app, request
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
@@ -53,7 +52,7 @@ def generate_confirmation_token(user):
:param user: The user to work with :param user: The user to work with
""" """
data = [user.id, md5(user.email)] data = [str(user.id), md5(user.email)]
return _security.confirm_serializer.dumps(data) return _security.confirm_serializer.dumps(data)
@@ -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)
+95 -15
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
@@ -21,6 +21,9 @@ 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, \
ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \
SendConfirmationForm, PasswordlessLoginForm
# Convenient references # Convenient references
_security = LocalProxy(lambda: current_app.extensions['security']) _security = LocalProxy(lambda: current_app.extensions['security'])
@@ -30,6 +33,7 @@ _security = LocalProxy(lambda: current_app.extensions['security'])
_default_config = { _default_config = {
'BLUEPRINT_NAME': 'security', 'BLUEPRINT_NAME': 'security',
'URL_PREFIX': None, 'URL_PREFIX': None,
'SUBDOMAIN': None,
'FLASH_MESSAGES': True, 'FLASH_MESSAGES': True,
'PASSWORD_HASH': 'plaintext', 'PASSWORD_HASH': 'plaintext',
'PASSWORD_SALT': None, 'PASSWORD_SALT': None,
@@ -37,6 +41,7 @@ _default_config = {
'LOGOUT_URL': '/logout', 'LOGOUT_URL': '/logout',
'REGISTER_URL': '/register', 'REGISTER_URL': '/register',
'RESET_URL': '/reset', 'RESET_URL': '/reset',
'CHANGE_URL': '/change',
'CONFIRM_URL': '/confirm', 'CONFIRM_URL': '/confirm',
'POST_LOGIN_VIEW': '/', 'POST_LOGIN_VIEW': '/',
'POST_LOGOUT_VIEW': '/', 'POST_LOGOUT_VIEW': '/',
@@ -44,12 +49,21 @@ _default_config = {
'POST_REGISTER_VIEW': None, 'POST_REGISTER_VIEW': None,
'POST_CONFIRM_VIEW': None, 'POST_CONFIRM_VIEW': None,
'POST_RESET_VIEW': None, 'POST_RESET_VIEW': None,
'POST_CHANGE_VIEW': None,
'UNAUTHORIZED_VIEW': None, 'UNAUTHORIZED_VIEW': None,
'FORGOT_PASSWORD_TEMPLATE': 'security/forgot_password.html',
'LOGIN_USER_TEMPLATE': 'security/login_user.html',
'REGISTER_USER_TEMPLATE': 'security/register_user.html',
'RESET_PASSWORD_TEMPLATE': 'security/reset_password.html',
'SEND_CONFIRMATION_TEMPLATE': 'security/send_confirmation.html',
'SEND_LOGIN_TEMPLATE': 'security/send_login.html',
'CONFIRMABLE': False, 'CONFIRMABLE': False,
'REGISTERABLE': False, 'REGISTERABLE': False,
'RECOVERABLE': False, 'RECOVERABLE': False,
'TRACKABLE': False, 'TRACKABLE': False,
'PASSWORDLESS': False, 'PASSWORDLESS': False,
'CHANGEABLE': False,
'SEND_REGISTER_EMAIL': True,
'LOGIN_WITHIN': '1 days', 'LOGIN_WITHIN': '1 days',
'CONFIRM_EMAIL_WITHIN': '5 days', 'CONFIRM_EMAIL_WITHIN': '5 days',
'RESET_PASSWORD_WITHIN': '5 days', 'RESET_PASSWORD_WITHIN': '5 days',
@@ -60,12 +74,15 @@ _default_config = {
'CONFIRM_SALT': 'confirm-salt', 'CONFIRM_SALT': 'confirm-salt',
'RESET_SALT': 'reset-salt', 'RESET_SALT': 'reset-salt',
'LOGIN_SALT': 'login-salt', 'LOGIN_SALT': 'login-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',
'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions', 'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions',
'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset', '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'
} }
@@ -76,8 +93,10 @@ _default_messages = {
'EMAIL_CONFIRMED': ('Thank you. Your email has been confirmed.', 'success'), 'EMAIL_CONFIRMED': ('Thank you. Your email has been confirmed.', 'success'),
'ALREADY_CONFIRMED': ('Your email has already been confirmed.', 'info'), 'ALREADY_CONFIRMED': ('Your email has already been confirmed.', 'info'),
'INVALID_CONFIRMATION_TOKEN': ('Invalid confirmation token.', 'error'), 'INVALID_CONFIRMATION_TOKEN': ('Invalid confirmation token.', 'error'),
'ALREADY_CONFIRMED': ('This email has already been confirmed', 'info'), 'EMAIL_ALREADY_ASSOCIATED': ('%(email)s is already associated with an account.', 'error'),
'PASSWORD_MISMATCH': ('Password does not match', 'error'), 'PASSWORD_MISMATCH': ('Password does not match', 'error'),
'RETYPE_PASSWORD_MISMATCH': ('Passwords do not match', 'error'),
'INVALID_REDIRECT': ('Redirections outside the domain are forbidden', 'error'),
'PASSWORD_RESET_REQUEST': ('Instructions to reset your password have been sent to %(email)s.', 'info'), 'PASSWORD_RESET_REQUEST': ('Instructions to reset your password have been sent to %(email)s.', 'info'),
'PASSWORD_RESET_EXPIRED': ('You did not reset your password within %(within)s. New instructions have been sent to %(email)s.', 'error'), 'PASSWORD_RESET_EXPIRED': ('You did not reset your password within %(within)s. New instructions have been sent to %(email)s.', 'error'),
'INVALID_RESET_PASSWORD_TOKEN': ('Invalid reset password token.', 'error'), 'INVALID_RESET_PASSWORD_TOKEN': ('Invalid reset password token.', 'error'),
@@ -88,8 +107,38 @@ _default_messages = {
'LOGIN_EMAIL_SENT': ('Instructions to login have been sent to %(email)s.', 'success'), 'LOGIN_EMAIL_SENT': ('Instructions to login have been sent to %(email)s.', 'success'),
'INVALID_LOGIN_TOKEN': ('Invalid login token.', 'error'), 'INVALID_LOGIN_TOKEN': ('Invalid login token.', 'error'),
'DISABLED_ACCOUNT': ('Account is disabled.', 'error'), 'DISABLED_ACCOUNT': ('Account is disabled.', 'error'),
'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'),
'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'),
'PASSWORD_NOT_PROVIDED': ('Password not provided', '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'), '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_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'),
'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'),
}
_allowed_password_hash_schemes = [
'bcrypt',
'des_crypt',
'pbkdf2_sha256',
'pbkdf2_sha512',
'sha256_crypt',
'sha512_crypt',
# And always last one...
'plaintext'
]
_default_forms = {
'login_form': LoginForm,
'confirm_register_form': ConfirmRegisterForm,
'register_form': RegisterForm,
'forgot_password_form': ForgotPasswordForm,
'reset_password_form': ResetPasswordForm,
'change_password_form': ChangePasswordForm,
'send_confirmation_form': SendConfirmationForm,
'passwordless_login_form': PasswordlessLoginForm,
} }
@@ -105,8 +154,7 @@ def _token_loader(token):
return user return user
except: except:
pass pass
return AnonymousUser()
return None
def _identity_loader(): def _identity_loader():
@@ -131,6 +179,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)
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
@@ -143,7 +199,10 @@ def _get_principal(app):
def _get_pwd_context(app): def _get_pwd_context(app):
pw_hash = cv('PASSWORD_HASH', app=app) pw_hash = cv('PASSWORD_HASH', app=app)
return CryptContext(schemes=[pw_hash], default=pw_hash) if pw_hash not in _allowed_password_hash_schemes:
allowed = ', '.join(_allowed_password_hash_schemes[:-1]) + ' and ' + _allowed_password_hash_schemes[-1]
raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed))
return CryptContext(schemes=_allowed_password_hash_schemes, default=pw_hash)
def _get_serializer(app, name): def _get_serializer(app, name):
@@ -170,6 +229,10 @@ def _get_state(app, datastore, **kwargs):
_send_mail_task=None _send_mail_task=None
)) ))
for key, value in _default_forms.items():
if key not in kwargs or not kwargs[key]:
kwargs[key] = value
return _SecurityState(**kwargs) return _SecurityState(**kwargs)
@@ -184,8 +247,7 @@ class RoleMixin(object):
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):
@@ -204,14 +266,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):
@@ -251,6 +315,9 @@ class _SecurityState(object):
def reset_password_context_processor(self, fn): def reset_password_context_processor(self, fn):
self._add_ctx_processor('reset_password', fn) self._add_ctx_processor('reset_password', fn)
def change_password_context_processor(self, fn):
self._add_ctx_processor('change_password', fn)
def send_confirmation_context_processor(self, fn): def send_confirmation_context_processor(self, fn):
self._add_ctx_processor('send_confirmation', fn) self._add_ctx_processor('send_confirmation', fn)
@@ -277,12 +344,17 @@ class Security(object):
if app is not None and datastore is not None: if app is not None and datastore is not None:
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,
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 """Initializes the Flask-Security extension for the specified
application and datastore implentation. application and datastore implentation.
:param app: The application. :param app: The application.
:param datastore: An instance of a user datastore. :param datastore: An instance of a user datastore.
:param register_blueprint: to register the Security blueprint or not.
""" """
datastore = datastore or self.datastore datastore = datastore or self.datastore
@@ -294,7 +366,15 @@ class Security(object):
identity_loaded.connect_via(app)(_on_identity_loaded) identity_loaded.connect_via(app)(_on_identity_loaded)
state = _get_state(app, datastore) state = _get_state(app, datastore,
login_form=login_form,
confirm_register_form=confirm_register_form,
register_form=register_form,
forgot_password_form=forgot_password_form,
reset_password_form=reset_password_form,
change_password_form=change_password_form,
send_confirmation_form=send_confirmation_form,
passwordless_login_form=passwordless_login_form)
if register_blueprint: if register_blueprint:
app.register_blueprint(create_blueprint(state, __name__)) app.register_blueprint(create_blueprint(state, __name__))
+94 -9
View File
@@ -44,6 +44,15 @@ class MongoEngineDatastore(Datastore):
model.delete() model.delete()
class PeeweeDatastore(Datastore):
def put(self, model):
model.save()
return model
def delete(self, model):
model.delete_instance()
class UserDatastore(object): class UserDatastore(object):
"""Abstracted user datastore. """Abstracted user datastore.
@@ -57,7 +66,7 @@ class UserDatastore(object):
def _prepare_role_modify_args(self, user, role): def _prepare_role_modify_args(self, user, role):
if isinstance(user, basestring): if isinstance(user, basestring):
user = self.find_user(email=user.email) user = self.find_user(email=user)
if isinstance(role, basestring): if isinstance(role, basestring):
role = self.find_role(role) role = self.find_role(role)
return user, role return user, role
@@ -72,12 +81,12 @@ class UserDatastore(object):
kwargs['roles'] = roles kwargs['roles'] = roles
return kwargs return kwargs
def find_user(self, **kwargs): def find_user(self, *args, **kwargs):
"""Returns a user matching the provided paramters.""" """Returns a user matching the provided parameters."""
raise NotImplementedError raise NotImplementedError
def find_role(self, **kwargs): def find_role(self, *args, **kwargs):
"""Returns a role matching the provided paramters.""" """Returns a role matching the provided name."""
raise NotImplementedError raise NotImplementedError
def add_role_to_user(self, user, role): def add_role_to_user(self, user, role):
@@ -86,12 +95,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
@@ -137,6 +145,13 @@ class UserDatastore(object):
role = self.role_model(**kwargs) role = self.role_model(**kwargs)
return self.put(role) return self.put(role)
def find_or_create_role(self, name, **kwargs):
"""Returns a role matching the given name or creates it with any
additionally provided parameters
"""
kwargs["name"] = name
return self.find_role(name) or self.create_role(**kwargs)
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."""
@@ -175,7 +190,77 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
UserDatastore.__init__(self, user_model, role_model) UserDatastore.__init__(self, user_model, role_model)
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
queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items())
query = QCombination(QCombination.AND, queries)
return self.user_model.objects(query).first()
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()
class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
"""A PeeweeD datastore implementation for Flask-Security that assumes
the use of the Flask-Peewee extension.
:param user_model: A user model class definition
:param role_model: A role model class definition
:param role_link: A model implementing the many-to-many user-role relation
"""
def __init__(self, db, user_model, role_model, role_link):
PeeweeDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
self.UserRole = role_link
def find_user(self, **kwargs):
try:
return self.user_model.filter(**kwargs).get()
except self.user_model.DoesNotExist:
return None
def find_role(self, role):
return self.role_model.filter(name=role).get()
def create_user(self, **kwargs):
"""Creates and returns a new user from the given parameters."""
roles = kwargs.pop('roles', [])
user = self.user_model(**self._prepare_create_user_args(**kwargs))
user = self.put(user)
for role in roles:
self.add_role_to_user(user, role)
return user
def add_role_to_user(self, user, role):
"""Adds a role tp a user
:param user: The user to manipulate
:param role: The role to add to the user
"""
user, role = self._prepare_role_modify_args(user, role)
result = self.UserRole.select() \
.where(self.UserRole.user == user.id, self.UserRole.role == role.id)
if result.count():
return False
else:
self.UserRole.create(user=user.id, role=role.id)
return True
def remove_role_from_user(self, user, role):
"""Removes a role from a user
:param user: The user to manipulate
:param role: The role to remove from the user
"""
user, role = self._prepare_role_modify_args(user, role)
result = self.UserRole.select() \
.where(self.UserRole.user == user, self.UserRole.role == role)
if result.count():
self.UserRole.delete().where(
self.UserRole.user == user, self.UserRole.role == role)
return True
else:
return False
+41 -3
View File
@@ -9,9 +9,10 @@
: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 from flask import current_app, Response, request, redirect, _request_ctx_stack
from flask.ext.login import current_user, login_required from flask.ext.login import current_user, login_required
from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
@@ -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
@@ -48,6 +51,8 @@ def _check_token():
args_key = _security.token_authentication_key args_key = _security.token_authentication_key
header_token = request.headers.get(header_key, None) header_token = request.headers.get(header_key, None)
token = request.args.get(args_key, header_token) token = request.args.get(args_key, header_token)
if request.json:
token = request.json.get(args_key, token)
serializer = _security.remember_token_serializer serializer = _security.remember_token_serializer
try: try:
@@ -59,16 +64,19 @@ def _check_token():
if utils.md5(user.password) == data[1]: 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
identity_changed.send(app, identity=Identity(user.id)) identity_changed.send(app, identity=Identity(user.id))
return True return True
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_password(auth.password, user.password): if user and utils.verify_and_update_password(auth.password, user):
_security.datastore.commit()
app = current_app._get_current_object() app = current_app._get_current_object()
_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
@@ -112,6 +120,36 @@ def auth_token_required(fn):
return decorated return decorated
def auth_required(*auth_methods):
"""
Decorator that protects enpoints through multiple mechanisms
Example::
@app.route('/dashboard')
@auth_required('token', 'session')
def dashboard():
return 'Dashboard'
:param auth_methods: Specified mechanisms.
"""
login_mechanisms = {
'token': lambda: _check_token(),
'basic': lambda: _check_http_auth(),
'session': lambda: current_user.is_authenticated()
}
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
mechanisms = [login_mechanisms.get(method) for method in auth_methods]
for mechanism in mechanisms:
if mechanism and mechanism():
return fn(*args, **kwargs)
return _get_unauthorized_response()
return decorated_view
return wrapper
def roles_required(*roles): def roles_required(*roles):
"""Decorator which specifies that a user must have all the specified roles. """Decorator which specifies that a user must have all the specified roles.
Example:: Example::
+126 -36
View File
@@ -9,91 +9,156 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
import inspect
import urlparse
import flask_wtf as wtf
from flask import request, current_app from flask import request, current_app
from flask.ext.wtf import Form as BaseForm, TextField, PasswordField, \ from flask_wtf import Form as BaseForm, TextField, PasswordField, \
SubmitField, HiddenField, Required, BooleanField, EqualTo, Email, \ SubmitField, HiddenField, BooleanField, ValidationError, Field
ValidationError, Length from flask_login import current_user
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from .confirmable import requires_confirmation from .confirmable import requires_confirmation
from .utils import verify_password, get_message from .utils import verify_and_update_password, get_message
# Convenient reference # Convenient reference
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore) _datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
email_required = Required(message='Email not provided') _default_field_labels = {
'email': 'Email Address',
'password': 'Password',
'remember_me': 'Remember Me',
'login': 'Login',
'retype_password': 'Retype Password',
'register': 'Register',
'send_confirmation': 'Resend Confirmation Instructions',
'recover_password': 'Recover Password',
'reset_password': 'Reset Password',
'retype_password': 'Retype Password',
'new_password': 'New Password',
'change_password': 'Change Password',
'send_login_link': 'Send Login Link'
}
email_validator = Email(message='Invalid email address')
password_required = Required(message="Password not provided") class ValidatorMixin(object):
def __call__(self, form, field):
if self.message and self.message.isupper():
self.message = get_message(self.message)[0]
return super(ValidatorMixin, self).__call__(form, field)
class EqualTo(ValidatorMixin, wtf.EqualTo):
pass
class Required(ValidatorMixin, wtf.Required):
pass
class Email(ValidatorMixin, wtf.Email):
pass
class Length(ValidatorMixin, wtf.Length):
pass
email_required = Required(message='EMAIL_NOT_PROVIDED')
email_validator = Email(message='INVALID_EMAIL_ADDRESS')
password_required = Required(message='PASSWORD_NOT_PROVIDED')
def get_form_field_label(key):
return _default_field_labels.get(key, '')
def unique_user_email(form, field): def unique_user_email(form, field):
if _datastore.find_user(email=field.data) is not None: if _datastore.find_user(email=field.data) is not None:
raise ValidationError(field.data + msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0]
' is already associated with an account') raise ValidationError(msg)
def valid_user_email(form, field): def valid_user_email(form, field):
form.user = _datastore.find_user(email=field.data) form.user = _datastore.find_user(email=field.data)
if form.user is None: if form.user is None:
raise ValidationError('Specified user does not exist') raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0])
class Form(BaseForm): class Form(BaseForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs.setdefault('csrf_enabled', not current_app.testing) if current_app.testing:
self.TIME_LIMIT = None
super(Form, self).__init__(*args, **kwargs) super(Form, self).__init__(*args, **kwargs)
class EmailFormMixin(): class EmailFormMixin():
email = TextField("Email Address", email = TextField(get_form_field_label('email'),
validators=[email_required, validators=[email_required,
email_validator]) email_validator])
class UserEmailFormMixin(): class UserEmailFormMixin():
user = None user = None
email = TextField("Email Address", email = TextField(get_form_field_label('email'),
validators=[email_required, validators=[email_required,
email_validator, email_validator,
valid_user_email]) valid_user_email])
class UniqueEmailFormMixin(): class UniqueEmailFormMixin():
email = TextField("Email Address", email = TextField(get_form_field_label('email'),
validators=[email_required, validators=[email_required,
email_validator, email_validator,
unique_user_email]) unique_user_email])
class PasswordFormMixin(): class PasswordFormMixin():
password = PasswordField("Password", password = PasswordField(get_form_field_label('password'),
validators=[password_required]) validators=[password_required])
class NewPasswordFormMixin(): class NewPasswordFormMixin():
password = PasswordField("Password", password = PasswordField(get_form_field_label('password'),
validators=[password_required, validators=[password_required,
Length(min=6, max=128)]) Length(min=6, max=128)])
class PasswordConfirmFormMixin(): class PasswordConfirmFormMixin():
password_confirm = PasswordField("Retype Password", password_confirm = PasswordField(
validators=[EqualTo('password', message="Passwords do not match")]) get_form_field_label('retype_password'),
validators=[EqualTo('password', message='RETYPE_PASSWORD_MISMATCH')])
class NextFormMixin(): class NextFormMixin():
next = HiddenField() 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])
class RegisterFormMixin(): class RegisterFormMixin():
submit = SubmitField("Register") submit = SubmitField(get_form_field_label('register'))
def to_dict(form):
def is_field_and_user_attr(member):
return isinstance(member, Field) and \
hasattr(_datastore.user_model, member.name)
fields = inspect.getmembers(form, is_field_and_user_attr)
return dict((key, value.data) for key, value in fields)
class SendConfirmationForm(Form, UserEmailFormMixin): class SendConfirmationForm(Form, UserEmailFormMixin):
"""The default forgot password form""" """The default forgot password form"""
submit = SubmitField("Resend Confirmation Instructions") submit = SubmitField(get_form_field_label('send_confirmation'))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SendConfirmationForm, self).__init__(*args, **kwargs) super(SendConfirmationForm, self).__init__(*args, **kwargs)
@@ -112,13 +177,13 @@ class SendConfirmationForm(Form, UserEmailFormMixin):
class ForgotPasswordForm(Form, UserEmailFormMixin): class ForgotPasswordForm(Form, UserEmailFormMixin):
"""The default forgot password form""" """The default forgot password form"""
submit = SubmitField("Recover Password") submit = SubmitField(get_form_field_label('recover_password'))
class PasswordlessLoginForm(Form, UserEmailFormMixin): class PasswordlessLoginForm(Form, UserEmailFormMixin):
"""The passwordless login form""" """The passwordless login form"""
submit = SubmitField("Send Login Link") submit = SubmitField(get_form_field_label('send_login_link'))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PasswordlessLoginForm, self).__init__(*args, **kwargs) super(PasswordlessLoginForm, self).__init__(*args, **kwargs)
@@ -134,32 +199,34 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
class LoginForm(Form, NextFormMixin): class LoginForm(Form, NextFormMixin):
"""The default login form""" """The default login form"""
email = TextField('Email Address')
password = PasswordField('Password') email = TextField(get_form_field_label('email'))
remember = BooleanField("Remember Me") password = PasswordField(get_form_field_label('password'))
submit = SubmitField("Login") remember = BooleanField(get_form_field_label('remember_me'))
submit = SubmitField(get_form_field_label('login'))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs) super(LoginForm, self).__init__(*args, **kwargs)
def validate(self): def validate(self):
super(LoginForm, self).validate() if not super(LoginForm, self).validate():
return False
if self.email.data.strip() == '': if self.email.data.strip() == '':
self.email.errors.append('Email not provided') self.email.errors.append(get_message('EMAIL_NOT_PROVIDED')[0])
return False return False
if self.password.data.strip() == '': if self.password.data.strip() == '':
self.password.errors.append('Password not provided') 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.find_user(email=self.email.data)
if self.user is None: if self.user is None:
self.email.errors.append('Specified user does not exist') self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
return False return False
if not verify_password(self.password.data, self.user.password): if not verify_and_update_password(self.password.data, self.user):
self.password.errors.append('Invalid password') self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False return False
if requires_confirmation(self.user): if requires_confirmation(self.user):
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
@@ -172,9 +239,7 @@ class LoginForm(Form, NextFormMixin):
class ConfirmRegisterForm(Form, RegisterFormMixin, class ConfirmRegisterForm(Form, RegisterFormMixin,
UniqueEmailFormMixin, NewPasswordFormMixin): UniqueEmailFormMixin, NewPasswordFormMixin):
def to_dict(self): pass
return dict(email=self.email.data,
password=self.password.data)
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin): class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
@@ -184,4 +249,29 @@ class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin): class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
"""The default reset password form""" """The default reset password form"""
submit = SubmitField("Reset Password") submit = SubmitField(get_form_field_label('reset_password'))
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_confirm = PasswordField(get_form_field_label('retype_password'),
validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
submit = SubmitField(get_form_field_label('change_password'))
def validate(self):
if not super(ChangePasswordForm, self).validate():
return False
if self.password.data.strip() == '':
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
return False
if not verify_and_update_password(self.password.data, current_user):
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False
return True
+5 -6
View File
@@ -14,7 +14,7 @@ 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):
@@ -45,7 +44,7 @@ def generate_login_token(user):
:param user: The user the token belongs to :param user: The user the token belongs to
""" """
return _security.login_serializer.dumps([user.id]) return _security.login_serializer.dumps([str(user.id)])
def login_token_status(token): def login_token_status(token):
+6 -7
View File
@@ -14,7 +14,7 @@ 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):
@@ -54,7 +53,7 @@ def generate_reset_password_token(user):
:param user: The user to work with :param user: The user to work with
""" """
data = [user.id, md5(user.password)] data = [str(user.id), md5(user.password)]
return _security.reset_serializer.dumps(data) return _security.reset_serializer.dumps(data)
@@ -77,4 +76,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)
+6 -5
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,10 +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)
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome', if config_value('SEND_REGISTER_EMAIL'):
user=user, confirmation_link=confirmation_link) send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
user=user, confirmation_link=confirmation_link)
return user return user
+2
View File
@@ -24,4 +24,6 @@ login_instructions_sent = signals.signal("login-instructions-sent")
password_reset = signals.signal("password-reset") password_reset = signals.signal("password-reset")
password_changed = signals.signal("password-changed")
reset_password_instructions_sent = signals.signal("password-reset-instructions-sent") reset_password_instructions_sent = signals.signal("password-reset-instructions-sent")
+1 -1
View File
@@ -12,4 +12,4 @@
<li><a href="{{ url_for_security('send_confirmation') }}">Confirm account</a></li> <li><a href="{{ url_for_security('send_confirmation') }}">Confirm account</a></li>
{% endif %} {% endif %}
</ul> </ul>
{% endif %} {% endif %}
@@ -0,0 +1,11 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% include "security/_messages.html" %}
<h1>Change password</h1>
<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
{{ change_password_form.hidden_tag() }}
{{ render_field_with_errors(change_password_form.password) }}
{{ render_field_with_errors(change_password_form.new_password) }}
{{ render_field_with_errors(change_password_form.new_password_confirm) }}
{{ render_field(change_password_form.submit) }}
</form>
@@ -0,0 +1,4 @@
<p>Your password has been changed.</p>
{% if security.recoverable %}
<p>If you did not change your password, <a href="{{ url_for_security('forgot_password', _external=True) }}">click here to reset it</a>.</p>
{% endif %}
@@ -0,0 +1,5 @@
Your password has been changed
{% if security.recoverable %}
If you did not change your password, click the link below to reset it.
{{ url_for_security('forgot_password', _external=True) }}
{% endif %}
+102 -23
View File
@@ -10,22 +10,25 @@
""" """
import base64 import base64
import blinker
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
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from .signals import user_registered, reset_password_instructions_sent, \ from .signals import user_registered, user_confirmed, \
login_instructions_sent confirm_instructions_sent, login_instructions_sent, \
password_reset, password_changed, reset_password_instructions_sent
# Convenient references # Convenient references
_security = LocalProxy(lambda: current_app.extensions['security']) _security = LocalProxy(lambda: current_app.extensions['security'])
@@ -35,26 +38,31 @@ _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."""
_login_user(user, remember) if remember is None:
remember = config_value('DEFAULT_REMEMBER_ME')
if not _login_user(user, remember):
return False
if _security.trackable: if _security.trackable:
old_current, new_current = user.current_login_at, datetime.utcnow() old_current_login, new_current_login = user.current_login_at, datetime.utcnow()
user.last_login_at = old_current or new_current
user.current_login_at = new_current
remote_addr = request.remote_addr or 'untrackable' remote_addr = request.remote_addr or 'untrackable'
old_current, new_current = user.current_login_ip, remote_addr old_current_ip, new_current_ip = user.current_login_ip, remote_addr
user.last_login_ip = old_current or new_current
user.current_login_ip = new_current
user.last_login_at = old_current_login or new_current_login
user.current_login_at = new_current_login
user.last_login_ip = old_current_ip or new_current_ip
user.current_login_ip = new_current_ip
user.login_count = user.login_count + 1 if user.login_count else 1 user.login_count = user.login_count + 1 if user.login_count else 1
_datastore.put(user) _datastore.put(user)
identity_changed.send(current_app._get_current_object(), identity_changed.send(current_app._get_current_object(),
identity=Identity(user.id)) identity=Identity(user.id))
return True
def logout_user(): def logout_user():
@@ -74,7 +82,7 @@ def get_hmac(password):
'must not be None when the value of `SECURITY_PASSWORD_HASH` is ' '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, hashlib.sha512) h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512)
return base64.b64encode(h.digest()) return base64.b64encode(h.digest())
@@ -82,6 +90,14 @@ def verify_password(password, password_hash):
return _pwd_context.verify(get_hmac(password), password_hash) return _pwd_context.verify(get_hmac(password), password_hash)
def verify_and_update_password(password, user):
verified, new_password = _pwd_context.verify_and_update(get_hmac(password), user.password)
if verified and new_password:
user.password = new_password
_datastore.put(user)
return verified
def encrypt_password(password): def encrypt_password(password):
return _pwd_context.encrypt(get_hmac(password)) return _pwd_context.encrypt(get_hmac(password))
@@ -132,11 +148,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):
@@ -184,7 +207,8 @@ def config_value(key, app=None, default=None):
def get_max_age(key, app=None): def get_max_age(key, app=None):
now = datetime.utcnow() now = datetime.utcnow()
expires = now + get_within_delta(key + '_WITHIN', app) expires = now + get_within_delta(key + '_WITHIN', app)
return int(expires.strftime('%s')) - int(now.strftime('%s')) td = (expires - now)
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6
def get_within_delta(key, app=None): def get_within_delta(key, app=None):
@@ -260,7 +284,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)
@@ -280,7 +304,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)
@@ -300,8 +324,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)
@@ -309,3 +333,58 @@ def capture_reset_password_requests(reset_password_sent_at=None):
yield reset_requests yield reset_requests
finally: finally:
reset_password_instructions_sent.disconnect(_on) reset_password_instructions_sent.disconnect(_on)
class CaptureSignals(object):
"""Testing utility for capturing blinker signals.
Context manager which mocks out selected signals and registers which are `sent` on and what
arguments were sent. Instantiate with a list of blinker `NamedSignals` to patch. Each signal
has it's `send` mocked out.
"""
def __init__(self, signals):
"""Patch all given signals and make them available as attributes.
:param signals: list of signals
"""
self._records = {}
self._receivers = {}
for signal in signals:
self._records[signal] = []
self._receivers[signal] = functools.partial(self._record, signal)
def __getitem__(self, signal):
"""All captured signals are available via `ctxt[signal]`.
"""
if isinstance(signal, blinker.base.NamedSignal):
return self._records[signal]
else:
super(CaptureSignals, self).__setitem__(signal)
def _record(self, signal, *args, **kwargs):
self._records[signal].append((args, kwargs))
def __enter__(self):
for signal, receiver in self._receivers.iteritems():
signal.connect(receiver)
return self
def __exit__(self, type, value, traceback):
for signal, receiver in self._receivers.iteritems():
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]])
def capture_signals():
"""Factory method that creates a `CaptureSignals` with all the flask_security signals."""
return CaptureSignals([user_registered, user_confirmed,
confirm_instructions_sent, login_instructions_sent,
password_reset, password_changed,
reset_password_instructions_sent])
+73 -34
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 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 .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
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 .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, \
url_for_security as url_for
# Convenient references # Convenient references
_security = LocalProxy(lambda: current_app.extensions['security']) _security = LocalProxy(lambda: current_app.extensions['security'])
@@ -64,10 +63,12 @@ def _ctx(endpoint):
def login(): def login():
"""View function for login view""" """View function for login view"""
form_class = _security.login_form
if request.json: if request.json:
form = LoginForm(MultiDict(request.json)) form = form_class(MultiDict(request.json))
else: else:
form = LoginForm() form = form_class()
if form.validate_on_submit(): if form.validate_on_submit():
login_user(form.user, remember=form.remember.data) login_user(form.user, remember=form.remember.data)
@@ -82,7 +83,7 @@ def login():
if request.json: if request.json:
return _render_json(form, True) return _render_json(form, True)
return render_template('security/login_user.html', return render_template(config_value('LOGIN_USER_TEMPLATE'),
login_user_form=form, login_user_form=form,
**_ctx('login')) **_ctx('login'))
@@ -101,9 +102,9 @@ def register():
"""View function which handles a registration request.""" """View function which handles a registration request."""
if _security.confirmable or request.json: if _security.confirmable or request.json:
form_class = ConfirmRegisterForm form_class = _security.confirm_register_form
else: else:
form_class = RegisterForm form_class = _security.register_form
if request.json: if request.json:
form_data = MultiDict(request.json) form_data = MultiDict(request.json)
@@ -121,14 +122,12 @@ 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)
return render_template('security/register_user.html', return render_template(config_value('REGISTER_USER_TEMPLATE'),
register_user_form=form, register_user_form=form,
**_ctx('register')) **_ctx('register'))
@@ -136,10 +135,12 @@ def register():
def send_login(): def send_login():
"""View function that sends login instructions for passwordless login""" """View function that sends login instructions for passwordless login"""
form_class = _security.passwordless_login_form
if request.json: if request.json:
form = PasswordlessLoginForm(MultiDict(request.json)) form = form_class(MultiDict(request.json))
else: else:
form = PasswordlessLoginForm() form = form_class()
if form.validate_on_submit(): if form.validate_on_submit():
send_login_instructions(form.user) send_login_instructions(form.user)
@@ -149,7 +150,7 @@ def send_login():
if request.json: if request.json:
return _render_json(form) return _render_json(form)
return render_template('security/send_login.html', return render_template(config_value('SEND_LOGIN_TEMPLATE'),
send_login_form=form, send_login_form=form,
**_ctx('send_login')) **_ctx('send_login'))
@@ -169,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'))
@@ -179,10 +180,12 @@ def token_login(token):
def send_confirmation(): def send_confirmation():
"""View function which sends confirmation instructions.""" """View function which sends confirmation instructions."""
form_class = _security.send_confirmation_form
if request.json: if request.json:
form = SendConfirmationForm(MultiDict(request.json)) form = form_class(MultiDict(request.json))
else: else:
form = SendConfirmationForm() form = form_class()
if form.validate_on_submit(): if form.validate_on_submit():
send_confirmation_instructions(form.user) send_confirmation_instructions(form.user)
@@ -192,7 +195,7 @@ def send_confirmation():
if request.json: if request.json:
return _render_json(form) return _render_json(form)
return render_template('security/send_confirmation.html', return render_template(config_value('SEND_CONFIRMATION_TEMPLATE'),
send_confirmation_form=form, send_confirmation_form=form,
**_ctx('send_confirmation')) **_ctx('send_confirmation'))
@@ -203,7 +206,8 @@ def confirm_email(token):
expired, invalid, user = confirm_email_token_status(token) expired, invalid, user = confirm_email_token_status(token)
if invalid: if not user or invalid:
invalid = True
do_flash(*get_message('INVALID_CONFIRMATION_TOKEN')) do_flash(*get_message('INVALID_CONFIRMATION_TOKEN'))
if expired: if expired:
send_confirmation_instructions(user) send_confirmation_instructions(user)
@@ -214,7 +218,7 @@ def confirm_email(token):
url_for('send_confirmation')) url_for('send_confirmation'))
confirm_user(user) confirm_user(user)
login_user(user, True) login_user(user)
after_this_request(_commit) after_this_request(_commit)
do_flash(*get_message('EMAIL_CONFIRMED')) do_flash(*get_message('EMAIL_CONFIRMED'))
@@ -225,10 +229,12 @@ def confirm_email(token):
def forgot_password(): def forgot_password():
"""View function that handles a forgotten password request.""" """View function that handles a forgotten password request."""
form_class = _security.forgot_password_form
if request.json: if request.json:
form = ForgotPasswordForm(MultiDict(request.json)) form = form_class(MultiDict(request.json))
else: else:
form = ForgotPasswordForm() form = form_class()
if form.validate_on_submit(): if form.validate_on_submit():
send_reset_password_instructions(form.user) send_reset_password_instructions(form.user)
@@ -238,7 +244,7 @@ def forgot_password():
if request.json: if request.json:
return _render_json(form) return _render_json(form)
return render_template('security/forgot_password.html', return render_template(config_value('FORGOT_PASSWORD_TEMPLATE'),
forgot_password_form=form, forgot_password_form=form,
**_ctx('forgot_password')) **_ctx('forgot_password'))
@@ -257,27 +263,55 @@ def reset_password(token):
if invalid or expired: if invalid or expired:
return redirect(url_for('forgot_password')) return redirect(url_for('forgot_password'))
form = ResetPasswordForm() form = _security.reset_password_form()
if form.validate_on_submit(): if form.validate_on_submit():
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))
return render_template('security/reset_password.html', return render_template(config_value('RESET_PASSWORD_TEMPLATE'),
reset_password_form=form, reset_password_form=form,
reset_password_token=token, reset_password_token=token,
**_ctx('reset_password')) **_ctx('reset_password'))
@login_required
def change_password():
"""View function which handles a change password request."""
form_class = _security.change_password_form
if request.json:
form = form_class(MultiDict(request.json))
else:
form = form_class()
if form.validate_on_submit():
after_this_request(_commit)
change_user_password(current_user, form.new_password.data)
if request.json is None:
do_flash(*get_message('PASSWORD_CHANGE'))
return redirect(get_url(_security.post_change_view) or
get_url(_security.post_login_view))
if request.json:
return _render_json(form)
return render_template('security/change_password.html',
change_password_form=form,
**_ctx('change_password'))
def create_blueprint(state, import_name): def create_blueprint(state, import_name):
"""Creates the security extension blueprint""" """Creates the security extension blueprint"""
bp = Blueprint(state.blueprint_name, import_name, bp = Blueprint(state.blueprint_name, import_name,
url_prefix=state.url_prefix, url_prefix=state.url_prefix,
subdomain=state.subdomain,
template_folder='templates') template_folder='templates')
bp.route(state.logout_url, endpoint='logout')(logout) bp.route(state.logout_url, endpoint='logout')(logout)
@@ -306,6 +340,11 @@ def create_blueprint(state, import_name):
methods=['GET', 'POST'], methods=['GET', 'POST'],
endpoint='reset_password')(reset_password) endpoint='reset_password')(reset_password)
if state.changeable:
bp.route(state.change_url,
methods=['GET', 'POST'],
endpoint='change_password')(change_password)
if state.confirmable: if state.confirmable:
bp.route(state.confirm_url, bp.route(state.confirm_url,
methods=['GET', 'POST'], methods=['GET', 'POST'],
+7 -6
View File
@@ -1,6 +1,7 @@
Flask==0.8 Flask>=0.9
Flask-Login==0.1 Flask-Login>=0.1.3
Flask-Principal==0.2 Flask-Mail>=0.7.3
Flask-Script==0.3.2 Flask-Principal>=0.3.3
Flask-WTF==0.5.4 Flask-WTF>=0.8
passlib=1.5.3 itsdangerous>=0.17
passlib>=1.6.1
+6 -5
View File
@@ -20,7 +20,7 @@ from setuptools import setup
setup( setup(
name='Flask-Security', name='Flask-Security',
version='1.5.1', version='1.6.5',
url='https://github.com/mattupstate/flask-security', url='https://github.com/mattupstate/flask-security',
license='MIT', license='MIT',
author='Matt Wright', author='Matt Wright',
@@ -34,19 +34,20 @@ setup(
include_package_data=True, include_package_data=True,
platforms='any', platforms='any',
install_requires=[ install_requires=[
'Flask>=0.8', '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',
'itsdangerous==0.17', 'itsdangerous>=0.17',
'passlib==1.6.1', 'passlib>=1.6.1',
], ],
test_suite='nose.collector', test_suite='nose.collector',
tests_require=[ tests_require=[
'nose', 'nose',
'Flask-SQLAlchemy', 'Flask-SQLAlchemy',
'Flask-MongoEngine', 'Flask-MongoEngine',
'Flask-Peewee',
'py-bcrypt', 'py-bcrypt',
'simplejson' 'simplejson'
], ],
+31 -15
View File
@@ -1,24 +1,40 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import hmac
from hashlib import sha1
from unittest import TestCase from unittest import TestCase
from tests.test_app.sqlalchemy import create_app from tests.test_app.sqlalchemy import create_app
class SecurityTest(TestCase): class SecurityTest(TestCase):
APP_KWARGS = {
'register_blueprint': True,
}
AUTH_CONFIG = None AUTH_CONFIG = None
def setUp(self): def setUp(self):
super(SecurityTest, self).setUp() super(SecurityTest, self).setUp()
app = self._create_app(self.AUTH_CONFIG or {}) app_kwargs = self.APP_KWARGS
app = self._create_app(self.AUTH_CONFIG or {}, **app_kwargs)
app.debug = False app.debug = False
app.config['TESTING'] = True app.config['TESTING'] = True
self.app = app self.app = app
self.client = app.test_client() self.client = app.test_client()
def _create_app(self, auth_config, register_blueprint=True): with self.client.session_transaction() as session:
return create_app(auth_config, register_blueprint) 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): 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,
@@ -26,30 +42,30 @@ class SecurityTest(TestCase):
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):
data['csrf_token'] = self.csrf_token
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 or 'application/x-www-form-urlencoded',
headers=headers) headers=headers)
def register(self, email, password='password'): def register(self, email, password='password'):
data = dict(email=email, password=password) data = dict(email=email, password=password, csrf_token=self.csrf_token)
return self.client.post('/register', data=data, follow_redirects=True) return self.client.post('/register', data=data, follow_redirects=True)
def authenticate(self, email="matt@lp.com", password="password", endpoint=None, **kwargs): def authenticate(self, email="matt@lp.com", password="password", endpoint=None, **kwargs):
data = dict(email=email, password=password, remember='y') data = dict(email=email, password=password, remember='y')
r = self._post(endpoint or '/login', data=data, **kwargs) return self._post(endpoint or '/login', data=data, **kwargs)
return r
def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None): def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None):
data = """ data = """{
{ "email": "%s",
"email": "%s", "password": "%s",
"password": "%s" "csrf_token": "%s"
} }"""
""" return self._post(endpoint or '/login', content_type="application/json",
return self._post(endpoint or '/login', data=data % (email, password, self.csrf_token))
content_type="application/json",
data=data % (email, password))
def logout(self, endpoint=None): def logout(self, endpoint=None):
return self._get(endpoint or '/logout', follow_redirects=True) return self._get(endpoint or '/logout', follow_redirects=True)
+331 -35
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement from __future__ import with_statement
import base64 import base64
@@ -5,7 +7,12 @@ import time
import simplejson as json import simplejson as json
from flask.ext.security.utils import capture_registrations, \ from flask.ext.security.utils import capture_registrations, \
capture_reset_password_requests, capture_passwordless_login_requests capture_reset_password_requests, capture_passwordless_login_requests
from flask.ext.security.forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
from flask.ext.security.forms import TextField, SubmitField, valid_user_email
from tests import SecurityTest from tests import SecurityTest
@@ -61,8 +68,16 @@ 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" }' 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')
data = json.loads(r.data) data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200) self.assertEquals(data['meta']['code'], 200)
@@ -102,6 +117,79 @@ class BadConfiguredSecurityTests(SecurityTest):
self.assertRaises(RuntimeError, self.authenticate) self.assertRaises(RuntimeError, self.authenticate)
class DefaultTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'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)
class RegisterableTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'SECURITY_REGISTER_USER_TEMPLATE': 'custom_security/register_user.html'
}
def test_register_user_template(self):
r = self._get('/register')
self.assertIn('CUSTOM REGISTER USER', r.data)
class RecoverableTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_FORGOT_PASSWORD_TEMPLATE': 'custom_security/forgot_password.html',
'SECURITY_RESET_PASSWORD_TEMPLATE': 'custom_security/reset_password.html',
}
def test_forgot_password_template(self):
r = self._get('/reset')
self.assertIn('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)
t = requests[0]['token']
r = self._get('/reset/' + t)
self.assertIn('CUSTOM RESET PASSWORD', r.data)
class ConfirmableTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_SEND_CONFIRMATION_TEMPLATE': 'custom_security/send_confirmation.html'
}
def test_send_confirmation_template(self):
r = self._get('/confirm')
self.assertIn('CUSTOM SEND CONFIRMATION', r.data)
class PasswordlessTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
'SECURITY_SEND_LOGIN_TEMPLATE': 'custom_security/send_login.html'
}
def test_send_login_template(self):
r = self._get('/login')
self.assertIn('CUSTOM SEND LOGIN', r.data)
class RegisterableTests(SecurityTest): class RegisterableTests(SecurityTest):
AUTH_CONFIG = { AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True, 'SECURITY_REGISTERABLE': True,
@@ -112,7 +200,7 @@ class RegisterableTests(SecurityTest):
data = dict(email='dude@lp.com', data = dict(email='dude@lp.com',
password='password', password='password',
password_confirm='password') password_confirm='password')
self.client.post('/register', data=data, follow_redirects=True) self._post('/register', data=data, follow_redirects=True)
r = self.authenticate('dude@lp.com') r = self.authenticate('dude@lp.com')
self.assertIn('Hello dude@lp.com', r.data) self.assertIn('Hello dude@lp.com', r.data)
@@ -140,7 +228,7 @@ class ConfirmableTests(SecurityTest):
self.client.get('/confirm/' + token, follow_redirects=True) self.client.get('/confirm/' + token, follow_redirects=True)
self.logout() self.logout()
r = self.client.post('/confirm', data=dict(email=e)) r = self._post('/confirm', data=dict(email=e))
self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data) self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data)
def test_register_sends_confirmation_email(self): def test_register_sends_confirmation_email(self):
@@ -165,7 +253,8 @@ class ConfirmableTests(SecurityTest):
def test_invalid_token_when_confirming_email(self): def test_invalid_token_when_confirming_email(self):
r = self.client.get('/confirm/bogus', follow_redirects=True) r = self.client.get('/confirm/bogus', follow_redirects=True)
self.assertIn('Invalid confirmation token', r.data) msg = self.app.config['SECURITY_MSG_INVALID_CONFIRMATION_TOKEN'][0]
self.assertIn(msg, r.data)
def test_send_confirmation_json(self): def test_send_confirmation_json(self):
r = self._post('/confirm', data='{"email": "matt@lp.com"}', r = self._post('/confirm', data='{"email": "matt@lp.com"}',
@@ -174,7 +263,8 @@ class ConfirmableTests(SecurityTest):
def test_send_confirmation_with_invalid_email(self): def test_send_confirmation_with_invalid_email(self):
r = self._post('/confirm', data=dict(email='bogus@bogus.com')) r = self._post('/confirm', data=dict(email='bogus@bogus.com'))
self.assertIn('Specified user does not exist', r.data) msg = self.app.config['SECURITY_MSG_USER_DOES_NOT_EXIST'][0]
self.assertIn(msg, r.data)
def test_resend_confirmation(self): def test_resend_confirmation(self):
e = 'dude@lp.com' e = 'dude@lp.com'
@@ -184,6 +274,23 @@ class ConfirmableTests(SecurityTest):
msg = self.get_message('CONFIRMATION_REQUEST', email=e) msg = self.get_message('CONFIRMATION_REQUEST', email=e)
self.assertIn(msg, r.data) self.assertIn(msg, r.data)
def test_user_deleted_before_confirmation(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
user = registrations[0]['user']
token = registrations[0]['confirm_token']
with self.app.app_context():
from flask_security.core import _security
_security.datastore.delete(user)
_security.datastore.commit()
r = self.client.get('/confirm/' + token, follow_redirects=True)
msg = self.app.config['SECURITY_MSG_INVALID_CONFIRMATION_TOKEN'][0]
self.assertIn(msg, r.data)
class ExpiredConfirmationTest(SecurityTest): class ExpiredConfirmationTest(SecurityTest):
AUTH_CONFIG = { AUTH_CONFIG = {
@@ -226,7 +333,7 @@ class LoginWithoutImmediateConfirmTests(SecurityTest):
e = 'dude@lp.com' e = 'dude@lp.com'
p = 'password' p = 'password'
data = dict(email=e, password=p, password_confirm=p) data = dict(email=e, password=p, password_confirm=p)
r = self.client.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)
@@ -240,7 +347,7 @@ 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.client.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']
@@ -250,23 +357,23 @@ class RecoverableTests(SecurityTest):
def test_forgot_post_sends_email(self): def test_forgot_post_sends_email(self):
with capture_reset_password_requests(): with capture_reset_password_requests():
with self.app.extensions['mail'].record_messages() as outbox: with self.app.extensions['mail'].record_messages() as outbox:
self.client.post('/reset', data=dict(email='joe@lp.com')) self._post('/reset', data=dict(email='joe@lp.com'))
self.assertEqual(len(outbox), 1) self.assertEqual(len(outbox), 1)
def test_forgot_password_json(self): def test_forgot_password_json(self):
r = self.client.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.client.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.client.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']
@@ -298,14 +405,13 @@ class ExpiredResetPasswordTest(SecurityTest):
def test_reset_password_with_expired_token(self): def test_reset_password_with_expired_token(self):
with capture_reset_password_requests() as requests: with capture_reset_password_requests() as requests:
r = self.client.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']
time.sleep(1) time.sleep(1)
r = self.client.post('/reset/' + t, data={ r = self._post('/reset/' + t, data={
'password': 'newpassword', 'password': 'newpassword',
'password_confirm': 'newpassword' 'password_confirm': 'newpassword'
}, follow_redirects=True) }, follow_redirects=True)
@@ -313,6 +419,95 @@ class ExpiredResetPasswordTest(SecurityTest):
self.assertIn('You did not reset your password within', r.data) self.assertIn('You did not reset your password within', r.data)
class ChangePasswordTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_CHANGEABLE': True,
}
def test_change_password(self):
self.authenticate()
r = self.client.get('/change', follow_redirects=True)
self.assertIn('Change password', r.data)
def test_change_password_invalid(self):
self.authenticate()
r = self._post('/change', data={
'password': 'notpassword',
'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)
def test_change_password_mismatch(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'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)
def test_change_password_bad_password(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'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)
def test_change_password_success(self):
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)
self.assertIn('You successfully changed your password', r.data)
self.assertIn('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 ChangePasswordPostViewTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': True,
'SECURITY_POST_CHANGE_VIEW': '/profile',
}
def test_change_password_success(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('Profile Page', r.data)
class ChangePasswordDisabledTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': False,
}
def test_change_password_endpoint_is_404(self):
self.authenticate()
r = self.client.get('/change', follow_redirects=True)
self.assertEqual(404, r.status_code)
class TrackableTests(SecurityTest): class TrackableTests(SecurityTest):
AUTH_CONFIG = { AUTH_CONFIG = {
@@ -343,20 +538,19 @@ class PasswordlessTests(SecurityTest):
def test_login_request_for_inactive_user(self): def test_login_request_for_inactive_user(self):
msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0] msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0]
r = self.client.post('/login', r = self._post('/login', data=dict(email='tiya@lp.com'),
data=dict(email='tiya@lp.com'), follow_redirects=True)
follow_redirects=True)
self.assertIn(msg, r.data) self.assertIn(msg, r.data)
def test_request_login_token_with_json_and_valid_email(self): def test_request_login_token_with_json_and_valid_email(self):
data = '{"email": "matt@lp.com", "password": "password"}' data = '{"email": "matt@lp.com", "password": "password", "csrf_token":"%s"}' % self.csrf_token
r = self.client.post('/login', data=data, content_type='application/json') r = self._post('/login', data=data, content_type='application/json')
self.assertEquals(r.status_code, 200) self.assertEquals(r.status_code, 200)
self.assertNotIn('error', r.data) self.assertNotIn('error', r.data)
def test_request_login_token_with_json_and_invalid_email(self): def test_request_login_token_with_json_and_invalid_email(self):
data = '{"email": "nobody@lp.com", "password": "password"}' data = '{"email": "nobody@lp.com", "password": "password"}'
r = self.client.post('/login', data=data, content_type='application/json') r = self._post('/login', data=data, content_type='application/json')
self.assertIn('errors', r.data) self.assertIn('errors', r.data)
def test_request_login_token_sends_email_and_can_login(self): def test_request_login_token_sends_email_and_can_login(self):
@@ -365,9 +559,8 @@ class PasswordlessTests(SecurityTest):
with capture_passwordless_login_requests() as requests: with capture_passwordless_login_requests() as requests:
with self.app.extensions['mail'].record_messages() as outbox: with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.post('/login', r = self._post('/login', data=dict(email=e),
data=dict(email=e), follow_redirects=True)
follow_redirects=True)
self.assertEqual(len(outbox), 1) self.assertEqual(len(outbox), 1)
@@ -396,9 +589,8 @@ class PasswordlessTests(SecurityTest):
def test_token_login_when_already_authenticated(self): def test_token_login_when_already_authenticated(self):
with capture_passwordless_login_requests() as requests: with capture_passwordless_login_requests() as requests:
self.client.post('/login', self._post('/login', data=dict(email='matt@lp.com'),
data=dict(email='matt@lp.com'), follow_redirects=True)
follow_redirects=True)
token = requests[0]['login_token'] token = requests[0]['login_token']
r = self.client.get('/login/' + token, follow_redirects=True) r = self.client.get('/login/' + token, follow_redirects=True)
@@ -426,9 +618,7 @@ class ExpiredLoginTokenTests(SecurityTest):
e = 'matt@lp.com' e = 'matt@lp.com'
with capture_passwordless_login_requests() as requests: with capture_passwordless_login_requests() as requests:
self.client.post('/login', self._post('/login', data=dict(email=e), follow_redirects=True)
data=dict(email=e),
follow_redirects=True)
token = requests[0]['login_token'] token = requests[0]['login_token']
time.sleep(1.25) time.sleep(1.25)
@@ -462,19 +652,20 @@ class AsyncMailTaskTests(SecurityTest):
def send_email(msg): def send_email(msg):
self.mail_sent = True self.mail_sent = True
self.client.post('/reset', data=dict(email='matt@lp.com')) self._post('/reset', data=dict(email='matt@lp.com'))
self.assertTrue(self.mail_sent) self.assertTrue(self.mail_sent)
class NoBlueprintTests(SecurityTest): class NoBlueprintTests(SecurityTest):
APP_KWARGS = {
'register_blueprint': False,
}
AUTH_CONFIG = { AUTH_CONFIG = {
'USER_COUNT': 1 'USER_COUNT': 1
} }
def _create_app(self, auth_config):
return super(NoBlueprintTests, self)._create_app(auth_config, False)
def test_login_endpoint_is_404(self): def test_login_endpoint_is_404(self):
r = self._get('/login') r = self._get('/login')
self.assertEqual(404, r.status_code) self.assertEqual(404, r.status_code)
@@ -483,3 +674,108 @@ class NoBlueprintTests(SecurityTest):
auth = 'Basic ' + base64.b64encode("matt@lp.com:password") auth = 'Basic ' + base64.b64encode("matt@lp.com:password")
r = self._get('/http', headers={'Authorization': auth}) r = self._get('/http', headers={'Authorization': auth})
self.assertIn('HTTP Authentication', r.data) self.assertIn('HTTP Authentication', r.data)
class ExtendFormsTest(SecurityTest):
class MyLoginForm(LoginForm):
email = TextField('My Login Email Address Field')
class MyRegisterForm(RegisterForm):
email = TextField('My Register Email Address Field')
APP_KWARGS = {
'login_form': MyLoginForm,
'register_form': MyRegisterForm,
}
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': False,
'SECURITY_REGISTERABLE': True,
}
def test_login_view(self):
r = self._get('/login', follow_redirects=True)
self.assertIn("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)
class RecoverableExtendFormsTest(SecurityTest):
class MyForgotPasswordForm(ForgotPasswordForm):
email = TextField('My Forgot Password Email Address Field',
validators=[valid_user_email])
class MyResetPasswordForm(ResetPasswordForm):
submit = SubmitField("My Reset Password Submit Field")
APP_KWARGS = {
'forgot_password_form': MyForgotPasswordForm,
'reset_password_form': MyResetPasswordForm,
}
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
}
def test_forgot_password(self):
r = self._get('/reset', follow_redirects=True)
self.assertIn("My Forgot Password Email Address Field", r.data)
def test_reset_password(self):
with capture_reset_password_requests() as requests:
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
token = requests[0]['token']
r = self._get('/reset/' + token)
self.assertIn("My Reset Password Submit Field", r.data)
class PasswordlessExtendFormsTest(SecurityTest):
class MyPasswordlessLoginForm(PasswordlessLoginForm):
email = TextField('My Passwordless Login Email Address Field')
APP_KWARGS = {
'passwordless_login_form': MyPasswordlessLoginForm,
}
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
}
def test_passwordless_login(self):
r = self._get('/login', follow_redirects=True)
self.assertIn("My Passwordless Login Email Address Field", r.data)
class ConfirmableExtendFormsTest(SecurityTest):
class MyConfirmRegisterForm(ConfirmRegisterForm):
email = TextField('My Confirm Register Email Address Field')
class MySendConfirmationForm(SendConfirmationForm):
email = TextField('My Send Confirmation Email Address Field')
APP_KWARGS = {
'confirm_register_form': MyConfirmRegisterForm,
'send_confirmation_form': MySendConfirmationForm,
}
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
}
def test_register(self):
r = self._get('/register', follow_redirects=True)
self.assertIn("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)
+42 -10
View File
@@ -35,23 +35,23 @@ class DefaultSecurityTests(SecurityTest):
def test_unprovided_username(self): def test_unprovided_username(self):
r = self.authenticate("") r = self.authenticate("")
self.assertIn("Email not provided", r.data) self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data)
def test_unprovided_password(self): def test_unprovided_password(self):
r = self.authenticate(password="") r = self.authenticate(password="")
self.assertIn("Password not provided", r.data) self.assertIn(self.get_message('PASSWORD_NOT_PROVIDED'), r.data)
def test_invalid_user(self): def test_invalid_user(self):
r = self.authenticate(email="bogus@bogus.com") r = self.authenticate(email="bogus@bogus.com")
self.assertIn("Specified user does not exist", r.data) self.assertIn(self.get_message('USER_DOES_NOT_EXIST'), r.data)
def test_bad_password(self): def test_bad_password(self):
r = self.authenticate(password="bogus") r = self.authenticate(password="bogus")
self.assertIn("Invalid password", r.data) self.assertIn(self.get_message('INVALID_PASSWORD'), r.data)
def test_inactive_user(self): def test_inactive_user(self):
r = self.authenticate("tiya@lp.com", "password") r = self.authenticate("tiya@lp.com", "password")
self.assertIn("Account is disabled", r.data) self.assertIn(self.get_message('DISABLED_ACCOUNT'), r.data)
def test_logout(self): def test_logout(self):
self.authenticate() self.authenticate()
@@ -60,7 +60,7 @@ class DefaultSecurityTests(SecurityTest):
def test_unauthorized_access(self): def test_unauthorized_access(self):
r = self._get('/profile', follow_redirects=True) r = self._get('/profile', follow_redirects=True)
self.assertIn('Please log in to access this page', 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 +142,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")
@@ -169,6 +176,24 @@ class DefaultSecurityTests(SecurityTest):
self.assertEquals('Basic realm="My Realm"', self.assertEquals('Basic realm="My Realm"',
r.headers['WWW-Authenticate']) r.headers['WWW-Authenticate'])
def test_multi_auth_basic(self):
r = self._get('/multi_auth', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password")
})
self.assertIn('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)
def test_multi_auth_session(self):
self.authenticate()
r = self._get('/multi_auth')
self.assertIn('Session', r.data)
def test_user_deleted_during_session_reverts_to_anonymous_user(self): def test_user_deleted_during_session_reverts_to_anonymous_user(self):
self.authenticate() self.authenticate()
@@ -201,9 +226,16 @@ class DefaultSecurityTests(SecurityTest):
class MongoEngineSecurityTests(DefaultSecurityTests): class MongoEngineSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config): def _create_app(self, auth_config, **kwargs):
from tests.test_app.mongoengine import create_app from tests.test_app.mongoengine import create_app
return create_app(auth_config) return create_app(auth_config, **kwargs)
class PeeweeSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config, **kwargs):
from tests.test_app.peewee_app import create_app
return create_app(auth_config, **kwargs)
class DefaultDatastoreTests(SecurityTest): class DefaultDatastoreTests(SecurityTest):
@@ -231,6 +263,6 @@ class DefaultDatastoreTests(SecurityTest):
class MongoEngineDatastoreTests(DefaultDatastoreTests): class MongoEngineDatastoreTests(DefaultDatastoreTests):
def _create_app(self, auth_config): def _create_app(self, auth_config, **kwargs):
from tests.test_app.mongoengine import create_app from tests.test_app.mongoengine import create_app
return create_app(auth_config) return create_app(auth_config, **kwargs)
+245
View File
@@ -0,0 +1,245 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
from flask_security.utils import capture_registrations, \
capture_reset_password_requests, capture_signals
from flask_security.signals import user_registered, user_confirmed, \
confirm_instructions_sent, login_instructions_sent, \
password_reset, password_changed, reset_password_instructions_sent
from tests import SecurityTest
def compare_user(a, b):
"""Helper to compare two users."""
return a.id == b.id and a.email == b.email and a.password == b.password
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 = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
}
def test_register(self):
e = 'dude@lp.com'
with capture_signals() as mocks:
self.register(e)
user = self.app.security.datastore.find_user(email='dude@lp.com')
self.assertEqual(mocks.signals_sent(), set([user_registered]))
calls = mocks[user_registered]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('confirm_token', kwargs)
self.assertEqual(args[0], self.app)
def test_register_without_password(self):
e = 'dude@lp.com'
with capture_signals() as mocks:
self.register(e, password='')
self.assertEqual(mocks.signals_sent(), set())
class ConfirmableSignalsTests(SignalTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
}
def test_confirm(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
with capture_signals() as mocks:
self.client.get('/confirm/' + token, follow_redirects=True)
user = self.app.security.datastore.find_user(email='dude@lp.com')
self.assertTrue(mocks.signals_sent(), set([user_confirmed]))
calls = mocks[user_confirmed]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertEqual(args[0], self.app)
self.assertTrue(compare_user(kwargs['user'], user))
def test_confirm_bad_token(self):
e = 'dude@lp.com'
with capture_registrations():
self.register(e)
with capture_signals() as mocks:
self.client.get('/confirm/bogus', follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_confirm_twice(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
self.client.get('/confirm/' + token, follow_redirects=True)
self.logout()
with capture_signals() as mocks:
self.client.get('/confirm/' + token, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([user_confirmed]))
# TODO: is that the desired behaviour?
def test_resend_confirmation(self):
e = 'dude@lp.com'
self.register(e)
with capture_signals() as mocks:
self._post('/confirm', data={'email': e})
user = self.app.security.datastore.find_user(email='dude@lp.com')
self.assertEqual(mocks.signals_sent(), set([confirm_instructions_sent]))
calls = mocks[confirm_instructions_sent]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertEqual(args[0], self.app)
def test_send_confirmation_bad_email(self):
with capture_signals() as mocks:
self._post('/confirm', data=dict(email='bogus@bogus.com'))
self.assertEqual(mocks.signals_sent(), set())
class RecoverableSignalsTests(SignalTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
'SECURITY_POST_FORGOT_VIEW': '/'
}
def test_reset_password_request(self):
with capture_signals() as mocks:
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([reset_password_instructions_sent]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[reset_password_instructions_sent]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('token', kwargs)
self.assertEqual(args[0], self.app)
def test_reset_password(self):
with capture_reset_password_requests() as requests:
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
token = requests[0]['token']
with capture_signals() as mocks:
data = dict(password='newpassword', password_confirm='newpassword')
self._post('/reset/' + token, data, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([password_reset]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[password_reset]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertEqual(args[0], self.app)
def test_reset_password_invalid_emails(self):
with capture_signals() as mocks:
self._post('/reset', data=dict(email='nobody@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_reset_password_invalid_token(self):
with capture_signals() as mocks:
data = dict(password='newpassword', password_confirm='newpassword')
self._post('/reset/bogus', data, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
class ChangeableSignalsTests(SignalTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': True,
}
def test_change_password(self):
self.authenticate('joe@lp.com')
with capture_signals() as mocks:
with self.client as client:
client.post('/change',
data=dict(password='password',
new_password='newpassword',
new_password_confirm='newpassword',
csrf_token=self.csrf_token))
self.assertEqual(mocks.signals_sent(), set([password_changed]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[password_changed]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(args[0], user))
self.assertEqual(kwargs['app'], self.app)
def test_change_password_invalid_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='notpassword',
new_password='newpassword',
new_password_confirm='newpassword'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_change_password_bad_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='notpassword',
new_password='a',
new_password_confirm='a'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_change_password_mismatch_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='password',
new_password='newpassword',
new_password_confirm='notnewpassword'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
class PasswordlessTests(SignalTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True
}
def test_login_request_for_inactive_user(self):
with capture_signals() as mocks:
self._post('/login', data=dict(email='tiya@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_login_request_for_invalid_email(self):
with capture_signals() as mocks:
self._post('/login', data=dict(email='nobody@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_request_login_token_sends_email_and_can_login(self):
e = 'matt@lp.com'
with capture_signals() as mocks:
self._post('/login', data=dict(email=e), follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([login_instructions_sent]))
user = self.app.security.datastore.find_user(email='matt@lp.com')
calls = mocks[login_instructions_sent]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('login_token', kwargs)
self.assertEqual(args[0], self.app)
+16 -1
View File
@@ -4,16 +4,18 @@ from flask import Flask, render_template, current_app
from flask.ext.mail import Mail from flask.ext.mail import Mail
from flask.ext.security import login_required, roles_required, roles_accepted from flask.ext.security import login_required, roles_required, roles_accepted
from flask.ext.security.decorators import http_auth_required, \ from flask.ext.security.decorators import http_auth_required, \
auth_token_required auth_token_required, auth_required
from flask.ext.security.utils import encrypt_password from flask.ext.security.utils import encrypt_password
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
ds = LocalProxy(lambda: current_app.extensions['security'].datastore) ds = LocalProxy(lambda: current_app.extensions['security'].datastore)
def create_app(config): def create_app(config):
app = Flask(__name__) app = Flask(__name__)
app.debug = True app.debug = True
app.config['SECRET_KEY'] = 'secret' app.config['SECRET_KEY'] = 'secret'
app.config['TESTING'] = True
for key, value in config.items(): for key, value in config.items():
app.config[key] = value app.config[key] = value
@@ -50,6 +52,11 @@ def create_app(config):
def token(): def token():
return render_template('index.html', content='Token Authentication') return render_template('index.html', content='Token Authentication')
@app.route('/multi_auth')
@auth_required('session', 'token', 'basic')
def multi_auth():
return render_template('index.html', content='Session, Token, Basic auth')
@app.route('/post_logout') @app.route('/post_logout')
def post_logout(): def post_logout():
return render_template('index.html', content='Post Logout') return render_template('index.html', content='Post Logout')
@@ -106,13 +113,19 @@ 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
def create_roles(): def create_roles():
for role in ('admin', 'editor', 'author'): for role in ('admin', 'editor', 'author'):
ds.create_role(name=role) ds.create_role(name=role)
ds.commit() ds.commit()
def create_users(count=None): def create_users(count=None):
users = [('matt@lp.com', 'password', ['admin'], True), users = [('matt@lp.com', 'password', ['admin'], True),
('joe@lp.com', 'password', ['editor'], True), ('joe@lp.com', 'password', ['editor'], True),
@@ -127,10 +140,12 @@ def create_users(count=None):
roles=u[2], active=u[3]) roles=u[2], active=u[3])
ds.commit() ds.commit()
def populate_data(user_count=None): def populate_data(user_count=None):
create_roles() create_roles()
create_users(user_count) create_users(user_count)
def add_context_processors(s): def add_context_processors(s):
@s.context_processor @s.context_processor
def for_all(): def for_all():
+2 -2
View File
@@ -13,7 +13,7 @@ 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): def create_app(config, **kwargs):
app = create_base_app(config) app = create_base_app(config)
app.config['MONGODB_SETTINGS'] = dict( app.config['MONGODB_SETTINGS'] = dict(
@@ -46,7 +46,7 @@ def create_app(config):
Role.drop_collection() Role.drop_collection()
populate_data(app.config.get('USER_COUNT', None)) populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, MongoEngineUserDatastore(db, User, Role)) app.security = Security(app, datastore=MongoEngineUserDatastore(db, User, Role), **kwargs)
add_context_processors(app.security) add_context_processors(app.security)
+63
View File
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask_peewee.db import Database
from peewee import *
from flask.ext.security import Security, UserMixin, RoleMixin, \
PeeweeUserDatastore
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config, **kwargs):
app = create_base_app(config)
app.config['DATABASE'] = {
'name': 'example2.db',
'engine': 'peewee.SqliteDatabase'
}
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
description = TextField(null=True)
class User(db.Model, UserMixin):
email = TextField()
password = TextField()
last_login_at = DateTimeField(null=True)
current_login_at = DateTimeField(null=True)
last_login_ip = TextField(null=True)
current_login_ip = TextField(null=True)
login_count = IntegerField(null=True)
active = BooleanField(default=True)
confirmed_at = DateTimeField(null=True)
class UserRoles(db.Model):
""" Peewee does not have built-in many-to-many support, so we have to
create this mapping class to link users to roles."""
user = ForeignKeyField(User, related_name='roles')
role = ForeignKeyField(Role, related_name='users')
name = property(lambda self: self.role.name)
description = property(lambda self: self.role.description)
@app.before_first_request
def before_first_request():
for Model in (Role, User, UserRoles):
Model.drop_table(fail_silently=True)
Model.create_table(fail_silently=True)
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, datastore=PeeweeUserDatastore(db, User, Role, UserRoles), **kwargs)
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
+4 -4
View File
@@ -14,10 +14,11 @@ 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, register_blueprint=True): def create_app(config, **kwargs):
app = create_base_app(config) app = create_base_app(config)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test' #app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
db = SQLAlchemy(app) db = SQLAlchemy(app)
@@ -50,8 +51,7 @@ def create_app(config, register_blueprint=True):
db.create_all() db.create_all()
populate_data(app.config.get('USER_COUNT', None)) populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, SQLAlchemyUserDatastore(db, User, Role), app.security = Security(app, datastore=SQLAlchemyUserDatastore(db, User, Role), **kwargs)
register_blueprint=register_blueprint)
add_context_processors(app.security) add_context_processors(app.security)
@@ -0,0 +1 @@
CUSTOM FORGOT PASSWORD
@@ -0,0 +1 @@
CUSTOM LOGIN USER
@@ -0,0 +1 @@
CUSTOM REGISTER USER
@@ -0,0 +1 @@
CUSTOM RESET PASSWORD
@@ -0,0 +1 @@
CUSTOM SEND CONFIRMATION
@@ -0,0 +1 @@
CUSTOM SEND LOGIN
+2 -2
View File
@@ -55,8 +55,8 @@ class DatastoreTests(unittest.TestCase):
self.assertRaises(NotImplementedError, ds.delete, None) self.assertRaises(NotImplementedError, ds.delete, None)
def test_unimplemented_user_datastore_methods(self): def test_unimplemented_user_datastore_methods(self):
self.assertRaises(NotImplementedError, self.ds.find_user) self.assertRaises(NotImplementedError, self.ds.find_user, None)
self.assertRaises(NotImplementedError, self.ds.find_role) self.assertRaises(NotImplementedError, self.ds.find_role, None)
def test_toggle_active(self): def test_toggle_active(self):
user.active = True user.active = True