mirror of
https://github.com/wassname/flask-security.git
synced 2026-08-07 11:22:21 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f46f35981 | ||
|
|
c0d9eecf10 | ||
|
|
04bb2c4041 | ||
|
|
8eeb832d2e | ||
|
|
8f760aadbd | ||
|
|
03d27cd600 | ||
|
|
d30a27b3bb | ||
|
|
78903fa2e5 | ||
|
|
514de64303 | ||
|
|
bad63265f8 | ||
|
|
66a9dcd2e6 | ||
|
|
df1647f1f9 | ||
|
|
89ecded480 | ||
|
|
2b35b37a66 | ||
|
|
4d8a813004 | ||
|
|
06312ef50f | ||
|
|
c3ad5b2fa6 | ||
|
|
637fc913cd | ||
|
|
e00522f331 | ||
|
|
3d34d87a97 | ||
|
|
461ace9303 | ||
|
|
d19bb98abd | ||
|
|
c24af5ca6e | ||
|
|
26045fc4dc | ||
|
|
bf260d4b7e | ||
|
|
db56ff74a9 | ||
|
|
e03efe0b34 | ||
|
|
c587988a3a | ||
|
|
f2d5245bd8 | ||
|
|
97e1960abd | ||
|
|
e749b77ca7 | ||
|
|
6f3c163ee7 | ||
|
|
3b81ec57ea | ||
|
|
38874433c7 | ||
|
|
4eda3e756f | ||
|
|
4815b1afed | ||
|
|
99ac732d10 | ||
|
|
e8b0c62818 | ||
|
|
1108f1670c | ||
|
|
3575a2df18 | ||
|
|
c84c485493 | ||
|
|
8298ac461e | ||
|
|
105d04768e | ||
|
|
f1cca43d9c | ||
|
|
e8352fa265 | ||
|
|
37d84ddd73 | ||
|
|
6f9869e9c2 | ||
|
|
abc061ba46 | ||
|
|
95c80e5677 | ||
|
|
ba1758e5c7 | ||
|
|
36198c1993 | ||
|
|
8708fd8514 | ||
|
|
44a320ee74 | ||
|
|
520b8ecef4 | ||
|
|
34aa43ead3 | ||
|
|
8ecc3b9a78 | ||
|
|
c0d1d0566f | ||
|
|
2e01cab3f8 | ||
|
|
bbe99b5436 | ||
|
|
246ab41479 | ||
|
|
48dd3fa5bf | ||
|
|
7db5fe32a8 | ||
|
|
38a1dfa336 | ||
|
|
8b41b531a6 | ||
|
|
4a048a4918 | ||
|
|
ae64370478 | ||
|
|
b03f355fae | ||
|
|
8085e0031e | ||
|
|
dbc2dcc625 | ||
|
|
ad6227006e | ||
|
|
4f414cf70f | ||
|
|
adb2680289 | ||
|
|
f1f621d178 | ||
|
|
996e162d11 | ||
|
|
840f72a589 | ||
|
|
eca83f83ac | ||
|
|
0f070b494a | ||
|
|
0ea1e0c03d | ||
|
|
c49d9b57ed | ||
|
|
8d7e0f5190 | ||
|
|
012781103a | ||
|
|
34b3bf9e80 | ||
|
|
4c203a4777 | ||
|
|
02c49ee423 | ||
|
|
3f243d1c11 | ||
|
|
cf37d59d47 | ||
|
|
75c10c75d9 | ||
|
|
51e06bdbb0 | ||
|
|
462fb1ae7e | ||
|
|
aea5b91649 | ||
|
|
e3e96d546a | ||
|
|
46c2355a7e | ||
|
|
423e430e04 | ||
|
|
70b11d9015 | ||
|
|
5687f2f5a9 | ||
|
|
b82a8d681d | ||
|
|
29af22bd6e | ||
|
|
68f8127286 | ||
|
|
647e1a06d5 | ||
|
|
84759b5dbd | ||
|
|
39f62374aa | ||
|
|
4de2cbcf9f | ||
|
|
3f9ca423bd | ||
|
|
bbed019ca5 | ||
|
|
3081d76787 | ||
|
|
1a0ddff82b | ||
|
|
e9b40a12c8 | ||
|
|
3adb4afd60 | ||
|
|
c7af67f3a3 | ||
|
|
cca9298e74 | ||
|
|
4f9e23e0bc | ||
|
|
ded62a556b | ||
|
|
508f4d1b52 | ||
|
|
050ccb847a | ||
|
|
b67e61d625 | ||
|
|
9a47ec1ed9 | ||
|
|
c5c27768f2 | ||
|
|
4dd944a8c1 | ||
|
|
e22aff51a4 | ||
|
|
6adc26a897 | ||
|
|
9726726e5a | ||
|
|
6202abb75c | ||
|
|
3c65923d33 | ||
|
|
daed6e1064 | ||
|
|
f566f41fb3 | ||
|
|
e4190a0315 | ||
|
|
ca0d1d0b50 | ||
|
|
4e57226a1b | ||
|
|
a89b76d648 | ||
|
|
d0497fc886 | ||
|
|
a1c007599f | ||
|
|
81040a57a6 | ||
|
|
09fe5a2cb7 | ||
|
|
d760aa41c5 | ||
|
|
53576c6013 | ||
|
|
029466830d | ||
|
|
31595196fc | ||
|
|
2a0b582911 | ||
|
|
63fb30daf8 | ||
|
|
675b29b4fe | ||
|
|
075ccf6a82 | ||
|
|
ae6f3b6753 | ||
|
|
1a87a4cd0c | ||
|
|
f83092865b | ||
|
|
b15736accd | ||
|
|
79ce6f00d3 | ||
|
|
7f43acc167 | ||
|
|
f8fbd6cec8 | ||
|
|
9d11dd0787 | ||
|
|
4c31b6b689 | ||
|
|
d468fae0ea | ||
|
|
3a5a1b4f52 | ||
|
|
0724bd12a5 | ||
|
|
c7e3e642fa | ||
|
|
ee4c8f2a3f | ||
|
|
1092ffc9ea | ||
|
|
f4b6eb9869 | ||
|
|
5e49a4e05b | ||
|
|
adc50f07eb | ||
|
|
874c758340 | ||
|
|
ed031f01ba | ||
|
|
4cbae25ac0 | ||
|
|
7f002ac612 | ||
|
|
9e8fcbc879 | ||
|
|
c8a24a8647 | ||
|
|
e83a970b3a | ||
|
|
514d27fd66 | ||
|
|
16976fce4e | ||
|
|
3dc2304d6e | ||
|
|
7a04a857f2 | ||
|
|
a107cd98db | ||
|
|
d7f52af371 |
@@ -25,3 +25,9 @@ pip-log.txt
|
|||||||
|
|
||||||
#Mr Developer
|
#Mr Developer
|
||||||
.mr.developer.cfg
|
.mr.developer.cfg
|
||||||
|
|
||||||
|
#Virtualenv
|
||||||
|
env/
|
||||||
|
|
||||||
|
#Editor temporaries
|
||||||
|
*~
|
||||||
|
|||||||
+4
-3
@@ -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;'
|
||||||
|
|||||||
@@ -3,6 +3,117 @@ 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.7
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released July 11th 2013
|
||||||
|
|
||||||
|
- Made password length form error message configurable
|
||||||
|
- Fixed email confirmation bug that prevented logged in users from confirming their email
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.6
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 28th 2013
|
||||||
|
|
||||||
|
- Fixed dependency versions
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.5
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 20th 2013
|
||||||
|
|
||||||
|
- Fixed bug in `flask.ext.security.confirmable.generate_confirmation_link`
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.4
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 18th 2013
|
||||||
|
|
||||||
|
- Added `SECURITY_DEFAULT_REMEMBER_ME` configuration value to unify behavior between endpoints
|
||||||
|
- Fixed Flask-Login dependency problem
|
||||||
|
- Added optional `next` parameter to registration endpoint, similar to that of login
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released May 8th 2013
|
||||||
|
|
||||||
|
- Fixed bug in regards to imports with latest version of MongoEngine
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released April 4th 2013
|
||||||
|
|
||||||
|
- Fixed bug with http basic auth
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released April 3rd 2013
|
||||||
|
|
||||||
|
- Fixed bug with signals
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
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
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released November 26th 2012
|
||||||
|
|
||||||
|
- Fixed bug with `next` form variable
|
||||||
|
- Added better documentation regarding Flask-Mail configuration
|
||||||
|
- Added ability to configure email subjects
|
||||||
|
|
||||||
Version 1.5.0
|
Version 1.5.0
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
|
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
|
||||||
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
|
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
|
||||||
<li><a href="http://github.com/jfinkels/flask-security/issues">Issue Tracker</a></li>
|
<li><a href="http://github.com/mattupstate/flask-security/issues">Issue Tracker</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="http://pypi.python.org/pypi/Flask-Social">Flask-Social</a></li>
|
<li><a href="http://pypi.python.org/pypi/Flask-Social">Flask-Social</a></li>
|
||||||
|
|||||||
+52
-2
@@ -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
@@ -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.0'
|
version = '1.6.7'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = version
|
release = version
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +156,34 @@ 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
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
|
||||||
|
|
||||||
|
=========================================== ====================================
|
||||||
|
``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the
|
||||||
|
confirmation email. Defaults to
|
||||||
|
``Welcome``
|
||||||
|
``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the
|
||||||
|
passwordless feature. Defaults to
|
||||||
|
``Login instructions``
|
||||||
|
``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password
|
||||||
|
notice. Defaults to
|
||||||
|
``Your password has been reset``
|
||||||
|
``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the password
|
||||||
|
reset. Defaults to
|
||||||
|
``Password reset instructions``
|
||||||
|
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
|
||||||
|
confirmation message. Defaults to
|
||||||
|
``Please confirm your email``
|
||||||
|
=========================================== ====================================
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
-------------
|
-------------
|
||||||
@@ -136,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
|
||||||
@@ -167,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``.
|
||||||
======================================= ========================================
|
======================================= ========================================
|
||||||
|
|||||||
+47
-1
@@ -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`
|
||||||
|
|||||||
+6
-5
@@ -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
|
||||||
+6
-6
@@ -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**
|
||||||
|
|
||||||
|
|||||||
+192
-10
@@ -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,8 +72,181 @@ 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
|
||||||
|
===================
|
||||||
|
|
||||||
|
Flask-Security integrates with Flask-Mail to handle all email
|
||||||
|
communications between user and site, so it's important to configure
|
||||||
|
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 basic application code in the previous section::
|
||||||
|
|
||||||
|
# At top of file
|
||||||
|
from flask_mail import Mail
|
||||||
|
|
||||||
|
# After 'Create app'
|
||||||
|
app.config['MAIL_SERVER'] = 'smtp.example.com'
|
||||||
|
app.config['MAIL_PORT'] = 465
|
||||||
|
app.config['MAIL_USE_SSL'] = True
|
||||||
|
app.config['MAIL_USERNAME'] = 'username'
|
||||||
|
app.config['MAIL_PASSWORD'] = 'password'
|
||||||
|
mail = Mail(app)
|
||||||
|
|
||||||
|
To learn more about the various Flask-Mail settings to configure it to
|
||||||
|
work with your particular email server configuration, please see the
|
||||||
|
`Flask-Mail documentation <http://packages.python.org/Flask-Mail/>`_.
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
:license: MIT, see LICENSE for more details.
|
:license: MIT, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = '1.5.0'
|
__version__ = '1.6.7'
|
||||||
|
|
||||||
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, \
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -14,7 +14,8 @@ from datetime import datetime
|
|||||||
from flask import current_app as app, request
|
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
|
||||||
from .signals import user_confirmed, confirm_instructions_sent
|
from .signals import user_confirmed, confirm_instructions_sent
|
||||||
|
|
||||||
|
|
||||||
@@ -26,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):
|
||||||
@@ -39,11 +39,11 @@ def send_confirmation_instructions(user):
|
|||||||
|
|
||||||
confirmation_link, token = generate_confirmation_link(user)
|
confirmation_link, token = generate_confirmation_link(user)
|
||||||
|
|
||||||
send_mail('Please confirm your email', user.email,
|
send_mail(config_value('EMAIL_SUBJECT_CONFIRM'), user.email,
|
||||||
'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
|
||||||
|
|
||||||
|
|
||||||
@@ -52,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -79,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)
|
||||||
|
|||||||
+102
-16
@@ -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,8 +74,16 @@ _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_HTTP_AUTH_REALM': 'Login Required'
|
'DEFAULT_REMEMBER_ME': False,
|
||||||
|
'DEFAULT_HTTP_AUTH_REALM': 'Login Required',
|
||||||
|
'EMAIL_SUBJECT_REGISTER': 'Welcome',
|
||||||
|
'EMAIL_SUBJECT_CONFIRM': 'Please confirm your email',
|
||||||
|
'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions',
|
||||||
|
'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset',
|
||||||
|
'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed',
|
||||||
|
'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions'
|
||||||
}
|
}
|
||||||
|
|
||||||
#: Default Flask-Security messages
|
#: Default Flask-Security messages
|
||||||
@@ -71,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'),
|
||||||
@@ -83,8 +107,39 @@ _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'),
|
||||||
|
'PASSWORD_INVALID_LENGTH': ('Password must be at least 6 characters', 'error'),
|
||||||
|
'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'),
|
||||||
|
'INVALID_PASSWORD': ('Invalid password', 'error'),
|
||||||
'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'),
|
'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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -100,8 +155,7 @@ def _token_loader(token):
|
|||||||
return user
|
return user
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
return AnonymousUser()
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _identity_loader():
|
def _identity_loader():
|
||||||
@@ -126,6 +180,14 @@ def _get_login_manager(app):
|
|||||||
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
|
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
|
||||||
lm.user_loader(_user_loader)
|
lm.user_loader(_user_loader)
|
||||||
lm.token_loader(_token_loader)
|
lm.token_loader(_token_loader)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@@ -138,7 +200,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):
|
||||||
@@ -165,6 +230,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -179,8 +248,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):
|
||||||
@@ -199,14 +267,16 @@ class UserMixin(BaseUserMixin):
|
|||||||
"""Returns `True` if the user identifies with the specified role.
|
"""Returns `True` if the user identifies with the specified role.
|
||||||
|
|
||||||
:param role: A role name or `Role` instance"""
|
:param role: A role name or `Role` instance"""
|
||||||
return role in self.roles
|
if isinstance(role, basestring):
|
||||||
|
return role in (role.name for role in self.roles)
|
||||||
|
else:
|
||||||
|
return role in self.roles
|
||||||
|
|
||||||
|
|
||||||
class AnonymousUser(AnonymousUserBase):
|
class AnonymousUser(AnonymousUserMixin):
|
||||||
"""AnonymousUser definition"""
|
"""AnonymousUser definition"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(AnonymousUser, self).__init__()
|
|
||||||
self.roles = ImmutableList()
|
self.roles = ImmutableList()
|
||||||
|
|
||||||
def has_role(self, *args):
|
def has_role(self, *args):
|
||||||
@@ -246,6 +316,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)
|
||||||
|
|
||||||
@@ -272,12 +345,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
|
||||||
|
|
||||||
@@ -289,7 +367,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__))
|
||||||
|
|||||||
+98
-10
@@ -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.
|
||||||
|
|
||||||
@@ -56,8 +65,11 @@ class UserDatastore(object):
|
|||||||
self.role_model = role_model
|
self.role_model = role_model
|
||||||
|
|
||||||
def _prepare_role_modify_args(self, user, role):
|
def _prepare_role_modify_args(self, user, role):
|
||||||
role = role.name if isinstance(role, self.role_model) else role
|
if isinstance(user, basestring):
|
||||||
return self.find_user(email=user.email), self.find_role(role)
|
user = self.find_user(email=user)
|
||||||
|
if isinstance(role, basestring):
|
||||||
|
role = self.find_role(role)
|
||||||
|
return user, role
|
||||||
|
|
||||||
def _prepare_create_user_args(self, **kwargs):
|
def _prepare_create_user_args(self, **kwargs):
|
||||||
kwargs.setdefault('active', True)
|
kwargs.setdefault('active', True)
|
||||||
@@ -69,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):
|
||||||
@@ -83,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
|
||||||
@@ -134,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."""
|
||||||
|
|
||||||
@@ -172,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
|
||||||
|
|||||||
@@ -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::
|
||||||
|
|||||||
+127
-38
@@ -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')
|
||||||
|
password_length = Length(min=6, max=128, message='PASSWORD_INVALID_LENGTH')
|
||||||
|
|
||||||
|
|
||||||
|
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, password_length])
|
||||||
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,28 @@ 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, password_length])
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ from flask import request, current_app as app
|
|||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
from .signals import login_instructions_sent
|
from .signals import login_instructions_sent
|
||||||
from .utils import send_mail, url_for_security, get_token_status
|
from .utils import send_mail, url_for_security, get_token_status, \
|
||||||
|
config_value
|
||||||
|
|
||||||
|
|
||||||
# Convenient references
|
# Convenient references
|
||||||
@@ -29,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('Login Instructions', 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):
|
||||||
@@ -44,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):
|
||||||
|
|||||||
@@ -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
|
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('Password reset instructions', 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):
|
||||||
@@ -45,7 +44,7 @@ def send_password_reset_notice(user):
|
|||||||
|
|
||||||
:param user: The user to send the notice to
|
:param user: The user to send the notice to
|
||||||
"""
|
"""
|
||||||
send_mail('Your password has been reset', user.email,
|
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
|
||||||
'reset_notice', user=user)
|
'reset_notice', user=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)
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ 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
|
||||||
|
|
||||||
# Convenient references
|
# Convenient references
|
||||||
_security = LocalProxy(lambda: app.extensions['security'])
|
_security = LocalProxy(lambda: app.extensions['security'])
|
||||||
@@ -32,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('Welcome', 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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+79
-35
@@ -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)
|
||||||
@@ -76,10 +77,13 @@ def login():
|
|||||||
if not request.json:
|
if not request.json:
|
||||||
return redirect(get_post_login_redirect())
|
return redirect(get_post_login_redirect())
|
||||||
|
|
||||||
|
form.next.data = get_url(request.args.get('next')) \
|
||||||
|
or get_url(request.form.get('next')) or ''
|
||||||
|
|
||||||
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'))
|
||||||
|
|
||||||
@@ -98,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)
|
||||||
@@ -118,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'))
|
||||||
|
|
||||||
@@ -133,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)
|
||||||
@@ -146,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'))
|
||||||
|
|
||||||
@@ -166,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'))
|
||||||
|
|
||||||
@@ -176,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)
|
||||||
@@ -189,18 +195,18 @@ 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'))
|
||||||
|
|
||||||
|
|
||||||
@anonymous_user_required
|
|
||||||
def confirm_email(token):
|
def confirm_email(token):
|
||||||
"""View function which handles a email confirmation request."""
|
"""View function which handles a email confirmation request."""
|
||||||
|
|
||||||
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)
|
||||||
@@ -210,8 +216,11 @@ def confirm_email(token):
|
|||||||
return redirect(get_url(_security.confirm_error_view) or
|
return redirect(get_url(_security.confirm_error_view) or
|
||||||
url_for('send_confirmation'))
|
url_for('send_confirmation'))
|
||||||
|
|
||||||
|
if user != current_user:
|
||||||
|
logout_user()
|
||||||
|
login_user(user)
|
||||||
|
|
||||||
confirm_user(user)
|
confirm_user(user)
|
||||||
login_user(user, True)
|
|
||||||
after_this_request(_commit)
|
after_this_request(_commit)
|
||||||
do_flash(*get_message('EMAIL_CONFIRMED'))
|
do_flash(*get_message('EMAIL_CONFIRMED'))
|
||||||
|
|
||||||
@@ -222,10 +231,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)
|
||||||
@@ -235,7 +246,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'))
|
||||||
|
|
||||||
@@ -254,27 +265,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)
|
||||||
@@ -303,6 +342,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
@@ -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
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ source-dir = docs/
|
|||||||
build-dir = docs/_build
|
build-dir = docs/_build
|
||||||
|
|
||||||
[upload_sphinx]
|
[upload_sphinx]
|
||||||
upload-dir = docs/_build
|
upload-dir = docs/_build/html
|
||||||
@@ -20,7 +20,7 @@ from setuptools import setup
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='Flask-Security',
|
name='Flask-Security',
|
||||||
version='1.5.0',
|
version='1.6.7',
|
||||||
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
@@ -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)
|
||||||
|
|||||||
+351
-35
@@ -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)
|
||||||
|
|
||||||
@@ -121,6 +209,7 @@ class ConfirmableTests(SecurityTest):
|
|||||||
AUTH_CONFIG = {
|
AUTH_CONFIG = {
|
||||||
'SECURITY_CONFIRMABLE': True,
|
'SECURITY_CONFIRMABLE': True,
|
||||||
'SECURITY_REGISTERABLE': True,
|
'SECURITY_REGISTERABLE': True,
|
||||||
|
'SECURITY_EMAIL_SUBJECT_REGISTER': 'Custom welcome subject',
|
||||||
'USER_COUNT': 1
|
'USER_COUNT': 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,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):
|
||||||
@@ -148,6 +237,7 @@ class ConfirmableTests(SecurityTest):
|
|||||||
self.register(e)
|
self.register(e)
|
||||||
self.assertEqual(len(outbox), 1)
|
self.assertEqual(len(outbox), 1)
|
||||||
self.assertIn(e, outbox[0].html)
|
self.assertIn(e, outbox[0].html)
|
||||||
|
self.assertEqual('Custom welcome subject', outbox[0].subject)
|
||||||
|
|
||||||
def test_confirm_email(self):
|
def test_confirm_email(self):
|
||||||
e = 'dude@lp.com'
|
e = 'dude@lp.com'
|
||||||
@@ -163,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"}',
|
||||||
@@ -172,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'
|
||||||
@@ -182,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 = {
|
||||||
@@ -224,9 +333,27 @@ 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)
|
||||||
|
|
||||||
|
def test_confirm_email_of_user_different_than_current_user(self):
|
||||||
|
e1 = 'dude@lp.com'
|
||||||
|
e2 = 'lady@lp.com'
|
||||||
|
|
||||||
|
with capture_registrations() as registrations:
|
||||||
|
self.register(e1)
|
||||||
|
self.register(e2)
|
||||||
|
token1 = registrations[0]['confirm_token']
|
||||||
|
token2 = registrations[1]['confirm_token']
|
||||||
|
|
||||||
|
self.client.get('/confirm/' + token1, follow_redirects=True)
|
||||||
|
self.client.get('/logout')
|
||||||
|
self.authenticate(email=e1)
|
||||||
|
r = self.client.get('/confirm/' + token2, follow_redirects=True)
|
||||||
|
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
|
||||||
|
self.assertIn(msg, r.data)
|
||||||
|
self.assertIn('Hello %s' % e2, r.data)
|
||||||
|
|
||||||
|
|
||||||
class RecoverableTests(SecurityTest):
|
class RecoverableTests(SecurityTest):
|
||||||
|
|
||||||
@@ -238,7 +365,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']
|
||||||
@@ -248,23 +375,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']
|
||||||
@@ -296,14 +423,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)
|
||||||
@@ -311,6 +437,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('Password must be at least 6 characters', 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 = {
|
||||||
@@ -341,20 +556,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):
|
||||||
@@ -363,9 +577,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)
|
||||||
|
|
||||||
@@ -394,9 +607,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)
|
||||||
@@ -424,9 +636,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)
|
||||||
@@ -460,19 +670,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)
|
||||||
@@ -481,3 +692,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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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():
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user