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