mirror of
https://github.com/wassname/flask-security.git
synced 2026-08-07 11:22:21 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b1ab24341 | ||
|
|
ea77940ffc | ||
|
|
2adc572e80 | ||
|
|
2ac4f373ab | ||
|
|
132ced5675 | ||
|
|
aeb5ffbb1d | ||
|
|
c2ee2e2629 | ||
|
|
8005709997 | ||
|
|
2abdaa873a | ||
|
|
4508c717fb | ||
|
|
9583dc3e63 | ||
|
|
745fae1ab1 | ||
|
|
7e8551763e | ||
|
|
4049c06203 | ||
|
|
3b678b872f | ||
|
|
17a79ed6a3 | ||
|
|
d65c3cadcd | ||
|
|
bf01ad0727 | ||
|
|
cbd0db7c39 | ||
|
|
398f5c920b | ||
|
|
33252ae178 | ||
|
|
c10c9050c7 | ||
|
|
5697ff80c3 | ||
|
|
4411470202 | ||
|
|
a0e2037747 | ||
|
|
7884d637c5 | ||
|
|
8a14abaa1e | ||
|
|
6b55e9613a | ||
|
|
d08aac6d35 | ||
|
|
10fd1844d8 | ||
|
|
72d3a4b5e3 | ||
|
|
6541640ee8 | ||
|
|
9cda8baff3 | ||
|
|
d907e5b890 | ||
|
|
8a62b5f193 | ||
|
|
2e08ec87a6 | ||
|
|
79fe821c80 | ||
|
|
4d3c1c0bdc | ||
|
|
cd8982fa65 | ||
|
|
c8a3549e2d | ||
|
|
03d9cf2f0d | ||
|
|
e4d9d3ad17 | ||
|
|
f2a5e4b614 | ||
|
|
916f5ee012 | ||
|
|
bc1f5dd7f9 | ||
|
|
4659d10c5c | ||
|
|
a4581681e5 | ||
|
|
248ea5d272 | ||
|
|
4debc8d102 | ||
|
|
fccaccd282 | ||
|
|
3681823fcf | ||
|
|
923ad720a1 | ||
|
|
f4d758f561 | ||
|
|
665b164618 | ||
|
|
4d70f016ad | ||
|
|
7e4fc94601 | ||
|
|
6cfe662dc6 | ||
|
|
8c45271bf9 | ||
|
|
c7d0ea9cce | ||
|
|
94c7c09dc2 | ||
|
|
f6405797f1 | ||
|
|
2cffb6634d | ||
|
|
824a52b883 | ||
|
|
fe7e4c4afc | ||
|
|
591bc27a5e | ||
|
|
897b2fceab | ||
|
|
5bc37add88 | ||
|
|
8b036f2a3e | ||
|
|
76ad77a233 | ||
|
|
679cee7969 | ||
|
|
3d7b97ac31 | ||
|
|
3a0af73231 | ||
|
|
3458391791 | ||
|
|
37908ca335 | ||
|
|
f83cd56ff2 | ||
|
|
c10ec29fe5 | ||
|
|
d2fe7aefcb | ||
|
|
708ddeb6c4 | ||
|
|
2aeee348d4 | ||
|
|
52b177cd2e | ||
|
|
dab2fc8c8b | ||
|
|
31e3ab5470 | ||
|
|
6bdcd2930d | ||
|
|
1076887900 | ||
|
|
285fe888da | ||
|
|
bf139847cb | ||
|
|
15c9ef1d07 | ||
|
|
d1ec38ea18 | ||
|
|
577a3a8110 | ||
|
|
7b474efd7a | ||
|
|
f12bc17e7e | ||
|
|
999f882f61 | ||
|
|
a140c01b57 | ||
|
|
76cf3eaf6a | ||
|
|
a6b5d3053c | ||
|
|
0a48997fdd | ||
|
|
96f1b3e0d1 | ||
|
|
0facdaacd9 | ||
|
|
9cadf855a4 | ||
|
|
f387759cee | ||
|
|
faa9f65596 | ||
|
|
3d5b37cef2 | ||
|
|
d185407ac4 | ||
|
|
58b7fa8e2e | ||
|
|
721b985bc4 | ||
|
|
b08d7c2085 | ||
|
|
fff71e3e04 | ||
|
|
546680a94b | ||
|
|
00e121c869 | ||
|
|
7098a6757c | ||
|
|
510d1356a2 | ||
|
|
e5111dbb0c | ||
|
|
44b91a351a | ||
|
|
4b58a00abd | ||
|
|
f3014d01df | ||
|
|
ed645b51f2 | ||
|
|
bc60c021a3 | ||
|
|
9f00613906 | ||
|
|
0f872af03a | ||
|
|
1d3a75d77b | ||
|
|
1395df334e | ||
|
|
0268a2d568 | ||
|
|
09f6f64e5b | ||
|
|
8e8d581f0d | ||
|
|
65b454ca7e | ||
|
|
2006d2ae3d | ||
|
|
c0673c1239 | ||
|
|
922a038d28 | ||
|
|
668b4ee651 | ||
|
|
f854c24094 | ||
|
|
d314e54680 | ||
|
|
9400032a1f | ||
|
|
2e57734d1f | ||
|
|
c658ee4500 | ||
|
|
45c8951877 | ||
|
|
b4d1a7c921 | ||
|
|
f47fce9365 | ||
|
|
b2174bf035 | ||
|
|
316d945d96 | ||
|
|
6eb77c06ad | ||
|
|
35fd08772b | ||
|
|
76fc578cf5 | ||
|
|
91ee026ff7 | ||
|
|
e3b74acc5a | ||
|
|
968f85a39c | ||
|
|
68ecc7bb10 | ||
|
|
ac2dfe964b | ||
|
|
730b86fbc7 | ||
|
|
850b0e714b | ||
|
|
8d2815798f | ||
|
|
01a4199a16 | ||
|
|
910d80d511 | ||
|
|
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 | ||
|
|
6494275c8c | ||
|
|
aa80198047 | ||
|
|
23b128bcbd | ||
|
|
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 | ||
|
|
0f46f35981 | ||
|
|
c0d9eecf10 | ||
|
|
04bb2c4041 | ||
|
|
8eeb832d2e | ||
|
|
8f760aadbd | ||
|
|
03d27cd600 | ||
|
|
d30a27b3bb | ||
|
|
78903fa2e5 | ||
|
|
514de64303 | ||
|
|
bad63265f8 | ||
|
|
66a9dcd2e6 | ||
|
|
df1647f1f9 | ||
|
|
89ecded480 | ||
|
|
2b35b37a66 | ||
|
|
4d8a813004 | ||
|
|
06312ef50f | ||
|
|
c3ad5b2fa6 | ||
|
|
637fc913cd | ||
|
|
e00522f331 |
+13
@@ -31,3 +31,16 @@ env/
|
|||||||
|
|
||||||
#Editor temporaries
|
#Editor temporaries
|
||||||
*~
|
*~
|
||||||
|
|
||||||
|
*.db
|
||||||
|
*cache*
|
||||||
|
|
||||||
|
# vim
|
||||||
|
[._]*.s[a-w][a-z]
|
||||||
|
[._]s[a-w][a-z]
|
||||||
|
*.un~
|
||||||
|
Session.vim
|
||||||
|
.netrwhist
|
||||||
|
*~
|
||||||
|
|
||||||
|
.eggs/README.txt
|
||||||
|
|||||||
+23
-7
@@ -3,20 +3,36 @@ language: python
|
|||||||
python:
|
python:
|
||||||
- "2.6"
|
- "2.6"
|
||||||
- "2.7"
|
- "2.7"
|
||||||
|
- "3.3"
|
||||||
|
- "3.4"
|
||||||
|
- "3.5"
|
||||||
- "pypy"
|
- "pypy"
|
||||||
|
|
||||||
install:
|
sudo: false
|
||||||
- pip install . --quiet --use-mirrors
|
|
||||||
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib --quiet --use-mirrors; fi"
|
|
||||||
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors
|
|
||||||
|
|
||||||
before_script:
|
cache:
|
||||||
- mysql -e 'create database flask_security_test;'
|
- apt
|
||||||
|
- pip
|
||||||
|
|
||||||
|
install:
|
||||||
|
- pip install -r requirements.txt -r requirements-dev.txt -e .
|
||||||
|
- pip install coverage coveralls
|
||||||
|
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib; fi"
|
||||||
|
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
sources:
|
||||||
|
- deadsnakes
|
||||||
|
packages:
|
||||||
|
- python3.5
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- mongodb
|
- mongodb
|
||||||
|
|
||||||
script: nosetests
|
script: py.test --cov flask_security --cov-report term-missing --pep8 --flakes
|
||||||
|
|
||||||
|
after_script:
|
||||||
|
- coveralls
|
||||||
|
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
|
Walt Askew
|
||||||
@@ -3,6 +3,137 @@ 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.5
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released December 2nd 2015
|
||||||
|
|
||||||
|
- Added `SECURITY_TOKEN_MAX_AGE` configuration setting
|
||||||
|
- Fixed calls to `SQLAlchemyUserDatastore.get_user(None)` (this now returns `False` instead of raising a `TypeError`
|
||||||
|
- Fixed URL generation adding extra slashes in some cases (see GitHub #343)
|
||||||
|
- Fixed handling of trackable IP addresses when the `X-Forwarded-For` header contains multiple values
|
||||||
|
- Include WWW-Authenticate headers in `@auth_required` authentication checks
|
||||||
|
- Fixed error when `check_token` function is used with a json list
|
||||||
|
- Added support for custom `AnonymousUser` classes
|
||||||
|
- Restricted `forgot_password` endpoint to anonymous users
|
||||||
|
- Allowed unauthorized callback to be overridden
|
||||||
|
- Fixed issue where passwords cannot be reset if currently set to `None`
|
||||||
|
- Ensured that password reset tokens are invalidated after use
|
||||||
|
- Updated `is_authenticated` and `is_active` functions to support Flask-Login changes
|
||||||
|
- Various documentation improvements
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.7.4
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released October 13th 2014
|
||||||
|
|
||||||
|
- Fixed a bug related to changing existing passwords from plaintext to hashed
|
||||||
|
- Fixed a bug in form validation that did not enforce case insensivitiy
|
||||||
|
- Fixed a bug with validating redirects
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.7.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 10th 2014
|
||||||
|
|
||||||
|
- Fixed a bug where redirection to `SECURITY_POST_LOGIN_VIEW` was not respected
|
||||||
|
- Fixed string encoding in various places to be friendly to unicode
|
||||||
|
- Now using `werkzeug.security.safe_str_cmp` to check tokens
|
||||||
|
- Removed user information from JSON output on `/reset` responses
|
||||||
|
- Added Python 3.4 support
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.7.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released May 6th 2014
|
||||||
|
|
||||||
|
- Updated IP tracking to check for `X-Forwarded-For` header
|
||||||
|
- Fixed a bug regarding the re-hashing of passwords with a new algorithm
|
||||||
|
- Fixed a bug regarding the `password_changed` signal.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.7.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released January 14th 2014
|
||||||
|
|
||||||
|
- Fixed a bug where passwords would fail to verify when specifying a password hash algorithm
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.7.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released January 10th 2014
|
||||||
|
|
||||||
|
- Python 3.3 support!
|
||||||
|
- Dependency updates
|
||||||
|
- Fixed a bug when `SECURITY_LOGIN_WITHOUT_CONFIRMATION = True` did not allow users to log in
|
||||||
|
- Added `SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL` configuraiton option to optionally send password reset notice emails
|
||||||
|
- Add documentation for `@security.send_mail_task`
|
||||||
|
- Move to `request.get_json` as `request.json` is now deprecated in Flask
|
||||||
|
- Fixed a bug when using AJAX to change a user's password
|
||||||
|
- Added documentation for select functions in the `flask_security.utils` module
|
||||||
|
- Fixed a bug in `flask_security.forms.NextFormMixin`
|
||||||
|
- Added `CHANGE_PASSWORD_TEMPLATE` configuration option to optionally specify a different change password template
|
||||||
|
- Added the ability to specify addtional fields on the user model to be used for identifying the user via the `USER_IDENTITY_ATTRIBUTES` configuration option
|
||||||
|
- An error is now shown if a user tries to change their password and the password is the same as before. The message can be customed with the `SECURITY_MSG_PASSWORD_IS_SAME` configuration option
|
||||||
|
- Fixed a bug in `MongoEngineUserDatastore` where user model would not be updated when using the `add_role_to_user` method
|
||||||
|
- Added `SECURITY_SEND_PASSWORD_CHANGE_EMAIL` configuration option to optionally disable password change email from being sent
|
||||||
|
- Fixed a bug in the `find_or_create_role` method of the PeeWee datastore
|
||||||
|
- Removed pypy tests
|
||||||
|
- Fixed some tests
|
||||||
|
- Include CHANGES and LICENSE in MANIFEST.in
|
||||||
|
- A bit of documentation cleanup
|
||||||
|
- A bit of code cleanup including removal of unnecessary utcnow call and simplification of get_max_age method
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.9
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released August 20th 2013
|
||||||
|
|
||||||
|
- Fix bug in SQLAlchemy datastore's `get_user` function
|
||||||
|
- Fix bug in PeeWee datastore's `remove_role_from_user` function
|
||||||
|
- Fixed import error caused by new Flask-WTF release
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.8
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released August 1st 2013
|
||||||
|
|
||||||
|
- Fixed bug with case sensitivity of email address during login
|
||||||
|
- Code cleanup regarding token_callback
|
||||||
|
- Ignore validation errors in find_user function for MongoEngineUserDatastore
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.7
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released July 11th 2013
|
||||||
|
|
||||||
|
- Made password length form error message configurable
|
||||||
|
- Fixed email confirmation bug that prevented logged in users from confirming their email
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.6
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 28th 2013
|
||||||
|
|
||||||
|
- Fixed dependency versions
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.5
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 20th 2013
|
||||||
|
|
||||||
|
- Fixed bug in `flask.ext.security.confirmable.generate_confirmation_link`
|
||||||
|
|
||||||
|
|
||||||
Version 1.6.4
|
Version 1.6.4
|
||||||
-------------
|
-------------
|
||||||
|
|||||||
+7
-2
@@ -1,2 +1,7 @@
|
|||||||
recursive-include tests *.py
|
include README.rst
|
||||||
recursive-include flask_security/templates *.*
|
include requirements.txt
|
||||||
|
include requirements-dev.txt
|
||||||
|
include tox.ini
|
||||||
|
recursive-include flask_security/templates *.*
|
||||||
|
recursive-include tests *
|
||||||
|
recursive-exclude tests/__pycache__ *
|
||||||
|
|||||||
+16
-1
@@ -3,6 +3,21 @@ Flask-Security
|
|||||||
|
|
||||||
.. image:: https://secure.travis-ci.org/mattupstate/flask-security.png?branch=develop
|
.. image:: https://secure.travis-ci.org/mattupstate/flask-security.png?branch=develop
|
||||||
|
|
||||||
|
.. image:: https://pypip.in/v/Flask-Security/badge.png
|
||||||
|
:target: https://pypi.python.org/pypi/Flask-Security/
|
||||||
|
:alt: Latest Version
|
||||||
|
|
||||||
|
.. image:: https://coveralls.io/repos/mattupstate/flask-security/badge.png?branch=develop
|
||||||
|
:target: https://coveralls.io/r/mattupstate/flask-security
|
||||||
|
|
||||||
|
.. image:: https://pypip.in/d/Flask-Security/badge.png
|
||||||
|
:target: https://pypi.python.org/pypi//Flask-Security/
|
||||||
|
:alt: Downloads
|
||||||
|
|
||||||
|
.. image:: https://pypip.in/license/Flask-Security/badge.png
|
||||||
|
:target: https://pypi.python.org/pypi/Flask-Security/
|
||||||
|
:alt: License
|
||||||
|
|
||||||
Flask-Security quickly adds security features to your Flask application.
|
Flask-Security quickly adds security features to your Flask application.
|
||||||
|
|
||||||
Resources
|
Resources
|
||||||
@@ -12,4 +27,4 @@ Resources
|
|||||||
- `Issue Tracker <http://github.com/mattupstate/flask-security/issues>`_
|
- `Issue Tracker <http://github.com/mattupstate/flask-security/issues>`_
|
||||||
- `Code <http://github.com/mattupstate/flask-security/>`_
|
- `Code <http://github.com/mattupstate/flask-security/>`_
|
||||||
- `Development Version
|
- `Development Version
|
||||||
<http://github.com/mattupstate/flask-security/zipball/develop#egg=Flask-Security-dev>`_
|
<http://github.com/mattupstate/flask-security/zipball/develop#egg=Flask-Security-dev>`_
|
||||||
|
|||||||
Vendored
+2
-2
@@ -9,9 +9,9 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
|
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
|
||||||
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
|
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
|
||||||
<li><a href="http://github.com/jfinkels/flask-security/issues">Issue Tracker</a></li>
|
<li><a href="http://github.com/mattupstate/flask-security/issues">Issue Tracker</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="http://pypi.python.org/pypi/Flask-Social">Flask-Social</a></li>
|
<li><a href="http://pypi.python.org/pypi/Flask-Social">Flask-Social</a></li>
|
||||||
<li><a href="http://github.com/mattupstate/flask-social">Flask-Social @ github</a></li>
|
<li><a href="http://github.com/mattupstate/flask-social">Flask-Social @ github</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+36
-19
@@ -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
|
||||||
@@ -65,43 +87,38 @@ sends the following signals.
|
|||||||
|
|
||||||
.. data:: user_registered
|
.. data:: user_registered
|
||||||
|
|
||||||
Sent when a user registers on the site. It is passed a dict with
|
Sent when a user registers on the site. In addition to the app (which is the
|
||||||
the `user` and `confirm_token`, the user being logged in and the
|
sender), it is passed `user` and `confirm_token` arguments.
|
||||||
(if so configured) the confirmation token issued.
|
|
||||||
|
|
||||||
.. data:: user_confirmed
|
.. data:: user_confirmed
|
||||||
|
|
||||||
Sent when a user is confirmed. It is passed `user`, which is the
|
Sent when a user is confirmed. In addition to the app (which is the
|
||||||
user being confirmed.
|
sender), it is passed a `user` argument.
|
||||||
|
|
||||||
.. data:: confirm_instructions_sent
|
.. data:: confirm_instructions_sent
|
||||||
|
|
||||||
Sent when a user requests confirmation instructions. It is passed
|
Sent when a user requests confirmation instructions. In addition to the app
|
||||||
the `user`.
|
(which is the sender), it is passed a `user` argument.
|
||||||
|
|
||||||
.. data:: login_instructions_sent
|
.. data:: login_instructions_sent
|
||||||
|
|
||||||
Sent when passwordless login is used and user logs in. It is passed
|
Sent when passwordless login is used and user logs in. In addition to the app
|
||||||
a dict with the `user` and `login_token`, the user being logged in
|
(which is the sender), it is passed `user` and `login_token` arguments.
|
||||||
and the (if so configured) the login token issued.
|
|
||||||
|
|
||||||
.. data:: password_reset
|
.. data:: password_reset
|
||||||
|
|
||||||
Sent when a user completes a password reset. It is passed the
|
Sent when a user completes a password reset. In addition to the app (which is
|
||||||
`user`.
|
the sender), it is passed a `user` argument.
|
||||||
|
|
||||||
.. data:: password_changed
|
.. data:: password_changed
|
||||||
|
|
||||||
Sent when a user completes a password change. It is passed the
|
Sent when a user completes a password change. In addition to the app (which is
|
||||||
`user`.
|
the sender), it is passed a `user` argument.
|
||||||
|
|
||||||
.. data:: reset_password_instructions_sent
|
.. data:: reset_password_instructions_sent
|
||||||
|
|
||||||
Sent when a user requests a password reset. It is passed a dict
|
Sent when a user requests a password reset. In addition to the app (which is
|
||||||
with the `user` and `token`, the user being logged in and
|
the sender), it is passed `user` and `token` arguments.
|
||||||
the (if so configured) the reset token issued.
|
|
||||||
|
|
||||||
All signals are also passed a `app` keyword argument, which is the
|
|
||||||
current application.
|
|
||||||
|
|
||||||
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
|
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
|
||||||
|
|||||||
@@ -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.4'
|
version = '1.7.5'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = version
|
release = version
|
||||||
|
|
||||||
|
|||||||
+156
-70
@@ -15,15 +15,18 @@ Core
|
|||||||
``SECURITY_URL_PREFIX`` Specifies the URL prefix for the
|
``SECURITY_URL_PREFIX`` Specifies the URL prefix for the
|
||||||
Flask-Security blueprint. Defaults to
|
Flask-Security blueprint. Defaults to
|
||||||
``None``.
|
``None``.
|
||||||
``SECURITY_FLASH_MESSAGES`` Specifies wether or not to flash
|
``SECURITY_FLASH_MESSAGES`` Specifies whether 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``,
|
||||||
``sha512_crypt``, or ``pbkdf2_sha512``.
|
``sha512_crypt``, or ``pbkdf2_sha512``.
|
||||||
Defaults to ``plaintext``.
|
Defaults to ``plaintext``. Note:
|
||||||
|
``bcrypt>=2.0.0`` is not currently
|
||||||
|
supported. If ``bcrypt`` is preferred,
|
||||||
|
please use ``bcrypt<2.0``.
|
||||||
``SECURITY_PASSWORD_SALT`` Specifies the HMAC salt. This is only
|
``SECURITY_PASSWORD_SALT`` Specifies the HMAC salt. This is only
|
||||||
used if the password hash type is set
|
used if the password hash type is set
|
||||||
to something other than plain text.
|
to something other than plain text.
|
||||||
@@ -31,12 +34,16 @@ Core
|
|||||||
``SECURITY_EMAIL_SENDER`` Specifies the email address to send
|
``SECURITY_EMAIL_SENDER`` Specifies the email address to send
|
||||||
emails as. Defaults to
|
emails as. Defaults to
|
||||||
``no-reply@localhost``.
|
``no-reply@localhost``.
|
||||||
``SECURITY_TOKEN_AUTHENTICATION_KEY`` Specifies the query sting parameter to
|
``SECURITY_TOKEN_AUTHENTICATION_KEY`` Specifies the query string parameter to
|
||||||
read when using token authentication.
|
read when using token authentication.
|
||||||
Defaults to ``auth_token``.
|
Defaults to ``auth_token``.
|
||||||
``SECURITY_TOKEN_AUTHENTICATION_HEADER`` Specifies the HTTP header to read when
|
``SECURITY_TOKEN_AUTHENTICATION_HEADER`` Specifies the HTTP header to read when
|
||||||
using token authentication. Defaults to
|
using token authentication. Defaults to
|
||||||
``Authentication-Token``.
|
``Authentication-Token``.
|
||||||
|
``SECURITY_TOKEN_MAX_AGE`` Specifies the number of seconds before
|
||||||
|
an authentication token expires.
|
||||||
|
Defaults to None, meaning the token
|
||||||
|
never expires.
|
||||||
``SECURITY_DEFAULT_HTTP_AUTH_REALM`` Specifies the default authentication
|
``SECURITY_DEFAULT_HTTP_AUTH_REALM`` Specifies the default authentication
|
||||||
realm when using basic HTTP auth.
|
realm when using basic HTTP auth.
|
||||||
Defaults to ``Login Required``
|
Defaults to ``Login Required``
|
||||||
@@ -56,6 +63,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
|
||||||
@@ -67,31 +76,37 @@ URLs and Views
|
|||||||
``SECURITY_CONFIRM_ERROR_VIEW`` Specifies the view to redirect to if a
|
``SECURITY_CONFIRM_ERROR_VIEW`` Specifies the view to redirect to if a
|
||||||
confirmation error occurs. This value can be set
|
confirmation error occurs. This value can be set
|
||||||
to a URL or an endpoint name. If this value is
|
to a URL or an endpoint name. If this value is
|
||||||
``None`` the user is presented the default view
|
``None``, the user is presented the default view
|
||||||
to resend a confirmation link. Defaults to
|
to resend a confirmation link. Defaults to
|
||||||
``None``.
|
``None``.
|
||||||
``SECURITY_POST_REGISTER_VIEW`` Specifies the view to redirect to after a user
|
``SECURITY_POST_REGISTER_VIEW`` Specifies the view to redirect to after a user
|
||||||
successfully registers. This value can be set to
|
successfully registers. This value can be set to
|
||||||
a URL or an endpoint name. If this value is
|
a URL or an endpoint name. If this value is
|
||||||
``None`` the user is redirected to the value of
|
``None``, the user is redirected to the value of
|
||||||
``SECURITY_POST_LOGIN_VIEW``. Defaults to
|
``SECURITY_POST_LOGIN_VIEW``. Defaults to
|
||||||
``None``.
|
``None``.
|
||||||
``SECURITY_POST_CONFIRM_VIEW`` Specifies the view to redirect to after a user
|
``SECURITY_POST_CONFIRM_VIEW`` Specifies the view to redirect to after a user
|
||||||
successfully confirms their email. This value
|
successfully confirms their email. 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
|
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
|
||||||
to ``None``.
|
to ``None``.
|
||||||
``SECURITY_POST_RESET_VIEW`` Specifies the view to redirect to after a user
|
``SECURITY_POST_RESET_VIEW`` Specifies the view to redirect to after a user
|
||||||
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
|
||||||
``None`` the user is presented with a default
|
``None``, the user is presented with a default
|
||||||
HTTP 403 response. Defaults to ``None``.
|
HTTP 403 response. Defaults to ``None``.
|
||||||
=============================== ================================================
|
=============================== ================================================
|
||||||
|
|
||||||
@@ -114,6 +129,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
|
||||||
@@ -133,7 +151,7 @@ Feature Flags
|
|||||||
========================= ======================================================
|
========================= ======================================================
|
||||||
``SECURITY_CONFIRMABLE`` Specifies if users are required to confirm their email
|
``SECURITY_CONFIRMABLE`` Specifies if users are required to confirm their email
|
||||||
address when registering a new account. If this value
|
address when registering a new account. If this value
|
||||||
is `True` Flask-Security creates an endpoint to handle
|
is `True`, Flask-Security creates an endpoint to handle
|
||||||
confirmations and requests to resend confirmation
|
confirmations and requests to resend confirmation
|
||||||
instructions. The URL for this endpoint is specified
|
instructions. The URL for this endpoint is specified
|
||||||
by the ``SECURITY_CONFIRM_URL`` configuration option.
|
by the ``SECURITY_CONFIRM_URL`` configuration option.
|
||||||
@@ -147,15 +165,20 @@ Feature Flags
|
|||||||
specified by the ``SECURITY_RESET_URL`` configuration
|
specified by the ``SECURITY_RESET_URL`` configuration
|
||||||
option. Defaults to ``False``.
|
option. Defaults to ``False``.
|
||||||
``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user
|
``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user
|
||||||
login statistics. If set to ``True`` ensure your
|
login statistics. If set to ``True``, ensure your
|
||||||
models have the required fields/attribues. Defaults to
|
models have the required fields/attribues. Be sure to
|
||||||
|
use `ProxyFix <http://flask.pocoo.org/docs/0.10/deploying/wsgi-standalone/#proxy-setups>` if you are using a proxy. Defaults to
|
||||||
``False``
|
``False``
|
||||||
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the
|
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the
|
||||||
passwordless login feature. If set to ``True`` users
|
passwordless login feature. If set to ``True``, users
|
||||||
are not required to enter a password to login but are
|
are not required to enter a password to login but are
|
||||||
sent an email with a login link. This feature is
|
sent an email with a login link. This feature is
|
||||||
experimental and should be used with caution. Defaults
|
experimental and should be used with caution. Defaults
|
||||||
to ``False``.
|
to ``False``.
|
||||||
|
``SECURITY_CHANGEABLE`` Specifies if Flask-Security should enable the
|
||||||
|
change password endpoint. The URL for this endpoint is
|
||||||
|
specified by the ``SECURITY_CHANGE_URL`` configuration
|
||||||
|
option. Defaults to ``False``.
|
||||||
========================= ======================================================
|
========================= ======================================================
|
||||||
|
|
||||||
Email
|
Email
|
||||||
@@ -163,64 +186,127 @@ 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``.
|
||||||
|
============================================= ==================================
|
||||||
|
|
||||||
|
Messages
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The following are the messages Flask-Security uses. They are tuples; the first
|
||||||
|
element is the message and the second element is the error level.
|
||||||
|
|
||||||
|
The default messages and error levels can be found in ``core.py``.
|
||||||
|
|
||||||
|
* ``SECURITY_MSG_ALREADY_CONFIRMED``
|
||||||
|
* ``SECURITY_MSG_CONFIRMATION_EXPIRED``
|
||||||
|
* ``SECURITY_MSG_CONFIRMATION_REQUEST``
|
||||||
|
* ``SECURITY_MSG_CONFIRMATION_REQUIRED``
|
||||||
|
* ``SECURITY_MSG_CONFIRM_REGISTRATION``
|
||||||
|
* ``SECURITY_MSG_DISABLED_ACCOUNT``
|
||||||
|
* ``SECURITY_MSG_EMAIL_ALREADY_ASSOCIATED``
|
||||||
|
* ``SECURITY_MSG_EMAIL_CONFIRMED``
|
||||||
|
* ``SECURITY_MSG_EMAIL_NOT_PROVIDED``
|
||||||
|
* ``SECURITY_MSG_INVALID_CONFIRMATION_TOKEN``
|
||||||
|
* ``SECURITY_MSG_INVALID_EMAIL_ADDRESS``
|
||||||
|
* ``SECURITY_MSG_INVALID_LOGIN_TOKEN``
|
||||||
|
* ``SECURITY_MSG_INVALID_PASSWORD``
|
||||||
|
* ``SECURITY_MSG_INVALID_REDIRECT``
|
||||||
|
* ``SECURITY_MSG_INVALID_RESET_PASSWORD_TOKEN``
|
||||||
|
* ``SECURITY_MSG_LOGIN``
|
||||||
|
* ``SECURITY_MSG_LOGIN_EMAIL_SENT``
|
||||||
|
* ``SECURITY_MSG_LOGIN_EXPIRED``
|
||||||
|
* ``SECURITY_MSG_PASSWORDLESS_LOGIN_SUCCESSFUL``
|
||||||
|
* ``SECURITY_MSG_PASSWORD_CHANGE``
|
||||||
|
* ``SECURITY_MSG_PASSWORD_INVALID_LENGTH``
|
||||||
|
* ``SECURITY_MSG_PASSWORD_IS_THE_SAME``
|
||||||
|
* ``SECURITY_MSG_PASSWORD_MISMATCH``
|
||||||
|
* ``SECURITY_MSG_PASSWORD_NOT_PROVIDED``
|
||||||
|
* ``SECURITY_MSG_PASSWORD_NOT_SET``
|
||||||
|
* ``SECURITY_MSG_PASSWORD_RESET``
|
||||||
|
* ``SECURITY_MSG_PASSWORD_RESET_EXPIRED``
|
||||||
|
* ``SECURITY_MSG_PASSWORD_RESET_REQUEST``
|
||||||
|
* ``SECURITY_MSG_REFRESH``
|
||||||
|
* ``SECURITY_MSG_RETYPE_PASSWORD_MISMATCH``
|
||||||
|
* ``SECURITY_MSG_UNAUTHORIZED``
|
||||||
|
* ``SECURITY_MSG_USER_DOES_NOT_EXIST``
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ Contents
|
|||||||
models
|
models
|
||||||
customizing
|
customizing
|
||||||
api
|
api
|
||||||
changelog
|
changelog
|
||||||
|
authors
|
||||||
|
|||||||
+42
-21
@@ -2,7 +2,7 @@ Customizing Views
|
|||||||
=================
|
=================
|
||||||
|
|
||||||
Flask-Security bootstraps your application with various views for handling its
|
Flask-Security bootstraps your application with various views for handling its
|
||||||
configured features to get you up and running as quick as possible. However,
|
configured features to get you up and running as quickly as possible. However,
|
||||||
you'll probably want to change the way these views look to be more in line with
|
you'll probably want to change the way these views look to be more in line with
|
||||||
your application's visual design.
|
your application's visual design.
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ Flask application context processor:
|
|||||||
* ``<template_name>_form``: A form object for the view
|
* ``<template_name>_form``: A form object for the view
|
||||||
* ``security``: The Flask-Security extension object
|
* ``security``: The Flask-Security extension object
|
||||||
|
|
||||||
To add more values to the template context you can specify a context processor
|
To add more values to the template context, you can specify a context processor
|
||||||
for all views or a specific view. For example::
|
for all views or a specific view. For example::
|
||||||
|
|
||||||
security = Security(app, user_datastore)
|
security = Security(app, user_datastore)
|
||||||
@@ -73,8 +73,8 @@ register form or override validators::
|
|||||||
from flask_security.forms import RegisterForm
|
from flask_security.forms import RegisterForm
|
||||||
|
|
||||||
class ExtendedRegisterForm(RegisterForm):
|
class ExtendedRegisterForm(RegisterForm):
|
||||||
first_name = TextField('First Name', [Required()])
|
first_name = StringField('First Name', [Required()])
|
||||||
last_name = TextField('Last Name', [Required()])
|
last_name = StringField('Last Name', [Required()])
|
||||||
|
|
||||||
security = Security(app, user_datastore,
|
security = Security(app, user_datastore,
|
||||||
register_form=ExtendedRegisterForm)
|
register_form=ExtendedRegisterForm)
|
||||||
@@ -106,22 +106,22 @@ The following is a list of all the available form overrides:
|
|||||||
Emails
|
Emails
|
||||||
------
|
------
|
||||||
|
|
||||||
Flask-Security is also packaged with a default tempalte for each email that it
|
Flask-Security is also packaged with a default template 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:
|
||||||
|
|
||||||
@@ -131,12 +131,33 @@ 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`_.
|
||||||
|
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)
|
||||||
|
|
||||||
|
.. _Celery: http://www.celeryproject.org/
|
||||||
|
|||||||
+19
-4
@@ -24,7 +24,7 @@ instance, you may assign roles such as `Admin`, `Editor`, `SuperUser`, or a
|
|||||||
combination of said roles to a user. Access control is based on the role name
|
combination of said roles to a user. Access control is based on the role name
|
||||||
and all roles should be uniquely named. This feature is implemented using the
|
and all roles should be uniquely named. This feature is implemented using the
|
||||||
`Flask-Principal`_ extension. If you'd like to implement more granular access
|
`Flask-Principal`_ extension. If you'd like to implement more granular access
|
||||||
control you can refer to the Flask-Princpal `documentation on this topic`_.
|
control, you can refer to the Flask-Principal `documentation on this topic`_.
|
||||||
|
|
||||||
|
|
||||||
Password Encryption
|
Password Encryption
|
||||||
@@ -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
|
||||||
@@ -88,7 +89,7 @@ User Registration
|
|||||||
|
|
||||||
Flask-Security comes packaged with a basic user registration view. This view is
|
Flask-Security comes packaged with a basic user registration view. This view is
|
||||||
very simple and new users need only supply an email address and their password.
|
very simple and new users need only supply an email address and their password.
|
||||||
This view can be overrided if your registration process requires more fields.
|
This view can be overridden if your registration process requires more fields.
|
||||||
|
|
||||||
|
|
||||||
Login Tracking
|
Login Tracking
|
||||||
@@ -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:
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@ Flask-Security assumes you'll be using libraries such as SQLAlchemy,
|
|||||||
MongoEngine or Peewee to define a data model that includes a `User` and
|
MongoEngine or Peewee to define a data model that includes a `User` and
|
||||||
`Role` model. The fields on your models must follow a particular convention
|
`Role` model. The fields on your models must follow a particular convention
|
||||||
depending on the functionality your app requires. Aside from this, you're
|
depending on the functionality your app requires. Aside from this, you're
|
||||||
free to add any additional fields to your model(s) if you want. At the bear
|
free to add any additional fields to your model(s) if you want. At the bare
|
||||||
minimum your `User` and `Role` model should include the following fields:
|
minimum your `User` and `Role` model should include the following fields:
|
||||||
|
|
||||||
**User**
|
**User**
|
||||||
@@ -32,7 +32,7 @@ Confirmable
|
|||||||
^^^^^^^^^^^
|
^^^^^^^^^^^
|
||||||
|
|
||||||
If you enable account confirmation by setting your application's
|
If you enable account confirmation by setting your application's
|
||||||
`SECURITY_CONFIRMABLE` configuration value to `True` your `User` model will
|
`SECURITY_CONFIRMABLE` configuration value to `True`, your `User` model will
|
||||||
require the following additional field:
|
require the following additional field:
|
||||||
|
|
||||||
* ``confirmed_at``
|
* ``confirmed_at``
|
||||||
@@ -41,7 +41,7 @@ Trackable
|
|||||||
^^^^^^^^^
|
^^^^^^^^^
|
||||||
|
|
||||||
If you enable user tracking by setting your application's `SECURITY_TRACKABLE`
|
If you enable user tracking by setting your application's `SECURITY_TRACKABLE`
|
||||||
configuration value to `True` your `User` model will require the following
|
configuration value to `True`, your `User` model will require the following
|
||||||
additional fields:
|
additional fields:
|
||||||
|
|
||||||
* ``last_login_at``
|
* ``last_login_at``
|
||||||
|
|||||||
+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):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security
|
flask_security
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security is a Flask extension that aims to add quick and simple
|
Flask-Security is a Flask extension that aims to add quick and simple
|
||||||
security via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
|
security via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
:license: MIT, see LICENSE for more details.
|
:license: MIT, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = '1.6.4'
|
|
||||||
|
|
||||||
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
|
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
|
||||||
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
|
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
|
||||||
from .decorators import auth_token_required, http_auth_required, \
|
from .decorators import auth_token_required, http_auth_required, \
|
||||||
login_required, roles_accepted, roles_required, auth_required
|
login_required, roles_accepted, roles_required, auth_required
|
||||||
from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
|
from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
|
||||||
ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
|
ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
|
||||||
from .signals import confirm_instructions_sent, password_reset, \
|
from .signals import confirm_instructions_sent, password_reset, \
|
||||||
reset_password_instructions_sent, user_confirmed, user_registered
|
reset_password_instructions_sent, user_confirmed, user_registered
|
||||||
from .utils import login_user, logout_user, url_for_security
|
from .utils import login_user, logout_user, url_for_security
|
||||||
|
|
||||||
|
__version__ = '1.7.5'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.changeable
|
flask_security.changeable
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security recoverable module
|
Flask-Security recoverable module
|
||||||
|
|
||||||
@@ -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,8 +28,9 @@ 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'):
|
||||||
'change_notice', user=user)
|
subject = config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE')
|
||||||
|
send_mail(subject, user.email, 'change_notice', user=user)
|
||||||
|
|
||||||
|
|
||||||
def change_user_password(user, password):
|
def change_user_password(user, password):
|
||||||
@@ -41,4 +42,5 @@ def change_user_password(user, password):
|
|||||||
user.password = encrypt_password(password)
|
user.password = encrypt_password(password)
|
||||||
_datastore.put(user)
|
_datastore.put(user)
|
||||||
send_password_changed_notice(user)
|
send_password_changed_notice(user)
|
||||||
password_changed.send(user, app=app._get_current_object())
|
password_changed.send(app._get_current_object(),
|
||||||
|
user=user._get_current_object())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.confirmable
|
flask_security.confirmable
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security confirmable module
|
Flask-Security confirmable module
|
||||||
|
|
||||||
@@ -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,\
|
||||||
@@ -27,7 +27,7 @@ _datastore = LocalProxy(lambda: _security.datastore)
|
|||||||
|
|
||||||
def generate_confirmation_link(user):
|
def generate_confirmation_link(user):
|
||||||
token = generate_confirmation_token(user)
|
token = generate_confirmation_token(user)
|
||||||
return url_for_security('confirm_email', token=token, _extenal=True), token
|
return url_for_security('confirm_email', token=token, _external=True), token
|
||||||
|
|
||||||
|
|
||||||
def send_confirmation_instructions(user):
|
def send_confirmation_instructions(user):
|
||||||
@@ -58,7 +58,9 @@ 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 is None)
|
||||||
|
|
||||||
|
|
||||||
def confirm_email_token_status(token):
|
def confirm_email_token_status(token):
|
||||||
@@ -77,6 +79,9 @@ def confirm_user(user):
|
|||||||
|
|
||||||
:param user: The user to confirm
|
:param user: The user to confirm
|
||||||
"""
|
"""
|
||||||
|
if user.confirmed_at is not None:
|
||||||
|
return False
|
||||||
user.confirmed_at = datetime.utcnow()
|
user.confirmed_at = datetime.utcnow()
|
||||||
_datastore.put(user)
|
_datastore.put(user)
|
||||||
user_confirmed.send(app._get_current_object(), user=user)
|
user_confirmed.send(app._get_current_object(), user=user)
|
||||||
|
return True
|
||||||
|
|||||||
+129
-67
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.core
|
flask_security.core
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security core module
|
Flask-Security core module
|
||||||
|
|
||||||
@@ -9,17 +9,18 @@
|
|||||||
:license: MIT, see LICENSE for more details.
|
:license: MIT, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app, render_template
|
||||||
from flask.ext.login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
|
from flask_login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
|
||||||
LoginManager, current_user
|
LoginManager, current_user
|
||||||
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \
|
from flask_principal import Principal, RoleNeed, UserNeed, Identity, \
|
||||||
identity_loaded
|
identity_loaded
|
||||||
from itsdangerous import URLSafeTimedSerializer
|
from itsdangerous import URLSafeTimedSerializer
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from werkzeug.datastructures import ImmutableList
|
from werkzeug.datastructures import ImmutableList
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
|
from werkzeug.security import safe_str_cmp
|
||||||
|
|
||||||
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 +56,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 +66,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',
|
||||||
@@ -71,6 +75,7 @@ _default_config = {
|
|||||||
'EMAIL_SENDER': 'no-reply@localhost',
|
'EMAIL_SENDER': 'no-reply@localhost',
|
||||||
'TOKEN_AUTHENTICATION_KEY': 'auth_token',
|
'TOKEN_AUTHENTICATION_KEY': 'auth_token',
|
||||||
'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token',
|
'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token',
|
||||||
|
'TOKEN_MAX_AGE': None,
|
||||||
'CONFIRM_SALT': 'confirm-salt',
|
'CONFIRM_SALT': 'confirm-salt',
|
||||||
'RESET_SALT': 'reset-salt',
|
'RESET_SALT': 'reset-salt',
|
||||||
'LOGIN_SALT': 'login-salt',
|
'LOGIN_SALT': 'login-salt',
|
||||||
@@ -83,53 +88,93 @@ _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'],
|
||||||
|
'PASSWORD_SCHEMES': [
|
||||||
|
'bcrypt',
|
||||||
|
'des_crypt',
|
||||||
|
'pbkdf2_sha256',
|
||||||
|
'pbkdf2_sha512',
|
||||||
|
'sha256_crypt',
|
||||||
|
'sha512_crypt',
|
||||||
|
# And always last one...
|
||||||
|
'plaintext'
|
||||||
|
],
|
||||||
|
'DEPRECATED_PASSWORD_SCHEMES': ['auto']
|
||||||
}
|
}
|
||||||
|
|
||||||
#: Default Flask-Security messages
|
#: Default Flask-Security messages
|
||||||
_default_messages = {
|
_default_messages = {
|
||||||
'UNAUTHORIZED': ('You do not have permission to view this resource.', 'error'),
|
'UNAUTHORIZED': (
|
||||||
'CONFIRM_REGISTRATION': ('Thank you. Confirmation instructions have been sent to %(email)s.', 'success'),
|
'You do not have permission to view this resource.', 'error'),
|
||||||
'EMAIL_CONFIRMED': ('Thank you. Your email has been confirmed.', 'success'),
|
'CONFIRM_REGISTRATION': (
|
||||||
'ALREADY_CONFIRMED': ('Your email has already been confirmed.', 'info'),
|
'Thank you. Confirmation instructions have been sent to %(email)s.', 'success'),
|
||||||
'INVALID_CONFIRMATION_TOKEN': ('Invalid confirmation token.', 'error'),
|
'EMAIL_CONFIRMED': (
|
||||||
'EMAIL_ALREADY_ASSOCIATED': ('%(email)s is already associated with an account.', 'error'),
|
'Thank you. Your email has been confirmed.', 'success'),
|
||||||
'PASSWORD_MISMATCH': ('Password does not match', 'error'),
|
'ALREADY_CONFIRMED': (
|
||||||
'RETYPE_PASSWORD_MISMATCH': ('Passwords do not match', 'error'),
|
'Your email has already been confirmed.', 'info'),
|
||||||
'INVALID_REDIRECT': ('Redirections outside the domain are forbidden', 'error'),
|
'INVALID_CONFIRMATION_TOKEN': (
|
||||||
'PASSWORD_RESET_REQUEST': ('Instructions to reset your password have been sent to %(email)s.', 'info'),
|
'Invalid confirmation token.', 'error'),
|
||||||
'PASSWORD_RESET_EXPIRED': ('You did not reset your password within %(within)s. New instructions have been sent to %(email)s.', 'error'),
|
'EMAIL_ALREADY_ASSOCIATED': (
|
||||||
'INVALID_RESET_PASSWORD_TOKEN': ('Invalid reset password token.', 'error'),
|
'%(email)s is already associated with an account.', 'error'),
|
||||||
'CONFIRMATION_REQUIRED': ('Email requires confirmation.', 'error'),
|
'PASSWORD_MISMATCH': (
|
||||||
'CONFIRMATION_REQUEST': ('Confirmation instructions have been sent to %(email)s.', 'info'),
|
'Password does not match', 'error'),
|
||||||
'CONFIRMATION_EXPIRED': ('You did not confirm your email within %(within)s. New instructions to confirm your email have been sent to %(email)s.', 'error'),
|
'RETYPE_PASSWORD_MISMATCH': (
|
||||||
'LOGIN_EXPIRED': ('You did not login within %(within)s. New instructions to login have been sent to %(email)s.', 'error'),
|
'Passwords do not match', 'error'),
|
||||||
'LOGIN_EMAIL_SENT': ('Instructions to login have been sent to %(email)s.', 'success'),
|
'INVALID_REDIRECT': (
|
||||||
'INVALID_LOGIN_TOKEN': ('Invalid login token.', 'error'),
|
'Redirections outside the domain are forbidden', 'error'),
|
||||||
'DISABLED_ACCOUNT': ('Account is disabled.', 'error'),
|
'PASSWORD_RESET_REQUEST': (
|
||||||
'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'),
|
'Instructions to reset your password have been sent to %(email)s.', 'info'),
|
||||||
'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'),
|
'PASSWORD_RESET_EXPIRED': (
|
||||||
'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'),
|
'You did not reset your password within %(within)s. New instructions have been sent '
|
||||||
'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'),
|
'to %(email)s.', 'error'),
|
||||||
'INVALID_PASSWORD': ('Invalid password', 'error'),
|
'INVALID_RESET_PASSWORD_TOKEN': (
|
||||||
'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'),
|
'Invalid reset password token.', 'error'),
|
||||||
'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'),
|
'CONFIRMATION_REQUIRED': (
|
||||||
'PASSWORD_CHANGE': ('You successfully changed your password.', 'success'),
|
'Email requires confirmation.', 'error'),
|
||||||
'LOGIN': ('Please log in to access this page.', 'info'),
|
'CONFIRMATION_REQUEST': (
|
||||||
'REFRESH': ('Please reauthenticate to access this page.', 'info'),
|
'Confirmation instructions have been sent to %(email)s.', 'info'),
|
||||||
|
'CONFIRMATION_EXPIRED': (
|
||||||
|
'You did not confirm your email within %(within)s. New instructions to confirm your email '
|
||||||
|
'have been sent to %(email)s.', 'error'),
|
||||||
|
'LOGIN_EXPIRED': (
|
||||||
|
'You did not login within %(within)s. New instructions to login have been sent to '
|
||||||
|
'%(email)s.', 'error'),
|
||||||
|
'LOGIN_EMAIL_SENT': (
|
||||||
|
'Instructions to login have been sent to %(email)s.', 'success'),
|
||||||
|
'INVALID_LOGIN_TOKEN': (
|
||||||
|
'Invalid login token.', 'error'),
|
||||||
|
'DISABLED_ACCOUNT': (
|
||||||
|
'Account is disabled.', 'error'),
|
||||||
|
'EMAIL_NOT_PROVIDED': (
|
||||||
|
'Email not provided', 'error'),
|
||||||
|
'INVALID_EMAIL_ADDRESS': (
|
||||||
|
'Invalid email address', 'error'),
|
||||||
|
'PASSWORD_NOT_PROVIDED': (
|
||||||
|
'Password not provided', 'error'),
|
||||||
|
'PASSWORD_NOT_SET': (
|
||||||
|
'No password is set for this user', 'error'),
|
||||||
|
'PASSWORD_INVALID_LENGTH': (
|
||||||
|
'Password must be at least 6 characters', 'error'),
|
||||||
|
'USER_DOES_NOT_EXIST': (
|
||||||
|
'Specified user does not exist', 'error'),
|
||||||
|
'INVALID_PASSWORD': (
|
||||||
|
'Invalid password', 'error'),
|
||||||
|
'PASSWORDLESS_LOGIN_SUCCESSFUL': (
|
||||||
|
'You have successfuly logged in.', 'success'),
|
||||||
|
'PASSWORD_RESET': (
|
||||||
|
'You successfully reset your password and you have been logged in automatically.',
|
||||||
|
'success'),
|
||||||
|
'PASSWORD_IS_THE_SAME': (
|
||||||
|
'Your new password must be different than your previous password.', 'error'),
|
||||||
|
'PASSWORD_CHANGE': (
|
||||||
|
'You successfully changed your password.', 'success'),
|
||||||
|
'LOGIN': (
|
||||||
|
'Please log in to access this page.', 'info'),
|
||||||
|
'REFRESH': (
|
||||||
|
'Please reauthenticate to access this page.', 'info'),
|
||||||
}
|
}
|
||||||
|
|
||||||
_allowed_password_hash_schemes = [
|
|
||||||
'bcrypt',
|
|
||||||
'des_crypt',
|
|
||||||
'pbkdf2_sha256',
|
|
||||||
'pbkdf2_sha512',
|
|
||||||
'sha256_crypt',
|
|
||||||
'sha512_crypt',
|
|
||||||
# And always last one...
|
|
||||||
'plaintext'
|
|
||||||
]
|
|
||||||
|
|
||||||
_default_forms = {
|
_default_forms = {
|
||||||
'login_form': LoginForm,
|
'login_form': LoginForm,
|
||||||
'confirm_register_form': ConfirmRegisterForm,
|
'confirm_register_form': ConfirmRegisterForm,
|
||||||
@@ -148,17 +193,17 @@ def _user_loader(user_id):
|
|||||||
|
|
||||||
def _token_loader(token):
|
def _token_loader(token):
|
||||||
try:
|
try:
|
||||||
data = _security.remember_token_serializer.loads(token)
|
data = _security.remember_token_serializer.loads(token, max_age=_security.token_max_age)
|
||||||
user = _security.datastore.find_user(id=data[0])
|
user = _security.datastore.find_user(id=data[0])
|
||||||
if user and md5(user.password) == data[1]:
|
if user and safe_str_cmp(md5(user.password), data[1]):
|
||||||
return user
|
return user
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return AnonymousUser()
|
return _security.login_manager.anonymous_user()
|
||||||
|
|
||||||
|
|
||||||
def _identity_loader():
|
def _identity_loader():
|
||||||
if not isinstance(current_user._get_current_object(), AnonymousUser):
|
if not isinstance(current_user._get_current_object(), AnonymousUserMixin):
|
||||||
identity = Identity(current_user.id)
|
identity = Identity(current_user.id)
|
||||||
return identity
|
return identity
|
||||||
|
|
||||||
@@ -173,9 +218,9 @@ def _on_identity_loaded(sender, identity):
|
|||||||
identity.user = current_user
|
identity.user = current_user
|
||||||
|
|
||||||
|
|
||||||
def _get_login_manager(app):
|
def _get_login_manager(app, anonymous_user):
|
||||||
lm = LoginManager()
|
lm = LoginManager()
|
||||||
lm.anonymous_user = AnonymousUser
|
lm.anonymous_user = anonymous_user or AnonymousUser
|
||||||
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
|
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
|
||||||
lm.user_loader(_user_loader)
|
lm.user_loader(_user_loader)
|
||||||
lm.token_loader(_token_loader)
|
lm.token_loader(_token_loader)
|
||||||
@@ -199,10 +244,12 @@ def _get_principal(app):
|
|||||||
|
|
||||||
def _get_pwd_context(app):
|
def _get_pwd_context(app):
|
||||||
pw_hash = cv('PASSWORD_HASH', app=app)
|
pw_hash = cv('PASSWORD_HASH', app=app)
|
||||||
if pw_hash not in _allowed_password_hash_schemes:
|
schemes = cv('PASSWORD_SCHEMES', app=app)
|
||||||
allowed = ', '.join(_allowed_password_hash_schemes[:-1]) + ' and ' + _allowed_password_hash_schemes[-1]
|
deprecated = cv('DEPRECATED_PASSWORD_SCHEMES', app=app)
|
||||||
|
if pw_hash not in schemes:
|
||||||
|
allowed = (', '.join(schemes[:-1]) + ' and ' + schemes[-1])
|
||||||
raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed))
|
raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed))
|
||||||
return CryptContext(schemes=_allowed_password_hash_schemes, default=pw_hash)
|
return CryptContext(schemes=schemes, default=pw_hash, deprecated=deprecated)
|
||||||
|
|
||||||
|
|
||||||
def _get_serializer(app, name):
|
def _get_serializer(app, name):
|
||||||
@@ -211,14 +258,14 @@ def _get_serializer(app, name):
|
|||||||
return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)
|
return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)
|
||||||
|
|
||||||
|
|
||||||
def _get_state(app, datastore, **kwargs):
|
def _get_state(app, datastore, anonymous_user=None, **kwargs):
|
||||||
for key, value in get_config(app).items():
|
for key, value in get_config(app).items():
|
||||||
kwargs[key.lower()] = value
|
kwargs[key.lower()] = value
|
||||||
|
|
||||||
kwargs.update(dict(
|
kwargs.update(dict(
|
||||||
app=app,
|
app=app,
|
||||||
datastore=datastore,
|
datastore=datastore,
|
||||||
login_manager=_get_login_manager(app),
|
login_manager=_get_login_manager(app, anonymous_user),
|
||||||
principal=_get_principal(app),
|
principal=_get_principal(app),
|
||||||
pwd_context=_get_pwd_context(app),
|
pwd_context=_get_pwd_context(app),
|
||||||
remember_token_serializer=_get_serializer(app, 'remember'),
|
remember_token_serializer=_get_serializer(app, 'remember'),
|
||||||
@@ -226,7 +273,8 @@ def _get_state(app, datastore, **kwargs):
|
|||||||
reset_serializer=_get_serializer(app, 'reset'),
|
reset_serializer=_get_serializer(app, 'reset'),
|
||||||
confirm_serializer=_get_serializer(app, 'confirm'),
|
confirm_serializer=_get_serializer(app, 'confirm'),
|
||||||
_context_processors={},
|
_context_processors={},
|
||||||
_send_mail_task=None
|
_send_mail_task=None,
|
||||||
|
_unauthorized_callback=None
|
||||||
))
|
))
|
||||||
|
|
||||||
for key, value in _default_forms.items():
|
for key, value in _default_forms.items():
|
||||||
@@ -242,17 +290,22 @@ def _context_processor():
|
|||||||
|
|
||||||
class RoleMixin(object):
|
class RoleMixin(object):
|
||||||
"""Mixin for `Role` model definitions"""
|
"""Mixin for `Role` model definitions"""
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (self.name == other or \
|
return (self.name == other or
|
||||||
self.name == getattr(other, 'name', None))
|
self.name == getattr(other, 'name', None))
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.name)
|
||||||
|
|
||||||
|
|
||||||
class UserMixin(BaseUserMixin):
|
class UserMixin(BaseUserMixin):
|
||||||
"""Mixin for `User` model definitions"""
|
"""Mixin for `User` model definitions"""
|
||||||
|
|
||||||
|
@property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
"""Returns `True` if the user is active."""
|
"""Returns `True` if the user is active."""
|
||||||
return self.active
|
return self.active
|
||||||
@@ -266,7 +319,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
|
||||||
@@ -294,7 +347,7 @@ class _SecurityState(object):
|
|||||||
fn not in group and group.append(fn)
|
fn not in group and group.append(fn)
|
||||||
|
|
||||||
def _run_ctx_processor(self, endpoint):
|
def _run_ctx_processor(self, endpoint):
|
||||||
rv, fns = {}, []
|
rv = {}
|
||||||
for g in [None, endpoint]:
|
for g in [None, endpoint]:
|
||||||
for fn in self._context_processors.setdefault(g, []):
|
for fn in self._context_processors.setdefault(g, []):
|
||||||
rv.update(fn())
|
rv.update(fn())
|
||||||
@@ -330,6 +383,9 @@ class _SecurityState(object):
|
|||||||
def send_mail_task(self, fn):
|
def send_mail_task(self, fn):
|
||||||
self._send_mail_task = fn
|
self._send_mail_task = fn
|
||||||
|
|
||||||
|
def unauthorized_handler(self, fn):
|
||||||
|
self._unauthorized_callback = fn
|
||||||
|
|
||||||
|
|
||||||
class Security(object):
|
class Security(object):
|
||||||
"""The :class:`Security` class initializes the Flask-Security extension.
|
"""The :class:`Security` class initializes the Flask-Security extension.
|
||||||
@@ -345,10 +401,11 @@ 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,
|
||||||
|
anonymous_user=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.
|
||||||
|
|
||||||
@@ -374,15 +431,20 @@ class Security(object):
|
|||||||
reset_password_form=reset_password_form,
|
reset_password_form=reset_password_form,
|
||||||
change_password_form=change_password_form,
|
change_password_form=change_password_form,
|
||||||
send_confirmation_form=send_confirmation_form,
|
send_confirmation_form=send_confirmation_form,
|
||||||
passwordless_login_form=passwordless_login_form)
|
passwordless_login_form=passwordless_login_form,
|
||||||
|
anonymous_user=anonymous_user)
|
||||||
|
|
||||||
if register_blueprint:
|
if register_blueprint:
|
||||||
app.register_blueprint(create_blueprint(state, __name__))
|
app.register_blueprint(create_blueprint(state, __name__))
|
||||||
app.context_processor(_context_processor)
|
app.context_processor(_context_processor)
|
||||||
|
|
||||||
|
state.render_template = self.render_template
|
||||||
app.extensions['security'] = state
|
app.extensions['security'] = state
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
def render_template(self, *args, **kwargs):
|
||||||
|
return render_template(*args, **kwargs)
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
return getattr(self._state, name, None)
|
return getattr(self._state, name, None)
|
||||||
|
|||||||
+83
-16
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.datastore
|
flask_security.datastore
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
This module contains an user datastore classes.
|
This module contains an user datastore classes.
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -90,7 +97,7 @@ class UserDatastore(object):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def add_role_to_user(self, user, role):
|
def add_role_to_user(self, user, role):
|
||||||
"""Adds a role tp a user
|
"""Adds a role to a user.
|
||||||
|
|
||||||
:param user: The user to manipulate
|
:param user: The user to manipulate
|
||||||
:param role: The role to add to the user
|
:param role: The role to add to the user
|
||||||
@@ -98,11 +105,12 @@ 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
|
||||||
|
|
||||||
def remove_role_from_user(self, user, role):
|
def remove_role_from_user(self, user, role):
|
||||||
"""Removes a role from a user
|
"""Removes a role from a user.
|
||||||
|
|
||||||
:param user: The user to manipulate
|
:param user: The user to manipulate
|
||||||
:param role: The role to remove from the user
|
:param role: The role to remove from the user
|
||||||
@@ -112,6 +120,7 @@ class UserDatastore(object):
|
|||||||
if role in user.roles:
|
if role in user.roles:
|
||||||
rv = True
|
rv = True
|
||||||
user.roles.remove(role)
|
user.roles.remove(role)
|
||||||
|
self.put(user)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def toggle_active(self, user):
|
def toggle_active(self, user):
|
||||||
@@ -147,19 +156,19 @@ class UserDatastore(object):
|
|||||||
|
|
||||||
def find_or_create_role(self, name, **kwargs):
|
def find_or_create_role(self, name, **kwargs):
|
||||||
"""Returns a role matching the given name or creates it with any
|
"""Returns a role matching the given name or creates it with any
|
||||||
additionally provided parameters
|
additionally provided parameters.
|
||||||
"""
|
"""
|
||||||
kwargs["name"] = name
|
kwargs["name"] = name
|
||||||
return self.find_role(name) or self.create_role(**kwargs)
|
return self.find_role(name) or self.create_role(**kwargs)
|
||||||
|
|
||||||
def create_user(self, **kwargs):
|
def create_user(self, **kwargs):
|
||||||
"""Creates and returns a new user from the given parameters."""
|
"""Creates and returns a new user from the given parameters."""
|
||||||
|
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):
|
||||||
"""Delete the specified user
|
"""Deletes the specified user.
|
||||||
|
|
||||||
:param user: The user to delete
|
:param user: The user to delete
|
||||||
"""
|
"""
|
||||||
@@ -174,6 +183,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 (TypeError, 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 +214,43 @@ 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: # pragma: no cover
|
||||||
|
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()
|
||||||
|
|
||||||
|
# TODO: Not sure why this was added but tests pass without it
|
||||||
|
# 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 +265,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 +285,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."""
|
||||||
@@ -232,10 +297,11 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
|||||||
user = self.put(user)
|
user = self.put(user)
|
||||||
for role in roles:
|
for role in roles:
|
||||||
self.add_role_to_user(user, role)
|
self.add_role_to_user(user, role)
|
||||||
|
self.put(user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def add_role_to_user(self, user, role):
|
def add_role_to_user(self, user, role):
|
||||||
"""Adds a role tp a user
|
"""Adds a role to a user.
|
||||||
|
|
||||||
:param user: The user to manipulate
|
:param user: The user to manipulate
|
||||||
:param role: The role to add to the user
|
:param role: The role to add to the user
|
||||||
@@ -246,11 +312,11 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
|||||||
if result.count():
|
if result.count():
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
self.UserRole.create(user=user.id, role=role.id)
|
self.put(self.UserRole.create(user=user.id, role=role.id))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def remove_role_from_user(self, user, role):
|
def remove_role_from_user(self, user, role):
|
||||||
"""Removes a role from a user
|
"""Removes a role from a user.
|
||||||
|
|
||||||
:param user: The user to manipulate
|
:param user: The user to manipulate
|
||||||
:param role: The role to remove from the user
|
:param role: The role to remove from the user
|
||||||
@@ -259,8 +325,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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.decorators
|
flask_security.decorators
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security decorators module
|
Flask-Security decorators module
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ from collections import namedtuple
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import current_app, Response, request, redirect, _request_ctx_stack
|
from flask import current_app, Response, request, redirect, _request_ctx_stack
|
||||||
from flask.ext.login import current_user, login_required
|
from flask_login import current_user, login_required # pragma: no flakes
|
||||||
from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed
|
from flask_principal import RoleNeed, Permission, Identity, identity_changed
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
@@ -51,23 +51,20 @@ 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)
|
if not isinstance(request.json, list):
|
||||||
serializer = _security.remember_token_serializer
|
token = request.json.get(args_key, token)
|
||||||
|
|
||||||
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)
|
||||||
@@ -94,9 +91,12 @@ def http_auth_required(realm):
|
|||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if _check_http_auth():
|
if _check_http_auth():
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
r = _security.default_http_auth_realm if callable(realm) else realm
|
if _security._unauthorized_callback:
|
||||||
h = {'WWW-Authenticate': 'Basic realm="%s"' % r}
|
return _security._unauthorized_callback()
|
||||||
return _get_unauthorized_response(headers=h)
|
else:
|
||||||
|
r = _security.default_http_auth_realm if callable(realm) else realm
|
||||||
|
h = {'WWW-Authenticate': 'Basic realm="%s"' % r}
|
||||||
|
return _get_unauthorized_response(headers=h)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
if callable(realm):
|
if callable(realm):
|
||||||
@@ -116,7 +116,10 @@ def auth_token_required(fn):
|
|||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
if _check_token():
|
if _check_token():
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
return _get_unauthorized_response()
|
if _security._unauthorized_callback:
|
||||||
|
return _security._unauthorized_callback()
|
||||||
|
else:
|
||||||
|
return _get_unauthorized_response()
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
@@ -135,17 +138,24 @@ def auth_required(*auth_methods):
|
|||||||
login_mechanisms = {
|
login_mechanisms = {
|
||||||
'token': lambda: _check_token(),
|
'token': lambda: _check_token(),
|
||||||
'basic': lambda: _check_http_auth(),
|
'basic': lambda: _check_http_auth(),
|
||||||
'session': lambda: current_user.is_authenticated()
|
'session': lambda: current_user.is_authenticated
|
||||||
}
|
}
|
||||||
|
|
||||||
def wrapper(fn):
|
def wrapper(fn):
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args, **kwargs):
|
||||||
mechanisms = [login_mechanisms.get(method) for method in auth_methods]
|
h = {}
|
||||||
for mechanism in mechanisms:
|
mechanisms = [(method, login_mechanisms.get(method)) for method in auth_methods]
|
||||||
|
for method, mechanism in mechanisms:
|
||||||
if mechanism and mechanism():
|
if mechanism and mechanism():
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
return _get_unauthorized_response()
|
elif method == 'basic':
|
||||||
|
r = _security.default_http_auth_realm
|
||||||
|
h['WWW-Authenticate'] = 'Basic realm="%s"' % r
|
||||||
|
if _security._unauthorized_callback:
|
||||||
|
return _security._unauthorized_callback()
|
||||||
|
else:
|
||||||
|
return _get_unauthorized_response(headers=h)
|
||||||
return decorated_view
|
return decorated_view
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
@@ -170,7 +180,10 @@ def roles_required(*roles):
|
|||||||
perms = [Permission(RoleNeed(role)) for role in roles]
|
perms = [Permission(RoleNeed(role)) for role in roles]
|
||||||
for perm in perms:
|
for perm in perms:
|
||||||
if not perm.can():
|
if not perm.can():
|
||||||
return _get_unauthorized_view()
|
if _security._unauthorized_callback:
|
||||||
|
return _security._unauthorized_callback()
|
||||||
|
else:
|
||||||
|
return _get_unauthorized_view()
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
return decorated_view
|
return decorated_view
|
||||||
return wrapper
|
return wrapper
|
||||||
@@ -196,7 +209,10 @@ def roles_accepted(*roles):
|
|||||||
perm = Permission(*[RoleNeed(role) for role in roles])
|
perm = Permission(*[RoleNeed(role) for role in roles])
|
||||||
if perm.can():
|
if perm.can():
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
return _get_unauthorized_view()
|
if _security._unauthorized_callback:
|
||||||
|
return _security._unauthorized_callback()
|
||||||
|
else:
|
||||||
|
return _get_unauthorized_view()
|
||||||
return decorated_view
|
return decorated_view
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
@@ -204,7 +220,7 @@ def roles_accepted(*roles):
|
|||||||
def anonymous_user_required(f):
|
def anonymous_user_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if current_user.is_authenticated():
|
if current_user.is_authenticated:
|
||||||
return redirect(utils.get_url(_security.post_login_view))
|
return redirect(utils.get_url(_security.post_login_view))
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
+61
-46
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.forms
|
flask_security.forms
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security forms module
|
Flask-Security forms module
|
||||||
|
|
||||||
@@ -10,18 +10,16 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import urlparse
|
|
||||||
|
|
||||||
import flask_wtf as wtf
|
from flask import request, current_app, flash
|
||||||
|
from flask_wtf import Form as BaseForm
|
||||||
from flask import request, current_app
|
from wtforms import StringField, PasswordField, validators, \
|
||||||
from flask_wtf import Form as BaseForm, TextField, PasswordField, \
|
|
||||||
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, validate_redirect_url
|
||||||
|
|
||||||
# Convenient reference
|
# Convenient reference
|
||||||
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
|
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
|
||||||
@@ -50,25 +48,26 @@ class ValidatorMixin(object):
|
|||||||
return super(ValidatorMixin, self).__call__(form, field)
|
return super(ValidatorMixin, self).__call__(form, field)
|
||||||
|
|
||||||
|
|
||||||
class EqualTo(ValidatorMixin, wtf.EqualTo):
|
class EqualTo(ValidatorMixin, validators.EqualTo):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Required(ValidatorMixin, wtf.Required):
|
class Required(ValidatorMixin, validators.Required):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Email(ValidatorMixin, wtf.Email):
|
class Email(ValidatorMixin, validators.Email):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Length(ValidatorMixin, wtf.Length):
|
class Length(ValidatorMixin, validators.Length):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
email_required = Required(message='EMAIL_NOT_PROVIDED')
|
email_required = Required(message='EMAIL_NOT_PROVIDED')
|
||||||
email_validator = Email(message='INVALID_EMAIL_ADDRESS')
|
email_validator = Email(message='INVALID_EMAIL_ADDRESS')
|
||||||
password_required = Required(message='PASSWORD_NOT_PROVIDED')
|
password_required = Required(message='PASSWORD_NOT_PROVIDED')
|
||||||
|
password_length = Length(min=6, max=128, message='PASSWORD_INVALID_LENGTH')
|
||||||
|
|
||||||
|
|
||||||
def get_form_field_label(key):
|
def get_form_field_label(key):
|
||||||
@@ -76,13 +75,13 @@ def get_form_field_label(key):
|
|||||||
|
|
||||||
|
|
||||||
def unique_user_email(form, field):
|
def unique_user_email(form, field):
|
||||||
if _datastore.find_user(email=field.data) is not None:
|
if _datastore.get_user(field.data) is not None:
|
||||||
msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0]
|
msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0]
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
|
|
||||||
|
|
||||||
def valid_user_email(form, field):
|
def valid_user_email(form, field):
|
||||||
form.user = _datastore.find_user(email=field.data)
|
form.user = _datastore.get_user(field.data)
|
||||||
if form.user is None:
|
if form.user is None:
|
||||||
raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0])
|
raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0])
|
||||||
|
|
||||||
@@ -95,35 +94,33 @@ class Form(BaseForm):
|
|||||||
|
|
||||||
|
|
||||||
class EmailFormMixin():
|
class EmailFormMixin():
|
||||||
email = TextField(get_form_field_label('email'),
|
email = StringField(
|
||||||
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 = StringField(
|
||||||
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 = StringField(
|
||||||
validators=[email_required,
|
get_form_field_label('email'),
|
||||||
email_validator,
|
validators=[email_required, email_validator, unique_user_email])
|
||||||
unique_user_email])
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordFormMixin():
|
class PasswordFormMixin():
|
||||||
password = PasswordField(get_form_field_label('password'),
|
password = PasswordField(
|
||||||
validators=[password_required])
|
get_form_field_label('password'), validators=[password_required])
|
||||||
|
|
||||||
|
|
||||||
class NewPasswordFormMixin():
|
class NewPasswordFormMixin():
|
||||||
password = PasswordField(get_form_field_label('password'),
|
password = PasswordField(
|
||||||
validators=[password_required,
|
get_form_field_label('password'),
|
||||||
Length(min=6, max=128)])
|
validators=[password_required, password_length])
|
||||||
|
|
||||||
|
|
||||||
class PasswordConfirmFormMixin():
|
class PasswordConfirmFormMixin():
|
||||||
@@ -136,10 +133,9 @@ 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 and not validate_redirect_url(field.data):
|
||||||
url_base = urlparse.urlsplit(request.host_url)
|
|
||||||
if url_next.netloc and url_next.netloc != url_base.netloc:
|
|
||||||
field.data = ''
|
field.data = ''
|
||||||
|
flash(*get_message('INVALID_REDIRECT'))
|
||||||
raise ValidationError(get_message('INVALID_REDIRECT')[0])
|
raise ValidationError(get_message('INVALID_REDIRECT')[0])
|
||||||
|
|
||||||
|
|
||||||
@@ -179,6 +175,14 @@ class ForgotPasswordForm(Form, UserEmailFormMixin):
|
|||||||
|
|
||||||
submit = SubmitField(get_form_field_label('recover_password'))
|
submit = SubmitField(get_form_field_label('recover_password'))
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
if not super(ForgotPasswordForm, self).validate():
|
||||||
|
return False
|
||||||
|
if requires_confirmation(self.user):
|
||||||
|
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class PasswordlessLoginForm(Form, UserEmailFormMixin):
|
class PasswordlessLoginForm(Form, UserEmailFormMixin):
|
||||||
"""The passwordless login form"""
|
"""The passwordless login form"""
|
||||||
@@ -191,7 +195,7 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
if not super(PasswordlessLoginForm, self).validate():
|
if not super(PasswordlessLoginForm, self).validate():
|
||||||
return False
|
return False
|
||||||
if not self.user.is_active():
|
if not self.user.is_active:
|
||||||
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
|
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@@ -200,13 +204,16 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
|
|||||||
class LoginForm(Form, NextFormMixin):
|
class LoginForm(Form, NextFormMixin):
|
||||||
"""The default login form"""
|
"""The default login form"""
|
||||||
|
|
||||||
email = TextField(get_form_field_label('email'))
|
email = StringField(get_form_field_label('email'))
|
||||||
password = PasswordField(get_form_field_label('password'))
|
password = PasswordField(get_form_field_label('password'))
|
||||||
remember = BooleanField(get_form_field_label('remember_me'))
|
remember = BooleanField(get_form_field_label('remember_me'))
|
||||||
submit = SubmitField(get_form_field_label('login'))
|
submit = SubmitField(get_form_field_label('login'))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(LoginForm, self).__init__(*args, **kwargs)
|
super(LoginForm, self).__init__(*args, **kwargs)
|
||||||
|
if not self.next.data:
|
||||||
|
self.next.data = request.args.get('next', '')
|
||||||
|
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,18 +227,21 @@ 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
|
||||||
if requires_confirmation(self.user):
|
if requires_confirmation(self.user):
|
||||||
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
|
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
|
||||||
return False
|
return False
|
||||||
if not self.user.is_active():
|
if not self.user.is_active:
|
||||||
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
|
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@@ -242,8 +252,12 @@ class ConfirmRegisterForm(Form, RegisterFormMixin,
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
|
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin,
|
||||||
pass
|
NextFormMixin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(RegisterForm, self).__init__(*args, **kwargs)
|
||||||
|
if not self.next.data:
|
||||||
|
self.next.data = request.args.get('next', '')
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
|
class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
|
||||||
@@ -255,11 +269,12 @@ class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
|
|||||||
class ChangePasswordForm(Form, PasswordFormMixin):
|
class ChangePasswordForm(Form, PasswordFormMixin):
|
||||||
"""The default change password form"""
|
"""The default change password form"""
|
||||||
|
|
||||||
new_password = PasswordField(get_form_field_label('new_password'),
|
new_password = PasswordField(
|
||||||
validators=[password_required,
|
get_form_field_label('new_password'),
|
||||||
Length(min=6, max=128)])
|
validators=[password_required, password_length])
|
||||||
|
|
||||||
new_password_confirm = PasswordField(get_form_field_label('retype_password'),
|
new_password_confirm = PasswordField(
|
||||||
|
get_form_field_label('retype_password'),
|
||||||
validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
|
validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
|
||||||
|
|
||||||
submit = SubmitField(get_form_field_label('change_password'))
|
submit = SubmitField(get_form_field_label('change_password'))
|
||||||
@@ -268,10 +283,10 @@ class ChangePasswordForm(Form, PasswordFormMixin):
|
|||||||
if not super(ChangePasswordForm, self).validate():
|
if not super(ChangePasswordForm, self).validate():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.password.data.strip() == '':
|
|
||||||
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
|
|
||||||
return False
|
|
||||||
if not verify_and_update_password(self.password.data, current_user):
|
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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.passwordless
|
flask_security.passwordless
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security passwordless module
|
Flask-Security passwordless module
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -35,8 +35,7 @@ def send_login_instructions(user):
|
|||||||
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
|
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
|
||||||
'login_instructions', user=user, login_link=login_link)
|
'login_instructions', user=user, login_link=login_link)
|
||||||
|
|
||||||
login_instructions_sent.send(app._get_current_object(),
|
login_instructions_sent.send(app._get_current_object(), user=user, login_token=token)
|
||||||
user=user, login_token=token)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_login_token(user):
|
def generate_login_token(user):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.recoverable
|
flask_security.recoverable
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security recoverable module
|
Flask-Security recoverable module
|
||||||
|
|
||||||
@@ -9,8 +9,9 @@
|
|||||||
: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 werkzeug.security import safe_str_cmp
|
||||||
|
|
||||||
from .signals import password_reset, reset_password_instructions_sent
|
from .signals import password_reset, reset_password_instructions_sent
|
||||||
from .utils import send_mail, md5, encrypt_password, url_for_security, \
|
from .utils import send_mail, md5, encrypt_password, url_for_security, \
|
||||||
@@ -35,8 +36,7 @@ def send_reset_password_instructions(user):
|
|||||||
'reset_instructions',
|
'reset_instructions',
|
||||||
user=user, reset_link=reset_link)
|
user=user, reset_link=reset_link)
|
||||||
|
|
||||||
reset_password_instructions_sent.send(app._get_current_object(),
|
reset_password_instructions_sent.send(app._get_current_object(), user=user, token=token)
|
||||||
user=user, token=token)
|
|
||||||
|
|
||||||
|
|
||||||
def send_password_reset_notice(user):
|
def send_password_reset_notice(user):
|
||||||
@@ -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):
|
||||||
@@ -53,7 +54,8 @@ def generate_reset_password_token(user):
|
|||||||
|
|
||||||
:param user: The user to work with
|
:param user: The user to work with
|
||||||
"""
|
"""
|
||||||
data = [str(user.id), md5(user.password)]
|
password_hash = md5(user.password) if user.password else None
|
||||||
|
data = [str(user.id), password_hash]
|
||||||
return _security.reset_serializer.dumps(data)
|
return _security.reset_serializer.dumps(data)
|
||||||
|
|
||||||
|
|
||||||
@@ -61,11 +63,20 @@ def reset_password_token_status(token):
|
|||||||
"""Returns the expired status, invalid status, and user of a password reset
|
"""Returns the expired status, invalid status, and user of a password reset
|
||||||
token. For example::
|
token. For example::
|
||||||
|
|
||||||
expired, invalid, user = reset_password_token_status('...')
|
expired, invalid, user, data = reset_password_token_status('...')
|
||||||
|
|
||||||
:param token: The password reset token
|
:param token: The password reset token
|
||||||
"""
|
"""
|
||||||
return get_token_status(token, 'reset', 'RESET_PASSWORD')
|
expired, invalid, user, data = get_token_status(token, 'reset', 'RESET_PASSWORD',
|
||||||
|
return_data=True)
|
||||||
|
if not invalid:
|
||||||
|
if user.password:
|
||||||
|
password_hash = md5(user.password)
|
||||||
|
if not safe_str_cmp(password_hash, data[1]):
|
||||||
|
invalid = True
|
||||||
|
|
||||||
|
return expired, invalid, user
|
||||||
|
|
||||||
|
|
||||||
def update_password(user, password):
|
def update_password(user, password):
|
||||||
"""Update the specified user's password
|
"""Update the specified user's password
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.registerable
|
flask_security.registerable
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security registerable module
|
Flask-Security registerable module
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
+18
-16
@@ -1,13 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.script
|
flask_security.script
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security script module
|
Flask-Security script module
|
||||||
|
|
||||||
:copyright: (c) 2012 by Matt Wright.
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
:license: MIT, see LICENSE for more details.
|
:license: MIT, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -16,7 +18,7 @@ except ImportError:
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask.ext.script import Command, Option
|
from flask_script import Command, Option
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
from .utils import encrypt_password
|
from .utils import encrypt_password
|
||||||
@@ -26,7 +28,7 @@ _datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
|
|||||||
|
|
||||||
|
|
||||||
def pprint(obj):
|
def pprint(obj):
|
||||||
print json.dumps(obj, sort_keys=True, indent=4)
|
print(json.dumps(obj, sort_keys=True, indent=4))
|
||||||
|
|
||||||
|
|
||||||
def commit(fn):
|
def commit(fn):
|
||||||
@@ -40,9 +42,9 @@ class CreateUserCommand(Command):
|
|||||||
"""Create a user"""
|
"""Create a user"""
|
||||||
|
|
||||||
option_list = (
|
option_list = (
|
||||||
Option('-e', '--email', dest='email', default=None),
|
Option('-e', '--email', dest='email', default=None),
|
||||||
Option('-p', '--password', dest='password', default=None),
|
Option('-p', '--password', dest='password', default=None),
|
||||||
Option('-a', '--active', dest='active', default=''),
|
Option('-a', '--active', dest='active', default=''),
|
||||||
)
|
)
|
||||||
|
|
||||||
@commit
|
@commit
|
||||||
@@ -59,11 +61,11 @@ class CreateUserCommand(Command):
|
|||||||
if form.validate():
|
if form.validate():
|
||||||
kwargs['password'] = encrypt_password(kwargs['password'])
|
kwargs['password'] = encrypt_password(kwargs['password'])
|
||||||
_datastore.create_user(**kwargs)
|
_datastore.create_user(**kwargs)
|
||||||
print 'User created successfully.'
|
print('User created successfully.')
|
||||||
kwargs['password'] = '****'
|
kwargs['password'] = '****'
|
||||||
pprint(kwargs)
|
pprint(kwargs)
|
||||||
else:
|
else:
|
||||||
print 'Error creating user'
|
print('Error creating user')
|
||||||
pprint(form.errors)
|
pprint(form.errors)
|
||||||
|
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ class CreateRoleCommand(Command):
|
|||||||
@commit
|
@commit
|
||||||
def run(self, **kwargs):
|
def run(self, **kwargs):
|
||||||
_datastore.create_role(**kwargs)
|
_datastore.create_role(**kwargs)
|
||||||
print 'Role "%(name)s" created successfully.' % kwargs
|
print('Role "%(name)s" created successfully.' % kwargs)
|
||||||
|
|
||||||
|
|
||||||
class _RoleCommand(Command):
|
class _RoleCommand(Command):
|
||||||
@@ -94,16 +96,16 @@ class AddRoleCommand(_RoleCommand):
|
|||||||
@commit
|
@commit
|
||||||
def run(self, user_identifier, role_name):
|
def run(self, user_identifier, role_name):
|
||||||
_datastore.add_role_to_user(user_identifier, role_name)
|
_datastore.add_role_to_user(user_identifier, role_name)
|
||||||
print "Role '%s' added to user '%s' successfully" % (role_name, user_identifier)
|
print("Role '%s' added to user '%s' successfully" % (role_name, user_identifier))
|
||||||
|
|
||||||
|
|
||||||
class RemoveRoleCommand(_RoleCommand):
|
class RemoveRoleCommand(_RoleCommand):
|
||||||
"""Add a role to a user"""
|
"""Remove a role from a user"""
|
||||||
|
|
||||||
@commit
|
@commit
|
||||||
def run(self, user_identifier, role_name):
|
def run(self, user_identifier, role_name):
|
||||||
_datastore.remove_role_from_user(user_identifier, role_name)
|
_datastore.remove_role_from_user(user_identifier, role_name)
|
||||||
print "Role '%s' removed from user '%s' successfully" % (role_name, user_identifier)
|
print("Role '%s' removed from user '%s' successfully" % (role_name, user_identifier))
|
||||||
|
|
||||||
|
|
||||||
class _ToggleActiveCommand(Command):
|
class _ToggleActiveCommand(Command):
|
||||||
@@ -113,18 +115,18 @@ class _ToggleActiveCommand(Command):
|
|||||||
|
|
||||||
|
|
||||||
class DeactivateUserCommand(_ToggleActiveCommand):
|
class DeactivateUserCommand(_ToggleActiveCommand):
|
||||||
"""Deactive a user"""
|
"""Deactivate a user"""
|
||||||
|
|
||||||
@commit
|
@commit
|
||||||
def run(self, user_identifier):
|
def run(self, user_identifier):
|
||||||
_datastore.deactivate_user(user_identifier)
|
_datastore.deactivate_user(user_identifier)
|
||||||
print "User '%s' has been deactivated" % user_identifier
|
print("User '%s' has been deactivated" % user_identifier)
|
||||||
|
|
||||||
|
|
||||||
class ActivateUserCommand(_ToggleActiveCommand):
|
class ActivateUserCommand(_ToggleActiveCommand):
|
||||||
"""Deactive a user"""
|
"""Activate a user"""
|
||||||
|
|
||||||
@commit
|
@commit
|
||||||
def run(self, user_identifier):
|
def run(self, user_identifier):
|
||||||
_datastore.activate_user(user_identifier)
|
_datastore.activate_user(user_identifier)
|
||||||
print "User '%s' has been activated" % user_identifier
|
print("User '%s' has been activated" % user_identifier)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.signals
|
flask_security.signals
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security signals module
|
Flask-Security signals module
|
||||||
|
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% if security.registerable or security.recoverable or security.confirmabled %}
|
{% if security.registerable or security.recoverable or security.confirmable %}
|
||||||
<h2>Menu</h2>
|
<h2>Menu</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ url_for_security('login') }}">Login</a></li>
|
<li><a href="{{ url_for_security('login') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">Login</a></li>
|
||||||
{% if security.registerable %}
|
{% if security.registerable %}
|
||||||
<li><a href="{{ url_for_security('register') }}">Register</a><br/></li>
|
<li><a href="{{ url_for_security('register') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">Register</a><br/></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if security.recoverable %}
|
{% if security.recoverable %}
|
||||||
<li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li>
|
<li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li>
|
||||||
|
|||||||
+144
-95
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.utils
|
flask_security.utils
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security utils module
|
Flask-Security utils module
|
||||||
|
|
||||||
@@ -10,25 +10,26 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import blinker
|
|
||||||
import functools
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urlparse import urlsplit
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from flask import url_for, flash, current_app, request, session, render_template
|
from flask import url_for, flash, current_app, request, session, render_template
|
||||||
from flask.ext.login import login_user as _login_user, \
|
from flask_login import login_user as _login_user, logout_user as _logout_user
|
||||||
logout_user as _logout_user
|
from flask_mail import Message
|
||||||
from flask.ext.mail import Message
|
from flask_principal import Identity, AnonymousIdentity, identity_changed
|
||||||
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
|
|
||||||
from itsdangerous import BadSignature, SignatureExpired
|
from itsdangerous import BadSignature, SignatureExpired
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
from .signals import user_registered, user_confirmed, \
|
from .signals import user_registered, login_instructions_sent, reset_password_instructions_sent
|
||||||
confirm_instructions_sent, login_instructions_sent, \
|
|
||||||
password_reset, password_changed, reset_password_instructions_sent
|
|
||||||
|
|
||||||
# Convenient references
|
# Convenient references
|
||||||
_security = LocalProxy(lambda: current_app.extensions['security'])
|
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||||
@@ -37,19 +38,36 @@ _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: # pragma: no cover
|
||||||
|
string_types = str, # pragma: no flakes
|
||||||
|
text_type = str # pragma: no flakes
|
||||||
|
else: # pragma: no cover
|
||||||
|
string_types = basestring, # pragma: no flakes
|
||||||
|
text_type = unicode # pragma: no flakes
|
||||||
|
|
||||||
|
|
||||||
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')
|
||||||
|
|
||||||
if not _login_user(user, remember):
|
if not _login_user(user, remember): # pragma: no cover
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if _security.trackable:
|
if _security.trackable:
|
||||||
|
if 'X-Forwarded-For' in request.headers:
|
||||||
|
remote_addr = request.headers.getlist("X-Forwarded-For")[0].rpartition(' ')[-1]
|
||||||
|
else:
|
||||||
|
remote_addr = request.remote_addr or 'untrackable'
|
||||||
|
|
||||||
old_current_login, new_current_login = user.current_login_at, datetime.utcnow()
|
old_current_login, new_current_login = user.current_login_at, datetime.utcnow()
|
||||||
remote_addr = request.remote_addr or 'untrackable'
|
|
||||||
old_current_ip, new_current_ip = user.current_login_ip, remote_addr
|
old_current_ip, new_current_ip = user.current_login_ip, remote_addr
|
||||||
|
|
||||||
user.last_login_at = old_current_login or new_current_login
|
user.last_login_at = old_current_login or new_current_login
|
||||||
@@ -66,6 +84,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,36 +94,75 @@ 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``.
|
||||||
|
|
||||||
if _security.password_salt is None:
|
:param password: The password to sign
|
||||||
raise RuntimeError('The configuration value `SECURITY_PASSWORD_SALT` '
|
"""
|
||||||
'must not be None when the value of `SECURITY_PASSWORD_HASH` is '
|
salt = _security.password_salt
|
||||||
|
|
||||||
|
if salt is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
'The configuration value `SECURITY_PASSWORD_SALT` must '
|
||||||
|
'not be None when the value of `SECURITY_PASSWORD_HASH` is '
|
||||||
'set to "%s"' % _security.password_hash)
|
'set to "%s"' % _security.password_hash)
|
||||||
|
|
||||||
h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512)
|
h = hmac.new(encode_string(salt), encode_string(password), 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 from your database)
|
||||||
|
"""
|
||||||
|
if _security.password_hash != 'plaintext':
|
||||||
|
password = get_hmac(password)
|
||||||
|
|
||||||
|
return _pwd_context.verify(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 _pwd_context.identify(user.password) != '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 = encrypt_password(password)
|
||||||
_datastore.put(user)
|
_datastore.put(user)
|
||||||
return verified
|
return verified
|
||||||
|
|
||||||
|
|
||||||
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 password to encrypt
|
||||||
|
"""
|
||||||
|
if _security.password_hash == 'plaintext':
|
||||||
|
return password
|
||||||
|
signed = get_hmac(password).decode('ascii')
|
||||||
|
return _pwd_context.encrypt(signed)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_string(string):
|
||||||
|
"""Encodes a string to bytes, if it isn't already.
|
||||||
|
|
||||||
|
:param string: The string to encode"""
|
||||||
|
|
||||||
|
if isinstance(string, text_type):
|
||||||
|
string = string.encode('utf-8')
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
def md5(data):
|
def md5(data):
|
||||||
return hashlib.md5(data).hexdigest()
|
return hashlib.md5(encode_string(data)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def do_flash(message, category=None):
|
def do_flash(message, category=None):
|
||||||
@@ -129,6 +188,14 @@ def get_url(endpoint_or_url):
|
|||||||
return endpoint_or_url
|
return endpoint_or_url
|
||||||
|
|
||||||
|
|
||||||
|
def slash_url_suffix(url, suffix):
|
||||||
|
"""Adds a slash either to the beginning or the end of a suffix
|
||||||
|
(which is to be appended to a URL), depending on whether or not
|
||||||
|
the URL ends with a slash."""
|
||||||
|
|
||||||
|
return url.endswith('/') and ('%s/' % suffix) or ('/%s' % suffix)
|
||||||
|
|
||||||
|
|
||||||
def get_security_endpoint_name(endpoint):
|
def get_security_endpoint_name(endpoint):
|
||||||
return '%s.%s' % (_security.blueprint_name, endpoint)
|
return '%s.%s' % (_security.blueprint_name, endpoint)
|
||||||
|
|
||||||
@@ -148,18 +215,35 @@ def url_for_security(endpoint, **values):
|
|||||||
return url_for(endpoint, **values)
|
return url_for(endpoint, **values)
|
||||||
|
|
||||||
|
|
||||||
def get_post_action_redirect(config_key):
|
def validate_redirect_url(url):
|
||||||
return (get_url(request.args.get('next')) or
|
if url is None or url.strip() == '':
|
||||||
get_url(request.form.get('next')) or
|
return False
|
||||||
find_redirect(config_key))
|
url_next = urlsplit(url)
|
||||||
|
url_base = urlsplit(request.host_url)
|
||||||
|
if (url_next.netloc or url_next.scheme) and url_next.netloc != url_base.netloc:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_post_login_redirect():
|
def get_post_action_redirect(config_key, declared=None):
|
||||||
return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW')
|
urls = [
|
||||||
|
get_url(request.args.get('next')),
|
||||||
|
get_url(request.form.get('next')),
|
||||||
|
find_redirect(config_key)
|
||||||
|
]
|
||||||
|
if declared:
|
||||||
|
urls.insert(0, declared)
|
||||||
|
for url in urls:
|
||||||
|
if validate_redirect_url(url):
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
def get_post_register_redirect():
|
def get_post_login_redirect(declared=None):
|
||||||
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW')
|
return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW', declared)
|
||||||
|
|
||||||
|
|
||||||
|
def get_post_register_redirect(declared=None):
|
||||||
|
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW', declared)
|
||||||
|
|
||||||
|
|
||||||
def find_redirect(key):
|
def find_redirect(key):
|
||||||
@@ -205,10 +289,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):
|
||||||
@@ -259,7 +341,15 @@ def send_mail(subject, recipient, template, **context):
|
|||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
def get_token_status(token, serializer, max_age=None):
|
def get_token_status(token, serializer, max_age=None, return_data=False):
|
||||||
|
"""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
|
||||||
@@ -270,14 +360,28 @@ def get_token_status(token, serializer, max_age=None):
|
|||||||
except SignatureExpired:
|
except SignatureExpired:
|
||||||
d, data = serializer.loads_unsafe(token)
|
d, data = serializer.loads_unsafe(token)
|
||||||
expired = True
|
expired = True
|
||||||
except BadSignature:
|
except (BadSignature, TypeError, ValueError):
|
||||||
invalid = True
|
invalid = True
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
user = _datastore.find_user(id=data[0])
|
user = _datastore.find_user(id=data[0])
|
||||||
|
|
||||||
expired = expired and (user is not None)
|
expired = expired and (user is not None)
|
||||||
return expired, invalid, user
|
|
||||||
|
if return_data:
|
||||||
|
return expired, invalid, user, data
|
||||||
|
else:
|
||||||
|
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
|
||||||
@@ -333,58 +437,3 @@ def capture_reset_password_requests(reset_password_sent_at=None):
|
|||||||
yield reset_requests
|
yield reset_requests
|
||||||
finally:
|
finally:
|
||||||
reset_password_instructions_sent.disconnect(_on)
|
reset_password_instructions_sent.disconnect(_on)
|
||||||
|
|
||||||
|
|
||||||
class CaptureSignals(object):
|
|
||||||
"""Testing utility for capturing blinker signals.
|
|
||||||
|
|
||||||
Context manager which mocks out selected signals and registers which are `sent` on and what
|
|
||||||
arguments were sent. Instantiate with a list of blinker `NamedSignals` to patch. Each signal
|
|
||||||
has it's `send` mocked out.
|
|
||||||
"""
|
|
||||||
def __init__(self, signals):
|
|
||||||
"""Patch all given signals and make them available as attributes.
|
|
||||||
|
|
||||||
:param signals: list of signals
|
|
||||||
"""
|
|
||||||
self._records = {}
|
|
||||||
self._receivers = {}
|
|
||||||
for signal in signals:
|
|
||||||
self._records[signal] = []
|
|
||||||
self._receivers[signal] = functools.partial(self._record, signal)
|
|
||||||
|
|
||||||
def __getitem__(self, signal):
|
|
||||||
"""All captured signals are available via `ctxt[signal]`.
|
|
||||||
"""
|
|
||||||
if isinstance(signal, blinker.base.NamedSignal):
|
|
||||||
return self._records[signal]
|
|
||||||
else:
|
|
||||||
super(CaptureSignals, self).__setitem__(signal)
|
|
||||||
|
|
||||||
def _record(self, signal, *args, **kwargs):
|
|
||||||
self._records[signal].append((args, kwargs))
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
for signal, receiver in self._receivers.iteritems():
|
|
||||||
signal.connect(receiver)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
|
||||||
for signal, receiver in self._receivers.iteritems():
|
|
||||||
signal.disconnect(receiver)
|
|
||||||
|
|
||||||
def signals_sent(self):
|
|
||||||
"""Return a set of the signals sent.
|
|
||||||
:rtype: list of blinker `NamedSignals`.
|
|
||||||
"""
|
|
||||||
return set([signal for signal, _ in self._records.iteritems() if self._records[signal]])
|
|
||||||
|
|
||||||
|
|
||||||
def capture_signals():
|
|
||||||
"""Factory method that creates a `CaptureSignals` with all the flask_security signals."""
|
|
||||||
return CaptureSignals([user_registered, user_confirmed,
|
|
||||||
confirm_instructions_sent, login_instructions_sent,
|
|
||||||
password_reset, password_changed,
|
|
||||||
reset_password_instructions_sent])
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+59
-45
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
flask.ext.security.views
|
flask_security.views
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Flask-Security views module
|
Flask-Security views module
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
:license: MIT, see LICENSE for more details.
|
:license: MIT, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import current_app, redirect, request, render_template, jsonify, \
|
from flask import current_app, redirect, request, jsonify, \
|
||||||
after_this_request, Blueprint
|
after_this_request, Blueprint
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from werkzeug.datastructures import MultiDict
|
from werkzeug.datastructures import MultiDict
|
||||||
@@ -26,7 +26,7 @@ from .changeable import change_user_password
|
|||||||
from .registerable import register_user
|
from .registerable import register_user
|
||||||
from .utils import config_value, do_flash, get_url, get_post_login_redirect, \
|
from .utils import config_value, do_flash, get_url, get_post_login_redirect, \
|
||||||
get_post_register_redirect, get_message, login_user, logout_user, \
|
get_post_register_redirect, get_message, login_user, logout_user, \
|
||||||
url_for_security as url_for
|
url_for_security as url_for, slash_url_suffix
|
||||||
|
|
||||||
# Convenient references
|
# Convenient references
|
||||||
_security = LocalProxy(lambda: current_app.extensions['security'])
|
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||||
@@ -34,7 +34,7 @@ _security = LocalProxy(lambda: current_app.extensions['security'])
|
|||||||
_datastore = LocalProxy(lambda: _security.datastore)
|
_datastore = LocalProxy(lambda: _security.datastore)
|
||||||
|
|
||||||
|
|
||||||
def _render_json(form, include_auth_token=False):
|
def _render_json(form, include_user=True, include_auth_token=False):
|
||||||
has_errors = len(form.errors) > 0
|
has_errors = len(form.errors) > 0
|
||||||
|
|
||||||
if has_errors:
|
if has_errors:
|
||||||
@@ -42,7 +42,9 @@ def _render_json(form, include_auth_token=False):
|
|||||||
response = dict(errors=form.errors)
|
response = dict(errors=form.errors)
|
||||||
else:
|
else:
|
||||||
code = 200
|
code = 200
|
||||||
response = dict(user=dict(id=str(form.user.id)))
|
response = dict()
|
||||||
|
if include_user:
|
||||||
|
response['user'] = dict(id=str(form.user.id))
|
||||||
if include_auth_token:
|
if include_auth_token:
|
||||||
token = form.user.get_auth_token()
|
token = form.user.get_auth_token()
|
||||||
response['user']['authentication_token'] = token
|
response['user']['authentication_token'] = token
|
||||||
@@ -75,29 +77,27 @@ def login():
|
|||||||
after_this_request(_commit)
|
after_this_request(_commit)
|
||||||
|
|
||||||
if not request.json:
|
if not request.json:
|
||||||
return redirect(get_post_login_redirect())
|
return redirect(get_post_login_redirect(form.next.data))
|
||||||
|
|
||||||
form.next.data = get_url(request.args.get('next')) \
|
|
||||||
or get_url(request.form.get('next')) or ''
|
|
||||||
|
|
||||||
if request.json:
|
if request.json:
|
||||||
return _render_json(form, True)
|
return _render_json(form, include_auth_token=True)
|
||||||
|
|
||||||
return render_template(config_value('LOGIN_USER_TEMPLATE'),
|
return _security.render_template(config_value('LOGIN_USER_TEMPLATE'),
|
||||||
login_user_form=form,
|
login_user_form=form,
|
||||||
**_ctx('login'))
|
**_ctx('login'))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def logout():
|
def logout():
|
||||||
"""View function which handles a logout request."""
|
"""View function which handles a logout request."""
|
||||||
|
|
||||||
logout_user()
|
if current_user.is_authenticated:
|
||||||
|
logout_user()
|
||||||
|
|
||||||
return redirect(request.args.get('next', None) or
|
return redirect(request.args.get('next', None) or
|
||||||
get_url(_security.post_logout_view))
|
get_url(_security.post_logout_view))
|
||||||
|
|
||||||
|
|
||||||
|
@anonymous_user_required
|
||||||
def register():
|
def register():
|
||||||
"""View function which handles a registration request."""
|
"""View function which handles a registration request."""
|
||||||
|
|
||||||
@@ -122,14 +122,20 @@ def register():
|
|||||||
login_user(user)
|
login_user(user)
|
||||||
|
|
||||||
if not request.json:
|
if not request.json:
|
||||||
return redirect(get_post_register_redirect())
|
if 'next' in form:
|
||||||
|
redirect_url = get_post_register_redirect(form.next.data)
|
||||||
|
else:
|
||||||
|
redirect_url = get_post_register_redirect()
|
||||||
|
|
||||||
|
return redirect(redirect_url)
|
||||||
|
return _render_json(form, include_auth_token=True)
|
||||||
|
|
||||||
if request.json:
|
if request.json:
|
||||||
return _render_json(form)
|
return _render_json(form)
|
||||||
|
|
||||||
return render_template(config_value('REGISTER_USER_TEMPLATE'),
|
return _security.render_template(config_value('REGISTER_USER_TEMPLATE'),
|
||||||
register_user_form=form,
|
register_user_form=form,
|
||||||
**_ctx('register'))
|
**_ctx('register'))
|
||||||
|
|
||||||
|
|
||||||
def send_login():
|
def send_login():
|
||||||
@@ -150,9 +156,9 @@ def send_login():
|
|||||||
if request.json:
|
if request.json:
|
||||||
return _render_json(form)
|
return _render_json(form)
|
||||||
|
|
||||||
return render_template(config_value('SEND_LOGIN_TEMPLATE'),
|
return _security.render_template(config_value('SEND_LOGIN_TEMPLATE'),
|
||||||
send_login_form=form,
|
send_login_form=form,
|
||||||
**_ctx('send_login'))
|
**_ctx('send_login'))
|
||||||
|
|
||||||
|
|
||||||
@anonymous_user_required
|
@anonymous_user_required
|
||||||
@@ -195,12 +201,11 @@ def send_confirmation():
|
|||||||
if request.json:
|
if request.json:
|
||||||
return _render_json(form)
|
return _render_json(form)
|
||||||
|
|
||||||
return render_template(config_value('SEND_CONFIRMATION_TEMPLATE'),
|
return _security.render_template(config_value('SEND_CONFIRMATION_TEMPLATE'),
|
||||||
send_confirmation_form=form,
|
send_confirmation_form=form,
|
||||||
**_ctx('send_confirmation'))
|
**_ctx('send_confirmation'))
|
||||||
|
|
||||||
|
|
||||||
@anonymous_user_required
|
|
||||||
def confirm_email(token):
|
def confirm_email(token):
|
||||||
"""View function which handles a email confirmation request."""
|
"""View function which handles a email confirmation request."""
|
||||||
|
|
||||||
@@ -217,15 +222,23 @@ def confirm_email(token):
|
|||||||
return redirect(get_url(_security.confirm_error_view) or
|
return redirect(get_url(_security.confirm_error_view) or
|
||||||
url_for('send_confirmation'))
|
url_for('send_confirmation'))
|
||||||
|
|
||||||
confirm_user(user)
|
if user != current_user:
|
||||||
login_user(user)
|
logout_user()
|
||||||
after_this_request(_commit)
|
login_user(user)
|
||||||
do_flash(*get_message('EMAIL_CONFIRMED'))
|
|
||||||
|
if confirm_user(user):
|
||||||
|
after_this_request(_commit)
|
||||||
|
msg = 'EMAIL_CONFIRMED'
|
||||||
|
else:
|
||||||
|
msg = 'ALREADY_CONFIRMED'
|
||||||
|
|
||||||
|
do_flash(*get_message(msg))
|
||||||
|
|
||||||
return redirect(get_url(_security.post_confirm_view) or
|
return redirect(get_url(_security.post_confirm_view) or
|
||||||
get_url(_security.post_login_view))
|
get_url(_security.post_login_view))
|
||||||
|
|
||||||
|
|
||||||
|
@anonymous_user_required
|
||||||
def forgot_password():
|
def forgot_password():
|
||||||
"""View function that handles a forgotten password request."""
|
"""View function that handles a forgotten password request."""
|
||||||
|
|
||||||
@@ -242,11 +255,11 @@ def forgot_password():
|
|||||||
do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email))
|
do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email))
|
||||||
|
|
||||||
if request.json:
|
if request.json:
|
||||||
return _render_json(form)
|
return _render_json(form, include_user=False)
|
||||||
|
|
||||||
return render_template(config_value('FORGOT_PASSWORD_TEMPLATE'),
|
return _security.render_template(config_value('FORGOT_PASSWORD_TEMPLATE'),
|
||||||
forgot_password_form=form,
|
forgot_password_form=form,
|
||||||
**_ctx('forgot_password'))
|
**_ctx('forgot_password'))
|
||||||
|
|
||||||
|
|
||||||
@anonymous_user_required
|
@anonymous_user_required
|
||||||
@@ -273,10 +286,10 @@ def reset_password(token):
|
|||||||
return redirect(get_url(_security.post_reset_view) or
|
return redirect(get_url(_security.post_reset_view) or
|
||||||
get_url(_security.post_login_view))
|
get_url(_security.post_login_view))
|
||||||
|
|
||||||
return render_template(config_value('RESET_PASSWORD_TEMPLATE'),
|
return _security.render_template(config_value('RESET_PASSWORD_TEMPLATE'),
|
||||||
reset_password_form=form,
|
reset_password_form=form,
|
||||||
reset_password_token=token,
|
reset_password_token=token,
|
||||||
**_ctx('reset_password'))
|
**_ctx('reset_password'))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -299,11 +312,12 @@ 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 _security.render_template(config_value('CHANGE_PASSWORD_TEMPLATE'),
|
||||||
change_password_form=form,
|
change_password_form=form,
|
||||||
**_ctx('change_password'))
|
**_ctx('change_password'))
|
||||||
|
|
||||||
|
|
||||||
def create_blueprint(state, import_name):
|
def create_blueprint(state, import_name):
|
||||||
@@ -320,7 +334,7 @@ def create_blueprint(state, import_name):
|
|||||||
bp.route(state.login_url,
|
bp.route(state.login_url,
|
||||||
methods=['GET', 'POST'],
|
methods=['GET', 'POST'],
|
||||||
endpoint='login')(send_login)
|
endpoint='login')(send_login)
|
||||||
bp.route(state.login_url + '/<token>',
|
bp.route(state.login_url + slash_url_suffix(state.login_url, '<token>'),
|
||||||
endpoint='token_login')(token_login)
|
endpoint='token_login')(token_login)
|
||||||
else:
|
else:
|
||||||
bp.route(state.login_url,
|
bp.route(state.login_url,
|
||||||
@@ -336,7 +350,7 @@ def create_blueprint(state, import_name):
|
|||||||
bp.route(state.reset_url,
|
bp.route(state.reset_url,
|
||||||
methods=['GET', 'POST'],
|
methods=['GET', 'POST'],
|
||||||
endpoint='forgot_password')(forgot_password)
|
endpoint='forgot_password')(forgot_password)
|
||||||
bp.route(state.reset_url + '/<token>',
|
bp.route(state.reset_url + slash_url_suffix(state.reset_url, '<token>'),
|
||||||
methods=['GET', 'POST'],
|
methods=['GET', 'POST'],
|
||||||
endpoint='reset_password')(reset_password)
|
endpoint='reset_password')(reset_password)
|
||||||
|
|
||||||
@@ -349,7 +363,7 @@ def create_blueprint(state, import_name):
|
|||||||
bp.route(state.confirm_url,
|
bp.route(state.confirm_url,
|
||||||
methods=['GET', 'POST'],
|
methods=['GET', 'POST'],
|
||||||
endpoint='send_confirmation')(send_confirmation)
|
endpoint='send_confirmation')(send_confirmation)
|
||||||
bp.route(state.confirm_url + '/<token>',
|
bp.route(state.confirm_url + slash_url_suffix(state.confirm_url, '<token>'),
|
||||||
methods=['GET', 'POST'],
|
methods=['GET', 'POST'],
|
||||||
endpoint='confirm_email')(confirm_email)
|
endpoint='confirm_email')(confirm_email)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Flask-SQLAlchemy>=1.0
|
||||||
|
bcrypt>=1.0.2,<2.0.0
|
||||||
|
flask-mongoengine>=0.7.0,<0.7.3
|
||||||
|
flask-peewee>=0.6.5
|
||||||
|
pymongo==2.8
|
||||||
|
pytest>=2.5.2
|
||||||
|
pytest-cov>=1.6
|
||||||
|
pytest-flakes>=0.2
|
||||||
|
pytest-pep8>=1.0.5
|
||||||
|
tox>=1.7.0
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
Flask>=0.9
|
Flask>=0.9
|
||||||
Flask-Login>=0.1.3
|
Flask-Login>=0.3.0,<0.4
|
||||||
Flask-Mail>=0.7.3
|
Flask-Mail>=0.7.3
|
||||||
Flask-Principal>=0.3.3
|
Flask-Principal>=0.3.3
|
||||||
Flask-WTF>=0.8
|
Flask-WTF>=0.8
|
||||||
|
|||||||
@@ -3,4 +3,17 @@ source-dir = docs/
|
|||||||
build-dir = docs/_build
|
build-dir = docs/_build
|
||||||
|
|
||||||
[upload_sphinx]
|
[upload_sphinx]
|
||||||
upload-dir = docs/_build/html
|
upload-dir = docs/_build/html
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
pep8maxlinelength = 99
|
||||||
|
|
||||||
|
pep8ignore =
|
||||||
|
docs/* ALL
|
||||||
|
scripts/* ALL
|
||||||
|
|
||||||
|
flakes-ignore =
|
||||||
|
ImportStarUsed
|
||||||
|
flask_security/__init__.py UnusedImport
|
||||||
|
docs/* ALL
|
||||||
|
scripts/* ALL
|
||||||
|
|||||||
@@ -1,56 +1,63 @@
|
|||||||
"""
|
"""
|
||||||
Flask-Security
|
Flask-Security
|
||||||
==============
|
==============
|
||||||
|
|
||||||
Flask-Security is a Flask extension that aims to add quick and simple security
|
|
||||||
to your Flask applications.
|
|
||||||
|
|
||||||
Resources
|
|
||||||
---------
|
|
||||||
|
|
||||||
* `Documentation <http://packages.python.org/Flask-Security/>`_
|
|
||||||
* `Issue Tracker <https://github.com/mattupstate/flask-security/issues>`_
|
|
||||||
* `Source <https://github.com/mattupstate/flask-security>`_
|
|
||||||
* `Development Version
|
|
||||||
<https://github.com/mattupstate/flask-security/raw/develop#egg=Flask-Security-dev>`_
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from setuptools import setup
|
import multiprocessing # pragma: no flakes
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
from setuptools.command.test import test as TestCommand
|
||||||
|
|
||||||
|
|
||||||
|
def get_requirements(suffix=''):
|
||||||
|
with open('requirements%s.txt' % suffix) as f:
|
||||||
|
rv = f.read().splitlines()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
def get_long_description():
|
||||||
|
with open('README.rst') as f:
|
||||||
|
rv = f.read()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
class PyTest(TestCommand):
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
TestCommand.finalize_options(self)
|
||||||
|
self.test_args = [
|
||||||
|
'-xrs',
|
||||||
|
'--cov', 'flask_security',
|
||||||
|
'--cov-report', 'term-missing',
|
||||||
|
'--pep8',
|
||||||
|
'--flakes',
|
||||||
|
'--cache-clear'
|
||||||
|
]
|
||||||
|
self.test_suite = True
|
||||||
|
|
||||||
|
def run_tests(self):
|
||||||
|
import pytest
|
||||||
|
errno = pytest.main(self.test_args)
|
||||||
|
sys.exit(errno)
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='Flask-Security',
|
name='Flask-Security',
|
||||||
version='1.6.4',
|
version='1.7.5',
|
||||||
url='https://github.com/mattupstate/flask-security',
|
url='https://github.com/mattupstate/flask-security',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
author='Matt Wright',
|
author='Matt Wright',
|
||||||
author_email='matt@nobien.net',
|
author_email='matt@nobien.net',
|
||||||
description='Simple security for Flask apps',
|
description='Simple security for Flask apps',
|
||||||
long_description=__doc__,
|
long_description=get_long_description(),
|
||||||
packages=[
|
packages=find_packages(),
|
||||||
'flask_security'
|
|
||||||
],
|
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
platforms='any',
|
platforms='any',
|
||||||
install_requires=[
|
install_requires=get_requirements(),
|
||||||
'Flask>=0.9',
|
tests_require=get_requirements('-dev'),
|
||||||
'Flask-Login==0.2.3',
|
cmdclass={'test': PyTest},
|
||||||
'Flask-Mail==0.7.3',
|
|
||||||
'Flask-Principal==0.3.3',
|
|
||||||
'Flask-WTF==0.8',
|
|
||||||
'itsdangerous>=0.17',
|
|
||||||
'passlib>=1.6.1',
|
|
||||||
],
|
|
||||||
test_suite='nose.collector',
|
|
||||||
tests_require=[
|
|
||||||
'nose',
|
|
||||||
'Flask-SQLAlchemy',
|
|
||||||
'Flask-MongoEngine',
|
|
||||||
'Flask-Peewee',
|
|
||||||
'py-bcrypt',
|
|
||||||
'simplejson'
|
|
||||||
],
|
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
'Environment :: Web Environment',
|
'Environment :: Web Environment',
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import hmac
|
|
||||||
|
|
||||||
from hashlib import sha1
|
|
||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from tests.test_app.sqlalchemy import create_app
|
|
||||||
|
|
||||||
|
|
||||||
class SecurityTest(TestCase):
|
|
||||||
|
|
||||||
APP_KWARGS = {
|
|
||||||
'register_blueprint': True,
|
|
||||||
}
|
|
||||||
AUTH_CONFIG = None
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(SecurityTest, self).setUp()
|
|
||||||
|
|
||||||
app_kwargs = self.APP_KWARGS
|
|
||||||
app = self._create_app(self.AUTH_CONFIG or {}, **app_kwargs)
|
|
||||||
app.debug = False
|
|
||||||
app.config['TESTING'] = True
|
|
||||||
|
|
||||||
self.app = app
|
|
||||||
self.client = app.test_client()
|
|
||||||
|
|
||||||
with self.client.session_transaction() as session:
|
|
||||||
session['csrf'] = 'csrf_token'
|
|
||||||
|
|
||||||
csrf_hmac = hmac.new(self.app.config['SECRET_KEY'],
|
|
||||||
'csrf_token'.encode('utf8'), digestmod=sha1)
|
|
||||||
self.csrf_token = '##' + csrf_hmac.hexdigest()
|
|
||||||
|
|
||||||
def _create_app(self, auth_config, **kwargs):
|
|
||||||
return create_app(auth_config, **kwargs)
|
|
||||||
|
|
||||||
def _get(self, route, content_type=None, follow_redirects=None, headers=None):
|
|
||||||
return self.client.get(route, follow_redirects=follow_redirects,
|
|
||||||
content_type=content_type or 'text/html',
|
|
||||||
headers=headers)
|
|
||||||
|
|
||||||
def _post(self, route, data=None, content_type=None, follow_redirects=True, headers=None):
|
|
||||||
if isinstance(data, dict):
|
|
||||||
data['csrf_token'] = self.csrf_token
|
|
||||||
|
|
||||||
return self.client.post(route, data=data,
|
|
||||||
follow_redirects=follow_redirects,
|
|
||||||
content_type=content_type or 'application/x-www-form-urlencoded',
|
|
||||||
headers=headers)
|
|
||||||
|
|
||||||
def register(self, email, password='password'):
|
|
||||||
data = dict(email=email, password=password, csrf_token=self.csrf_token)
|
|
||||||
return self.client.post('/register', data=data, follow_redirects=True)
|
|
||||||
|
|
||||||
def authenticate(self, email="matt@lp.com", password="password", endpoint=None, **kwargs):
|
|
||||||
data = dict(email=email, password=password, remember='y')
|
|
||||||
return self._post(endpoint or '/login', data=data, **kwargs)
|
|
||||||
|
|
||||||
def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None):
|
|
||||||
data = """{
|
|
||||||
"email": "%s",
|
|
||||||
"password": "%s",
|
|
||||||
"csrf_token": "%s"
|
|
||||||
}"""
|
|
||||||
return self._post(endpoint or '/login', content_type="application/json",
|
|
||||||
data=data % (email, password, self.csrf_token))
|
|
||||||
|
|
||||||
def logout(self, endpoint=None):
|
|
||||||
return self._get(endpoint or '/logout', follow_redirects=True)
|
|
||||||
|
|
||||||
def assertIsHomePage(self, data):
|
|
||||||
self.assertIn('Home Page', data)
|
|
||||||
|
|
||||||
def assertIn(self, member, container, msg=None):
|
|
||||||
if hasattr(TestCase, 'assertIn'):
|
|
||||||
return TestCase.assertIn(self, member, container, msg)
|
|
||||||
|
|
||||||
return self.assertTrue(member in container)
|
|
||||||
|
|
||||||
def assertNotIn(self, member, container, msg=None):
|
|
||||||
if hasattr(TestCase, 'assertNotIn'):
|
|
||||||
return TestCase.assertNotIn(self, member, container, msg)
|
|
||||||
|
|
||||||
return self.assertFalse(member in container)
|
|
||||||
|
|
||||||
def assertIsNotNone(self, obj, msg=None):
|
|
||||||
if hasattr(TestCase, 'assertIsNotNone'):
|
|
||||||
return TestCase.assertIsNotNone(self, obj, msg)
|
|
||||||
|
|
||||||
return self.assertTrue(obj is not None)
|
|
||||||
|
|
||||||
def get_message(self, key, **kwargs):
|
|
||||||
return self.app.config['SECURITY_MSG_' + key][0] % kwargs
|
|
||||||
@@ -1,781 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import time
|
|
||||||
import simplejson as json
|
|
||||||
|
|
||||||
from flask.ext.security.utils import capture_registrations, \
|
|
||||||
capture_reset_password_requests, capture_passwordless_login_requests
|
|
||||||
from flask.ext.security.forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
|
|
||||||
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
|
|
||||||
PasswordlessLoginForm
|
|
||||||
from flask.ext.security.forms import TextField, SubmitField, valid_user_email
|
|
||||||
|
|
||||||
|
|
||||||
from tests import SecurityTest
|
|
||||||
|
|
||||||
|
|
||||||
class ConfiguredPasswordHashSecurityTests(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_PASSWORD_HASH': 'bcrypt',
|
|
||||||
'SECURITY_PASSWORD_SALT': 'so-salty',
|
|
||||||
'USER_COUNT': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_authenticate(self):
|
|
||||||
r = self.authenticate(endpoint="/login")
|
|
||||||
self.assertIn('Home Page', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfiguredSecurityTests(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_REGISTERABLE': True,
|
|
||||||
'SECURITY_LOGOUT_URL': '/custom_logout',
|
|
||||||
'SECURITY_LOGIN_URL': '/custom_login',
|
|
||||||
'SECURITY_POST_LOGIN_VIEW': '/post_login',
|
|
||||||
'SECURITY_POST_LOGOUT_VIEW': '/post_logout',
|
|
||||||
'SECURITY_POST_REGISTER_VIEW': '/post_register',
|
|
||||||
'SECURITY_UNAUTHORIZED_VIEW': '/unauthorized',
|
|
||||||
'SECURITY_DEFAULT_HTTP_AUTH_REALM': 'Custom Realm'
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_login_view(self):
|
|
||||||
r = self._get('/custom_login')
|
|
||||||
self.assertIn("<h1>Login</h1>", r.data)
|
|
||||||
|
|
||||||
def test_authenticate(self):
|
|
||||||
r = self.authenticate(endpoint="/custom_login")
|
|
||||||
self.assertIn('Post Login', r.data)
|
|
||||||
|
|
||||||
def test_logout(self):
|
|
||||||
self.authenticate(endpoint="/custom_login")
|
|
||||||
r = self.logout(endpoint="/custom_logout")
|
|
||||||
self.assertIn('Post Logout', r.data)
|
|
||||||
|
|
||||||
def test_register_view(self):
|
|
||||||
r = self._get('/register')
|
|
||||||
self.assertIn('<h1>Register</h1>', r.data)
|
|
||||||
|
|
||||||
def test_register(self):
|
|
||||||
data = dict(email='dude@lp.com',
|
|
||||||
password='password',
|
|
||||||
password_confirm='password')
|
|
||||||
|
|
||||||
r = self._post('/register', data=data, follow_redirects=True)
|
|
||||||
self.assertIn('Post Register', r.data)
|
|
||||||
|
|
||||||
def test_register_with_next_querystring_argument(self):
|
|
||||||
data = dict(email='dude@lp.com',
|
|
||||||
password='password',
|
|
||||||
password_confirm='password')
|
|
||||||
|
|
||||||
r = self._post('/register?next=/page1', data=data, follow_redirects=True)
|
|
||||||
self.assertIn('Page 1', r.data)
|
|
||||||
|
|
||||||
def test_register_json(self):
|
|
||||||
data = '{ "email": "dude@lp.com", "password": "password", "csrf_token":"%s" }' % self.csrf_token
|
|
||||||
r = self._post('/register', data=data, content_type='application/json')
|
|
||||||
data = json.loads(r.data)
|
|
||||||
self.assertEquals(data['meta']['code'], 200)
|
|
||||||
|
|
||||||
def test_register_existing_email(self):
|
|
||||||
data = dict(email='matt@lp.com',
|
|
||||||
password='password',
|
|
||||||
password_confirm='password')
|
|
||||||
r = self._post('/register', data=data, follow_redirects=True)
|
|
||||||
msg = 'matt@lp.com is already associated with an account'
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
def test_unauthorized(self):
|
|
||||||
self.authenticate("joe@lp.com", endpoint="/custom_auth")
|
|
||||||
r = self._get("/admin", follow_redirects=True)
|
|
||||||
msg = 'You are not allowed to access the requested resouce'
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
def test_default_http_auth_realm(self):
|
|
||||||
r = self._get('/http', headers={
|
|
||||||
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
|
|
||||||
})
|
|
||||||
self.assertIn('<h1>Unauthorized</h1>', r.data)
|
|
||||||
self.assertIn('WWW-Authenticate', r.headers)
|
|
||||||
self.assertEquals('Basic realm="Custom Realm"',
|
|
||||||
r.headers['WWW-Authenticate'])
|
|
||||||
|
|
||||||
|
|
||||||
class BadConfiguredSecurityTests(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_PASSWORD_HASH': 'bcrypt',
|
|
||||||
'USER_COUNT': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_bad_configuration_raises_runtimer_error(self):
|
|
||||||
self.assertRaises(RuntimeError, self.authenticate)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultTemplatePathTests(SecurityTest):
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_LOGIN_USER_TEMPLATE': 'custom_security/login_user.html',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_login_user_template(self):
|
|
||||||
r = self._get('/login')
|
|
||||||
|
|
||||||
self.assertIn('CUSTOM LOGIN USER', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class RegisterableTemplatePathTests(SecurityTest):
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_REGISTERABLE': True,
|
|
||||||
'SECURITY_REGISTER_USER_TEMPLATE': 'custom_security/register_user.html'
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_register_user_template(self):
|
|
||||||
r = self._get('/register')
|
|
||||||
|
|
||||||
self.assertIn('CUSTOM REGISTER USER', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class RecoverableTemplatePathTests(SecurityTest):
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_RECOVERABLE': True,
|
|
||||||
'SECURITY_FORGOT_PASSWORD_TEMPLATE': 'custom_security/forgot_password.html',
|
|
||||||
'SECURITY_RESET_PASSWORD_TEMPLATE': 'custom_security/reset_password.html',
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_forgot_password_template(self):
|
|
||||||
r = self._get('/reset')
|
|
||||||
|
|
||||||
self.assertIn('CUSTOM FORGOT PASSWORD', r.data)
|
|
||||||
|
|
||||||
def test_reset_password_template(self):
|
|
||||||
with capture_reset_password_requests() as requests:
|
|
||||||
r = self._post('/reset',
|
|
||||||
data=dict(email='joe@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
|
|
||||||
t = requests[0]['token']
|
|
||||||
|
|
||||||
r = self._get('/reset/' + t)
|
|
||||||
|
|
||||||
self.assertIn('CUSTOM RESET PASSWORD', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmableTemplatePathTests(SecurityTest):
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CONFIRMABLE': True,
|
|
||||||
'SECURITY_SEND_CONFIRMATION_TEMPLATE': 'custom_security/send_confirmation.html'
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_send_confirmation_template(self):
|
|
||||||
r = self._get('/confirm')
|
|
||||||
|
|
||||||
self.assertIn('CUSTOM SEND CONFIRMATION', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordlessTemplatePathTests(SecurityTest):
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_PASSWORDLESS': True,
|
|
||||||
'SECURITY_SEND_LOGIN_TEMPLATE': 'custom_security/send_login.html'
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_send_login_template(self):
|
|
||||||
r = self._get('/login')
|
|
||||||
|
|
||||||
self.assertIn('CUSTOM SEND LOGIN', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class RegisterableTests(SecurityTest):
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_REGISTERABLE': True,
|
|
||||||
'USER_COUNT': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_register_valid_user(self):
|
|
||||||
data = dict(email='dude@lp.com',
|
|
||||||
password='password',
|
|
||||||
password_confirm='password')
|
|
||||||
self._post('/register', data=data, follow_redirects=True)
|
|
||||||
r = self.authenticate('dude@lp.com')
|
|
||||||
self.assertIn('Hello dude@lp.com', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmableTests(SecurityTest):
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CONFIRMABLE': True,
|
|
||||||
'SECURITY_REGISTERABLE': True,
|
|
||||||
'SECURITY_EMAIL_SUBJECT_REGISTER': 'Custom welcome subject',
|
|
||||||
'USER_COUNT': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_login_before_confirmation(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
self.register(e)
|
|
||||||
r = self.authenticate(email=e)
|
|
||||||
self.assertIn(self.get_message('CONFIRMATION_REQUIRED'), r.data)
|
|
||||||
|
|
||||||
def test_send_confirmation_of_already_confirmed_account(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
|
|
||||||
with capture_registrations() as registrations:
|
|
||||||
self.register(e)
|
|
||||||
token = registrations[0]['confirm_token']
|
|
||||||
|
|
||||||
self.client.get('/confirm/' + token, follow_redirects=True)
|
|
||||||
self.logout()
|
|
||||||
r = self._post('/confirm', data=dict(email=e))
|
|
||||||
self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data)
|
|
||||||
|
|
||||||
def test_register_sends_confirmation_email(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
with self.app.extensions['mail'].record_messages() as outbox:
|
|
||||||
self.register(e)
|
|
||||||
self.assertEqual(len(outbox), 1)
|
|
||||||
self.assertIn(e, outbox[0].html)
|
|
||||||
self.assertEqual('Custom welcome subject', outbox[0].subject)
|
|
||||||
|
|
||||||
def test_confirm_email(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
|
|
||||||
with capture_registrations() as registrations:
|
|
||||||
self.register(e)
|
|
||||||
token = registrations[0]['confirm_token']
|
|
||||||
|
|
||||||
r = self.client.get('/confirm/' + token, follow_redirects=True)
|
|
||||||
|
|
||||||
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
def test_invalid_token_when_confirming_email(self):
|
|
||||||
r = self.client.get('/confirm/bogus', follow_redirects=True)
|
|
||||||
msg = self.app.config['SECURITY_MSG_INVALID_CONFIRMATION_TOKEN'][0]
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
def test_send_confirmation_json(self):
|
|
||||||
r = self._post('/confirm', data='{"email": "matt@lp.com"}',
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEquals(r.status_code, 200)
|
|
||||||
|
|
||||||
def test_send_confirmation_with_invalid_email(self):
|
|
||||||
r = self._post('/confirm', data=dict(email='bogus@bogus.com'))
|
|
||||||
msg = self.app.config['SECURITY_MSG_USER_DOES_NOT_EXIST'][0]
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
def test_resend_confirmation(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
self.register(e)
|
|
||||||
r = self._post('/confirm', data={'email': e})
|
|
||||||
|
|
||||||
msg = self.get_message('CONFIRMATION_REQUEST', email=e)
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
def test_user_deleted_before_confirmation(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
|
|
||||||
with capture_registrations() as registrations:
|
|
||||||
self.register(e)
|
|
||||||
user = registrations[0]['user']
|
|
||||||
token = registrations[0]['confirm_token']
|
|
||||||
|
|
||||||
with self.app.app_context():
|
|
||||||
from flask_security.core import _security
|
|
||||||
_security.datastore.delete(user)
|
|
||||||
_security.datastore.commit()
|
|
||||||
|
|
||||||
r = self.client.get('/confirm/' + token, follow_redirects=True)
|
|
||||||
msg = self.app.config['SECURITY_MSG_INVALID_CONFIRMATION_TOKEN'][0]
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class ExpiredConfirmationTest(SecurityTest):
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CONFIRMABLE': True,
|
|
||||||
'SECURITY_REGISTERABLE': True,
|
|
||||||
'SECURITY_CONFIRM_EMAIL_WITHIN': '1 milliseconds',
|
|
||||||
'USER_COUNT': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_expired_confirmation_token_sends_email(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
|
|
||||||
with capture_registrations() as registrations:
|
|
||||||
self.register(e)
|
|
||||||
token = registrations[0]['confirm_token']
|
|
||||||
|
|
||||||
time.sleep(1.25)
|
|
||||||
|
|
||||||
with self.app.extensions['mail'].record_messages() as outbox:
|
|
||||||
r = self.client.get('/confirm/' + token, follow_redirects=True)
|
|
||||||
|
|
||||||
self.assertEqual(len(outbox), 1)
|
|
||||||
self.assertNotIn(token, outbox[0].html)
|
|
||||||
|
|
||||||
expire_text = self.AUTH_CONFIG['SECURITY_CONFIRM_EMAIL_WITHIN']
|
|
||||||
msg = self.app.config['SECURITY_MSG_CONFIRMATION_EXPIRED'][0]
|
|
||||||
msg = msg % dict(within=expire_text, email=e)
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class LoginWithoutImmediateConfirmTests(SecurityTest):
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CONFIRMABLE': True,
|
|
||||||
'SECURITY_REGISTERABLE': True,
|
|
||||||
'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True,
|
|
||||||
'USER_COUNT': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_register_valid_user_automatically_signs_in(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, r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class RecoverableTests(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_RECOVERABLE': True,
|
|
||||||
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
|
|
||||||
'SECURITY_POST_FORGOT_VIEW': '/'
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_reset_view(self):
|
|
||||||
with capture_reset_password_requests() as requests:
|
|
||||||
r = self._post('/reset',
|
|
||||||
data=dict(email='joe@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
t = requests[0]['token']
|
|
||||||
r = self._get('/reset/' + t)
|
|
||||||
self.assertIn('<h1>Reset password</h1>', r.data)
|
|
||||||
|
|
||||||
def test_forgot_post_sends_email(self):
|
|
||||||
with capture_reset_password_requests():
|
|
||||||
with self.app.extensions['mail'].record_messages() as outbox:
|
|
||||||
self._post('/reset', data=dict(email='joe@lp.com'))
|
|
||||||
self.assertEqual(len(outbox), 1)
|
|
||||||
|
|
||||||
def test_forgot_password_json(self):
|
|
||||||
r = self._post('/reset', data='{"email": "matt@lp.com"}',
|
|
||||||
content_type="application/json")
|
|
||||||
self.assertEquals(r.status_code, 200)
|
|
||||||
|
|
||||||
def test_forgot_password_invalid_email(self):
|
|
||||||
r = self._post('/reset',
|
|
||||||
data=dict(email='larry@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
self.assertIn("Specified user does not exist", r.data)
|
|
||||||
|
|
||||||
def test_reset_password_with_valid_token(self):
|
|
||||||
with capture_reset_password_requests() as requests:
|
|
||||||
r = self._post('/reset',
|
|
||||||
data=dict(email='joe@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
t = requests[0]['token']
|
|
||||||
|
|
||||||
r = self._post('/reset/' + t, data={
|
|
||||||
'password': 'newpassword',
|
|
||||||
'password_confirm': 'newpassword'
|
|
||||||
}, follow_redirects=True)
|
|
||||||
|
|
||||||
r = self.logout()
|
|
||||||
r = self.authenticate('joe@lp.com', 'newpassword')
|
|
||||||
self.assertIn('Hello joe@lp.com', r.data)
|
|
||||||
|
|
||||||
def test_reset_password_with_invalid_token(self):
|
|
||||||
r = self._post('/reset/bogus', data={
|
|
||||||
'password': 'newpassword',
|
|
||||||
'password_confirm': 'newpassword'
|
|
||||||
}, follow_redirects=True)
|
|
||||||
|
|
||||||
self.assertIn(self.get_message('INVALID_RESET_PASSWORD_TOKEN'), r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class ExpiredResetPasswordTest(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_RECOVERABLE': True,
|
|
||||||
'SECURITY_RESET_PASSWORD_WITHIN': '1 milliseconds'
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_reset_password_with_expired_token(self):
|
|
||||||
with capture_reset_password_requests() as requests:
|
|
||||||
r = self._post('/reset', data=dict(email='joe@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
t = requests[0]['token']
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
r = self._post('/reset/' + t, data={
|
|
||||||
'password': 'newpassword',
|
|
||||||
'password_confirm': 'newpassword'
|
|
||||||
}, follow_redirects=True)
|
|
||||||
|
|
||||||
self.assertIn('You did not reset your password within', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePasswordTest(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_RECOVERABLE': True,
|
|
||||||
'SECURITY_CHANGEABLE': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_change_password(self):
|
|
||||||
self.authenticate()
|
|
||||||
r = self.client.get('/change', follow_redirects=True)
|
|
||||||
self.assertIn('Change password', r.data)
|
|
||||||
|
|
||||||
def test_change_password_invalid(self):
|
|
||||||
self.authenticate()
|
|
||||||
r = self._post('/change', data={
|
|
||||||
'password': 'notpassword',
|
|
||||||
'new_password': 'newpassword',
|
|
||||||
'new_password_confirm': 'newpassword'
|
|
||||||
}, follow_redirects=True)
|
|
||||||
self.assertNotIn('You successfully changed your password', r.data)
|
|
||||||
self.assertIn('Invalid password', r.data)
|
|
||||||
|
|
||||||
def test_change_password_mismatch(self):
|
|
||||||
self.authenticate()
|
|
||||||
r = self._post('/change', data={
|
|
||||||
'password': 'password',
|
|
||||||
'new_password': 'newpassword',
|
|
||||||
'new_password_confirm': 'notnewpassword'
|
|
||||||
}, follow_redirects=True)
|
|
||||||
self.assertNotIn('You successfully changed your password', r.data)
|
|
||||||
self.assertIn('Passwords do not match', r.data)
|
|
||||||
|
|
||||||
def test_change_password_bad_password(self):
|
|
||||||
self.authenticate()
|
|
||||||
r = self._post('/change', data={
|
|
||||||
'password': 'password',
|
|
||||||
'new_password': 'a',
|
|
||||||
'new_password_confirm': 'a'
|
|
||||||
}, follow_redirects=True)
|
|
||||||
self.assertNotIn('You successfully changed your password', r.data)
|
|
||||||
self.assertIn('Field must be between', r.data)
|
|
||||||
|
|
||||||
def test_change_password_success(self):
|
|
||||||
self.authenticate()
|
|
||||||
with self.app.extensions['mail'].record_messages() as outbox:
|
|
||||||
r = self._post('/change', data={
|
|
||||||
'password': 'password',
|
|
||||||
'new_password': 'newpassword',
|
|
||||||
'new_password_confirm': 'newpassword'
|
|
||||||
}, follow_redirects=True)
|
|
||||||
|
|
||||||
self.assertIn('You successfully changed your password', r.data)
|
|
||||||
self.assertIn('Home Page', r.data)
|
|
||||||
|
|
||||||
self.assertEqual(len(outbox), 1)
|
|
||||||
self.assertIn("Your password has been changed", outbox[0].html)
|
|
||||||
self.assertIn("/reset", outbox[0].html)
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePasswordPostViewTest(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CHANGEABLE': True,
|
|
||||||
'SECURITY_POST_CHANGE_VIEW': '/profile',
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_change_password_success(self):
|
|
||||||
self.authenticate()
|
|
||||||
r = self._post('/change', data={
|
|
||||||
'password': 'password',
|
|
||||||
'new_password': 'newpassword',
|
|
||||||
'new_password_confirm': 'newpassword'
|
|
||||||
}, follow_redirects=True)
|
|
||||||
|
|
||||||
self.assertIn('Profile Page', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePasswordDisabledTest(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CHANGEABLE': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_change_password_endpoint_is_404(self):
|
|
||||||
self.authenticate()
|
|
||||||
r = self.client.get('/change', follow_redirects=True)
|
|
||||||
self.assertEqual(404, r.status_code)
|
|
||||||
|
|
||||||
|
|
||||||
class TrackableTests(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_TRACKABLE': True,
|
|
||||||
'USER_COUNT': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_did_track(self):
|
|
||||||
e = 'matt@lp.com'
|
|
||||||
self.authenticate(email=e)
|
|
||||||
self.logout()
|
|
||||||
self.authenticate(email=e)
|
|
||||||
|
|
||||||
with self.app.test_request_context('/profile'):
|
|
||||||
user = self.app.security.datastore.find_user(email=e)
|
|
||||||
self.assertIsNotNone(user.last_login_at)
|
|
||||||
self.assertIsNotNone(user.current_login_at)
|
|
||||||
self.assertEquals('untrackable', user.last_login_ip)
|
|
||||||
self.assertEquals('untrackable', user.current_login_ip)
|
|
||||||
self.assertEquals(2, user.login_count)
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordlessTests(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_PASSWORDLESS': True
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_login_request_for_inactive_user(self):
|
|
||||||
msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0]
|
|
||||||
r = self._post('/login', data=dict(email='tiya@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
def test_request_login_token_with_json_and_valid_email(self):
|
|
||||||
data = '{"email": "matt@lp.com", "password": "password", "csrf_token":"%s"}' % self.csrf_token
|
|
||||||
r = self._post('/login', data=data, content_type='application/json')
|
|
||||||
self.assertEquals(r.status_code, 200)
|
|
||||||
self.assertNotIn('error', r.data)
|
|
||||||
|
|
||||||
def test_request_login_token_with_json_and_invalid_email(self):
|
|
||||||
data = '{"email": "nobody@lp.com", "password": "password"}'
|
|
||||||
r = self._post('/login', data=data, content_type='application/json')
|
|
||||||
self.assertIn('errors', r.data)
|
|
||||||
|
|
||||||
def test_request_login_token_sends_email_and_can_login(self):
|
|
||||||
e = 'matt@lp.com'
|
|
||||||
r, user, token = None, None, None
|
|
||||||
|
|
||||||
with capture_passwordless_login_requests() as requests:
|
|
||||||
with self.app.extensions['mail'].record_messages() as outbox:
|
|
||||||
r = self._post('/login', data=dict(email=e),
|
|
||||||
follow_redirects=True)
|
|
||||||
|
|
||||||
self.assertEqual(len(outbox), 1)
|
|
||||||
|
|
||||||
self.assertEquals(1, len(requests))
|
|
||||||
self.assertIn('user', requests[0])
|
|
||||||
self.assertIn('login_token', requests[0])
|
|
||||||
|
|
||||||
user = requests[0]['user']
|
|
||||||
token = requests[0]['login_token']
|
|
||||||
|
|
||||||
msg = self.app.config['SECURITY_MSG_LOGIN_EMAIL_SENT'][0]
|
|
||||||
msg = msg % dict(email=user.email)
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
r = self.client.get('/login/' + token, follow_redirects=True)
|
|
||||||
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
r = self.client.get('/profile')
|
|
||||||
self.assertIn('Profile Page', r.data)
|
|
||||||
|
|
||||||
def test_invalid_login_token(self):
|
|
||||||
msg = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0]
|
|
||||||
r = self._get('/login/bogus', follow_redirects=True)
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
def test_token_login_when_already_authenticated(self):
|
|
||||||
with capture_passwordless_login_requests() as requests:
|
|
||||||
self._post('/login', data=dict(email='matt@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
token = requests[0]['login_token']
|
|
||||||
|
|
||||||
r = self.client.get('/login/' + token, follow_redirects=True)
|
|
||||||
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
r = self.client.get('/login/' + token, follow_redirects=True)
|
|
||||||
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
|
|
||||||
self.assertNotIn(msg, r.data)
|
|
||||||
|
|
||||||
def test_send_login_with_invalid_email(self):
|
|
||||||
r = self._post('/login', data=dict(email='bogus@bogus.com'))
|
|
||||||
self.assertIn('Specified user does not exist', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class ExpiredLoginTokenTests(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_PASSWORDLESS': True,
|
|
||||||
'SECURITY_LOGIN_WITHIN': '1 milliseconds',
|
|
||||||
'USER_COUNT': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_expired_login_token_sends_email(self):
|
|
||||||
e = 'matt@lp.com'
|
|
||||||
|
|
||||||
with capture_passwordless_login_requests() as requests:
|
|
||||||
self._post('/login', data=dict(email=e), follow_redirects=True)
|
|
||||||
token = requests[0]['login_token']
|
|
||||||
|
|
||||||
time.sleep(1.25)
|
|
||||||
|
|
||||||
with self.app.extensions['mail'].record_messages() as outbox:
|
|
||||||
r = self.client.get('/login/' + token, follow_redirects=True)
|
|
||||||
|
|
||||||
expire_text = self.AUTH_CONFIG['SECURITY_LOGIN_WITHIN']
|
|
||||||
msg = self.app.config['SECURITY_MSG_LOGIN_EXPIRED'][0]
|
|
||||||
msg = msg % dict(within=expire_text, email=e)
|
|
||||||
self.assertIn(msg, r.data)
|
|
||||||
|
|
||||||
self.assertEqual(len(outbox), 1)
|
|
||||||
self.assertIn(e, outbox[0].html)
|
|
||||||
self.assertNotIn(token, outbox[0].html)
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncMailTaskTests(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_RECOVERABLE': True,
|
|
||||||
'USER_COUNT': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(AsyncMailTaskTests, self).setUp()
|
|
||||||
self.mail_sent = False
|
|
||||||
|
|
||||||
def test_send_email_task_is_called(self):
|
|
||||||
@self.app.security.send_mail_task
|
|
||||||
def send_email(msg):
|
|
||||||
self.mail_sent = True
|
|
||||||
|
|
||||||
self._post('/reset', data=dict(email='matt@lp.com'))
|
|
||||||
self.assertTrue(self.mail_sent)
|
|
||||||
|
|
||||||
|
|
||||||
class NoBlueprintTests(SecurityTest):
|
|
||||||
|
|
||||||
APP_KWARGS = {
|
|
||||||
'register_blueprint': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'USER_COUNT': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_login_endpoint_is_404(self):
|
|
||||||
r = self._get('/login')
|
|
||||||
self.assertEqual(404, r.status_code)
|
|
||||||
|
|
||||||
def test_http_auth_without_blueprint(self):
|
|
||||||
auth = 'Basic ' + base64.b64encode("matt@lp.com:password")
|
|
||||||
r = self._get('/http', headers={'Authorization': auth})
|
|
||||||
self.assertIn('HTTP Authentication', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class ExtendFormsTest(SecurityTest):
|
|
||||||
|
|
||||||
class MyLoginForm(LoginForm):
|
|
||||||
email = TextField('My Login Email Address Field')
|
|
||||||
|
|
||||||
class MyRegisterForm(RegisterForm):
|
|
||||||
email = TextField('My Register Email Address Field')
|
|
||||||
|
|
||||||
APP_KWARGS = {
|
|
||||||
'login_form': MyLoginForm,
|
|
||||||
'register_form': MyRegisterForm,
|
|
||||||
}
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CONFIRMABLE': False,
|
|
||||||
'SECURITY_REGISTERABLE': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_login_view(self):
|
|
||||||
r = self._get('/login', follow_redirects=True)
|
|
||||||
self.assertIn("My Login Email Address Field", r.data)
|
|
||||||
|
|
||||||
def test_register(self):
|
|
||||||
r = self._get('/register', follow_redirects=True)
|
|
||||||
self.assertIn("My Register Email Address Field", r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class RecoverableExtendFormsTest(SecurityTest):
|
|
||||||
|
|
||||||
class MyForgotPasswordForm(ForgotPasswordForm):
|
|
||||||
email = TextField('My Forgot Password Email Address Field',
|
|
||||||
validators=[valid_user_email])
|
|
||||||
|
|
||||||
class MyResetPasswordForm(ResetPasswordForm):
|
|
||||||
submit = SubmitField("My Reset Password Submit Field")
|
|
||||||
|
|
||||||
APP_KWARGS = {
|
|
||||||
'forgot_password_form': MyForgotPasswordForm,
|
|
||||||
'reset_password_form': MyResetPasswordForm,
|
|
||||||
}
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_RECOVERABLE': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_forgot_password(self):
|
|
||||||
r = self._get('/reset', follow_redirects=True)
|
|
||||||
self.assertIn("My Forgot Password Email Address Field", r.data)
|
|
||||||
|
|
||||||
def test_reset_password(self):
|
|
||||||
with capture_reset_password_requests() as requests:
|
|
||||||
self._post('/reset', data=dict(email='joe@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
token = requests[0]['token']
|
|
||||||
r = self._get('/reset/' + token)
|
|
||||||
self.assertIn("My Reset Password Submit Field", r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordlessExtendFormsTest(SecurityTest):
|
|
||||||
|
|
||||||
class MyPasswordlessLoginForm(PasswordlessLoginForm):
|
|
||||||
email = TextField('My Passwordless Login Email Address Field')
|
|
||||||
|
|
||||||
APP_KWARGS = {
|
|
||||||
'passwordless_login_form': MyPasswordlessLoginForm,
|
|
||||||
}
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_PASSWORDLESS': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_passwordless_login(self):
|
|
||||||
r = self._get('/login', follow_redirects=True)
|
|
||||||
self.assertIn("My Passwordless Login Email Address Field", r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmableExtendFormsTest(SecurityTest):
|
|
||||||
|
|
||||||
class MyConfirmRegisterForm(ConfirmRegisterForm):
|
|
||||||
email = TextField('My Confirm Register Email Address Field')
|
|
||||||
|
|
||||||
class MySendConfirmationForm(SendConfirmationForm):
|
|
||||||
email = TextField('My Send Confirmation Email Address Field')
|
|
||||||
|
|
||||||
APP_KWARGS = {
|
|
||||||
'confirm_register_form': MyConfirmRegisterForm,
|
|
||||||
'send_confirmation_form': MySendConfirmationForm,
|
|
||||||
}
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CONFIRMABLE': True,
|
|
||||||
'SECURITY_REGISTERABLE': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_register(self):
|
|
||||||
r = self._get('/register', follow_redirects=True)
|
|
||||||
self.assertIn("My Confirm Register Email Address Field", r.data)
|
|
||||||
|
|
||||||
|
|
||||||
def test_send_confirmation(self):
|
|
||||||
r = self._get('/confirm', follow_redirects=True)
|
|
||||||
self.assertIn("My Send Confirmation Email Address Field", r.data)
|
|
||||||
|
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
conftest
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Test fixtures and what not
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flask import Flask, render_template
|
||||||
|
from flask_mail import Mail
|
||||||
|
|
||||||
|
from flask_security import Security, MongoEngineUserDatastore, SQLAlchemyUserDatastore, \
|
||||||
|
PeeweeUserDatastore, UserMixin, RoleMixin, http_auth_required, login_required, \
|
||||||
|
auth_token_required, auth_required, roles_required, roles_accepted
|
||||||
|
|
||||||
|
from utils import populate_data, Response
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def app(request):
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.response_class = Response
|
||||||
|
app.debug = True
|
||||||
|
app.config['SECRET_KEY'] = 'secret'
|
||||||
|
app.config['TESTING'] = True
|
||||||
|
app.config['LOGIN_DISABLED'] = False
|
||||||
|
app.config['WTF_CSRF_ENABLED'] = False
|
||||||
|
|
||||||
|
for opt in ['changeable', 'recoverable', 'registerable',
|
||||||
|
'trackable', 'passwordless', 'confirmable']:
|
||||||
|
app.config['SECURITY_' + opt.upper()] = opt in request.keywords
|
||||||
|
|
||||||
|
if 'settings' in request.keywords:
|
||||||
|
for key, value in request.keywords['settings'].kwargs.items():
|
||||||
|
app.config['SECURITY_' + key.upper()] = value
|
||||||
|
|
||||||
|
mail = Mail(app)
|
||||||
|
app.mail = mail
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('index.html', content='Home Page')
|
||||||
|
|
||||||
|
@app.route('/profile')
|
||||||
|
@login_required
|
||||||
|
def profile():
|
||||||
|
return render_template('index.html', content='Profile Page')
|
||||||
|
|
||||||
|
@app.route('/post_login')
|
||||||
|
@login_required
|
||||||
|
def post_login():
|
||||||
|
return render_template('index.html', content='Post Login')
|
||||||
|
|
||||||
|
@app.route('/http')
|
||||||
|
@http_auth_required
|
||||||
|
def http():
|
||||||
|
return 'HTTP Authentication'
|
||||||
|
|
||||||
|
@app.route('/http_custom_realm')
|
||||||
|
@http_auth_required('My Realm')
|
||||||
|
def http_custom_realm():
|
||||||
|
return render_template('index.html', content='HTTP Authentication')
|
||||||
|
|
||||||
|
@app.route('/token', methods=['GET', 'POST'])
|
||||||
|
@auth_token_required
|
||||||
|
def token():
|
||||||
|
return render_template('index.html', content='Token Authentication')
|
||||||
|
|
||||||
|
@app.route('/multi_auth')
|
||||||
|
@auth_required('session', 'token', 'basic')
|
||||||
|
def multi_auth():
|
||||||
|
return render_template('index.html', content='Session, Token, Basic auth')
|
||||||
|
|
||||||
|
@app.route('/post_logout')
|
||||||
|
def post_logout():
|
||||||
|
return render_template('index.html', content='Post Logout')
|
||||||
|
|
||||||
|
@app.route('/post_register')
|
||||||
|
def post_register():
|
||||||
|
return render_template('index.html', content='Post Register')
|
||||||
|
|
||||||
|
@app.route('/admin')
|
||||||
|
@roles_required('admin')
|
||||||
|
def admin():
|
||||||
|
return render_template('index.html', content='Admin Page')
|
||||||
|
|
||||||
|
@app.route('/admin_and_editor')
|
||||||
|
@roles_required('admin', 'editor')
|
||||||
|
def admin_and_editor():
|
||||||
|
return render_template('index.html', content='Admin and Editor Page')
|
||||||
|
|
||||||
|
@app.route('/admin_or_editor')
|
||||||
|
@roles_accepted('admin', 'editor')
|
||||||
|
def admin_or_editor():
|
||||||
|
return render_template('index.html', content='Admin or Editor Page')
|
||||||
|
|
||||||
|
@app.route('/unauthorized')
|
||||||
|
def unauthorized():
|
||||||
|
return render_template('unauthorized.html')
|
||||||
|
|
||||||
|
@app.route('/page1')
|
||||||
|
def page_1():
|
||||||
|
return 'Page 1'
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def mongoengine_datastore(request, app):
|
||||||
|
from flask_mongoengine import MongoEngine
|
||||||
|
|
||||||
|
db_name = 'flask_security_test_%s' % str(time.time()).replace('.', '_')
|
||||||
|
app.config['MONGODB_SETTINGS'] = {
|
||||||
|
'db': db_name,
|
||||||
|
'host': 'localhost',
|
||||||
|
'port': 27017,
|
||||||
|
'alias': db_name
|
||||||
|
}
|
||||||
|
|
||||||
|
db = MongoEngine(app)
|
||||||
|
|
||||||
|
class Role(db.Document, RoleMixin):
|
||||||
|
name = db.StringField(required=True, unique=True, max_length=80)
|
||||||
|
description = db.StringField(max_length=255)
|
||||||
|
meta = {"db_alias": db_name}
|
||||||
|
|
||||||
|
class User(db.Document, UserMixin):
|
||||||
|
email = db.StringField(unique=True, max_length=255)
|
||||||
|
username = db.StringField(max_length=255)
|
||||||
|
password = db.StringField(required=False, max_length=255)
|
||||||
|
last_login_at = db.DateTimeField()
|
||||||
|
current_login_at = db.DateTimeField()
|
||||||
|
last_login_ip = db.StringField(max_length=100)
|
||||||
|
current_login_ip = db.StringField(max_length=100)
|
||||||
|
login_count = db.IntField()
|
||||||
|
active = db.BooleanField(default=True)
|
||||||
|
confirmed_at = db.DateTimeField()
|
||||||
|
roles = db.ListField(db.ReferenceField(Role), default=[])
|
||||||
|
meta = {"db_alias": db_name}
|
||||||
|
|
||||||
|
request.addfinalizer(lambda: db.connection.drop_database(db_name))
|
||||||
|
|
||||||
|
return MongoEngineUserDatastore(db, User, Role)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def sqlalchemy_datastore(request, app, tmpdir):
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
f, path = tempfile.mkstemp(prefix='flask-security-test-db', suffix='.db', dir=str(tmpdir))
|
||||||
|
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + path
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
roles_users = db.Table(
|
||||||
|
'roles_users',
|
||||||
|
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
|
||||||
|
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
|
||||||
|
|
||||||
|
class Role(db.Model, RoleMixin):
|
||||||
|
id = db.Column(db.Integer(), primary_key=True)
|
||||||
|
name = db.Column(db.String(80), unique=True)
|
||||||
|
description = db.Column(db.String(255))
|
||||||
|
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
email = db.Column(db.String(255), unique=True)
|
||||||
|
username = db.Column(db.String(255))
|
||||||
|
password = db.Column(db.String(255))
|
||||||
|
last_login_at = db.Column(db.DateTime())
|
||||||
|
current_login_at = db.Column(db.DateTime())
|
||||||
|
last_login_ip = db.Column(db.String(100))
|
||||||
|
current_login_ip = db.Column(db.String(100))
|
||||||
|
login_count = db.Column(db.Integer)
|
||||||
|
active = db.Column(db.Boolean())
|
||||||
|
confirmed_at = db.Column(db.DateTime())
|
||||||
|
roles = db.relationship('Role', secondary=roles_users,
|
||||||
|
backref=db.backref('users', lazy='dynamic'))
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
request.addfinalizer(lambda: os.remove(path))
|
||||||
|
|
||||||
|
return SQLAlchemyUserDatastore(db, User, Role)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def peewee_datastore(request, app, tmpdir):
|
||||||
|
from peewee import TextField, DateTimeField, IntegerField, BooleanField, ForeignKeyField
|
||||||
|
from flask_peewee.db import Database
|
||||||
|
|
||||||
|
f, path = tempfile.mkstemp(prefix='flask-security-test-db', suffix='.db', dir=str(tmpdir))
|
||||||
|
|
||||||
|
app.config['DATABASE'] = {
|
||||||
|
'name': path,
|
||||||
|
'engine': 'peewee.SqliteDatabase'
|
||||||
|
}
|
||||||
|
|
||||||
|
db = Database(app)
|
||||||
|
|
||||||
|
class Role(db.Model, RoleMixin):
|
||||||
|
name = TextField(unique=True)
|
||||||
|
description = TextField(null=True)
|
||||||
|
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
email = TextField()
|
||||||
|
username = TextField()
|
||||||
|
password = TextField(null=True)
|
||||||
|
last_login_at = DateTimeField(null=True)
|
||||||
|
current_login_at = DateTimeField(null=True)
|
||||||
|
last_login_ip = TextField(null=True)
|
||||||
|
current_login_ip = TextField(null=True)
|
||||||
|
login_count = IntegerField(null=True)
|
||||||
|
active = BooleanField(default=True)
|
||||||
|
confirmed_at = DateTimeField(null=True)
|
||||||
|
|
||||||
|
class UserRoles(db.Model):
|
||||||
|
""" Peewee does not have built-in many-to-many support, so we have to
|
||||||
|
create this mapping class to link users to roles."""
|
||||||
|
user = ForeignKeyField(User, related_name='roles')
|
||||||
|
role = ForeignKeyField(Role, related_name='users')
|
||||||
|
name = property(lambda self: self.role.name)
|
||||||
|
description = property(lambda self: self.role.description)
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
for Model in (Role, User, UserRoles):
|
||||||
|
Model.create_table()
|
||||||
|
|
||||||
|
request.addfinalizer(lambda: os.remove(path))
|
||||||
|
|
||||||
|
return PeeweeUserDatastore(db, User, Role, UserRoles)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def sqlalchemy_app(app, sqlalchemy_datastore):
|
||||||
|
def create():
|
||||||
|
app.security = Security(app, datastore=sqlalchemy_datastore)
|
||||||
|
return app
|
||||||
|
return create
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def peewee_app(app, peewee_datastore):
|
||||||
|
def create():
|
||||||
|
app.security = Security(app, datastore=peewee_datastore)
|
||||||
|
return app
|
||||||
|
return create
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def mongoengine_app(app, mongoengine_datastore):
|
||||||
|
def create():
|
||||||
|
app.security = Security(app, datastore=mongoengine_datastore)
|
||||||
|
return app
|
||||||
|
return create
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def client(request, sqlalchemy_app):
|
||||||
|
app = sqlalchemy_app()
|
||||||
|
populate_data(app)
|
||||||
|
return app.test_client()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def get_message(app):
|
||||||
|
def fn(key, **kwargs):
|
||||||
|
rv = app.config['SECURITY_MSG_' + key][0] % kwargs
|
||||||
|
return rv.encode('utf-8')
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=['sqlalchemy', 'mongoengine', 'peewee'])
|
||||||
|
def datastore(request, sqlalchemy_datastore, mongoengine_datastore, peewee_datastore):
|
||||||
|
if request.param == 'sqlalchemy':
|
||||||
|
rv = sqlalchemy_datastore
|
||||||
|
elif request.param == 'mongoengine':
|
||||||
|
rv = mongoengine_datastore
|
||||||
|
elif request.param == 'peewee':
|
||||||
|
rv = peewee_datastore
|
||||||
|
return rv
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import simplejson as json
|
|
||||||
from cookielib import Cookie
|
|
||||||
|
|
||||||
from werkzeug.utils import parse_cookie
|
|
||||||
|
|
||||||
from tests import SecurityTest
|
|
||||||
|
|
||||||
|
|
||||||
def get_cookies(rv):
|
|
||||||
cookies = {}
|
|
||||||
for value in rv.headers.get_all("Set-Cookie"):
|
|
||||||
cookies.update(parse_cookie(value))
|
|
||||||
return cookies
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultSecurityTests(SecurityTest):
|
|
||||||
|
|
||||||
def test_instance(self):
|
|
||||||
self.assertIsNotNone(self.app)
|
|
||||||
self.assertIsNotNone(self.app.security)
|
|
||||||
self.assertIsNotNone(self.app.security.pwd_context)
|
|
||||||
|
|
||||||
def test_login_view(self):
|
|
||||||
r = self._get('/login')
|
|
||||||
self.assertIn('<h1>Login</h1>', r.data)
|
|
||||||
|
|
||||||
def test_authenticate(self):
|
|
||||||
r = self.authenticate()
|
|
||||||
self.assertIn('Hello matt@lp.com', r.data)
|
|
||||||
|
|
||||||
def test_unprovided_username(self):
|
|
||||||
r = self.authenticate("")
|
|
||||||
self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data)
|
|
||||||
|
|
||||||
def test_unprovided_password(self):
|
|
||||||
r = self.authenticate(password="")
|
|
||||||
self.assertIn(self.get_message('PASSWORD_NOT_PROVIDED'), r.data)
|
|
||||||
|
|
||||||
def test_invalid_user(self):
|
|
||||||
r = self.authenticate(email="bogus@bogus.com")
|
|
||||||
self.assertIn(self.get_message('USER_DOES_NOT_EXIST'), r.data)
|
|
||||||
|
|
||||||
def test_bad_password(self):
|
|
||||||
r = self.authenticate(password="bogus")
|
|
||||||
self.assertIn(self.get_message('INVALID_PASSWORD'), r.data)
|
|
||||||
|
|
||||||
def test_inactive_user(self):
|
|
||||||
r = self.authenticate("tiya@lp.com", "password")
|
|
||||||
self.assertIn(self.get_message('DISABLED_ACCOUNT'), r.data)
|
|
||||||
|
|
||||||
def test_logout(self):
|
|
||||||
self.authenticate()
|
|
||||||
r = self.logout()
|
|
||||||
self.assertIsHomePage(r.data)
|
|
||||||
|
|
||||||
def test_unauthorized_access(self):
|
|
||||||
r = self._get('/profile', follow_redirects=True)
|
|
||||||
self.assertIn('<li class="info">Please log in to access this page.</li>', r.data)
|
|
||||||
|
|
||||||
def test_authorized_access(self):
|
|
||||||
self.authenticate()
|
|
||||||
r = self._get("/profile")
|
|
||||||
self.assertIn('profile', r.data)
|
|
||||||
|
|
||||||
def test_valid_admin_role(self):
|
|
||||||
self.authenticate()
|
|
||||||
r = self._get("/admin")
|
|
||||||
self.assertIn('Admin Page', r.data)
|
|
||||||
|
|
||||||
def test_invalid_admin_role(self):
|
|
||||||
self.authenticate("joe@lp.com")
|
|
||||||
r = self._get("/admin", follow_redirects=True)
|
|
||||||
self.assertIsHomePage(r.data)
|
|
||||||
|
|
||||||
def test_roles_accepted(self):
|
|
||||||
for user in ("matt@lp.com", "joe@lp.com"):
|
|
||||||
self.authenticate(user)
|
|
||||||
r = self._get("/admin_or_editor")
|
|
||||||
self.assertIn('Admin or Editor Page', r.data)
|
|
||||||
self.logout()
|
|
||||||
|
|
||||||
self.authenticate("jill@lp.com")
|
|
||||||
r = self._get("/admin_or_editor", follow_redirects=True)
|
|
||||||
self.assertIsHomePage(r.data)
|
|
||||||
|
|
||||||
def test_unauthenticated_role_required(self):
|
|
||||||
r = self._get('/admin', follow_redirects=True)
|
|
||||||
self.assertIn(self.get_message('UNAUTHORIZED'), r.data)
|
|
||||||
|
|
||||||
def test_multiple_role_required(self):
|
|
||||||
for user in ("matt@lp.com", "joe@lp.com"):
|
|
||||||
self.authenticate(user)
|
|
||||||
r = self._get("/admin_and_editor", follow_redirects=True)
|
|
||||||
self.assertIsHomePage(r.data)
|
|
||||||
self._get('/logout')
|
|
||||||
|
|
||||||
self.authenticate('dave@lp.com')
|
|
||||||
r = self._get("/admin_and_editor", follow_redirects=True)
|
|
||||||
self.assertIn('Admin and Editor Page', r.data)
|
|
||||||
|
|
||||||
def test_ok_json_auth(self):
|
|
||||||
r = self.json_authenticate()
|
|
||||||
data = json.loads(r.data)
|
|
||||||
self.assertEquals(data['meta']['code'], 200)
|
|
||||||
self.assertIn('authentication_token', data['response']['user'])
|
|
||||||
|
|
||||||
def test_invalid_json_auth(self):
|
|
||||||
r = self.json_authenticate(password='junk')
|
|
||||||
self.assertIn('"code": 400', r.data)
|
|
||||||
|
|
||||||
def test_token_auth_via_querystring_valid_token(self):
|
|
||||||
r = self.json_authenticate()
|
|
||||||
data = json.loads(r.data)
|
|
||||||
token = data['response']['user']['authentication_token']
|
|
||||||
r = self._get('/token?auth_token=' + token)
|
|
||||||
self.assertIn('Token Authentication', r.data)
|
|
||||||
|
|
||||||
def test_token_auth_via_header_valid_token(self):
|
|
||||||
r = self.json_authenticate()
|
|
||||||
data = json.loads(r.data)
|
|
||||||
token = data['response']['user']['authentication_token']
|
|
||||||
headers = {"Authentication-Token": token}
|
|
||||||
r = self._get('/token', headers=headers)
|
|
||||||
self.assertIn('Token Authentication', r.data)
|
|
||||||
|
|
||||||
def test_token_auth_via_querystring_invalid_token(self):
|
|
||||||
r = self._get('/token?auth_token=X')
|
|
||||||
self.assertEqual(401, r.status_code)
|
|
||||||
|
|
||||||
def test_token_auth_via_header_invalid_token(self):
|
|
||||||
r = self._get('/token', headers={"Authentication-Token": 'X'})
|
|
||||||
self.assertEqual(401, r.status_code)
|
|
||||||
|
|
||||||
def test_http_auth(self):
|
|
||||||
r = self._get('/http', headers={
|
|
||||||
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password")
|
|
||||||
})
|
|
||||||
self.assertIn('HTTP Authentication', r.data)
|
|
||||||
|
|
||||||
def test_http_auth_no_authorization(self):
|
|
||||||
r = self._get('/http', headers={})
|
|
||||||
self.assertIn('<h1>Unauthorized</h1>', r.data)
|
|
||||||
self.assertIn('WWW-Authenticate', r.headers)
|
|
||||||
self.assertEquals('Basic realm="Login Required"',
|
|
||||||
r.headers['WWW-Authenticate'])
|
|
||||||
|
|
||||||
def test_invalid_http_auth_invalid_username(self):
|
|
||||||
r = self._get('/http', headers={
|
|
||||||
'Authorization': 'Basic ' + base64.b64encode("bogus:bogus")
|
|
||||||
})
|
|
||||||
self.assertIn('<h1>Unauthorized</h1>', r.data)
|
|
||||||
self.assertIn('WWW-Authenticate', r.headers)
|
|
||||||
self.assertEquals('Basic realm="Login Required"',
|
|
||||||
r.headers['WWW-Authenticate'])
|
|
||||||
|
|
||||||
def test_invalid_http_auth_bad_password(self):
|
|
||||||
r = self._get('/http', headers={
|
|
||||||
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
|
|
||||||
})
|
|
||||||
self.assertIn('<h1>Unauthorized</h1>', r.data)
|
|
||||||
self.assertIn('WWW-Authenticate', r.headers)
|
|
||||||
self.assertEquals('Basic realm="Login Required"',
|
|
||||||
r.headers['WWW-Authenticate'])
|
|
||||||
|
|
||||||
def test_custom_http_auth_realm(self):
|
|
||||||
r = self._get('/http_custom_realm', headers={
|
|
||||||
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
|
|
||||||
})
|
|
||||||
self.assertIn('<h1>Unauthorized</h1>', r.data)
|
|
||||||
self.assertIn('WWW-Authenticate', r.headers)
|
|
||||||
self.assertEquals('Basic realm="My Realm"',
|
|
||||||
r.headers['WWW-Authenticate'])
|
|
||||||
|
|
||||||
def test_multi_auth_basic(self):
|
|
||||||
r = self._get('/multi_auth', headers={
|
|
||||||
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password")
|
|
||||||
})
|
|
||||||
self.assertIn('Basic', r.data)
|
|
||||||
|
|
||||||
def test_multi_auth_token(self):
|
|
||||||
r = self.json_authenticate()
|
|
||||||
data = json.loads(r.data)
|
|
||||||
token = data['response']['user']['authentication_token']
|
|
||||||
r = self._get('/multi_auth?auth_token=' + token)
|
|
||||||
self.assertIn('Token', r.data)
|
|
||||||
|
|
||||||
def test_multi_auth_session(self):
|
|
||||||
self.authenticate()
|
|
||||||
r = self._get('/multi_auth')
|
|
||||||
self.assertIn('Session', r.data)
|
|
||||||
|
|
||||||
def test_user_deleted_during_session_reverts_to_anonymous_user(self):
|
|
||||||
self.authenticate()
|
|
||||||
|
|
||||||
with self.app.test_request_context('/'):
|
|
||||||
user = self.app.security.datastore.find_user(email='matt@lp.com')
|
|
||||||
self.app.security.datastore.delete_user(user)
|
|
||||||
self.app.security.datastore.commit()
|
|
||||||
|
|
||||||
r = self._get('/')
|
|
||||||
self.assertNotIn('Hello matt@lp.com', r.data)
|
|
||||||
|
|
||||||
def test_remember_token(self):
|
|
||||||
r = self.authenticate(follow_redirects=False)
|
|
||||||
self.client.cookie_jar.clear_session_cookies()
|
|
||||||
r = self._get('/profile')
|
|
||||||
self.assertIn('profile', r.data)
|
|
||||||
|
|
||||||
def test_token_loader_does_not_fail_with_invalid_token(self):
|
|
||||||
c = Cookie(version=0, name='remember_token', value='None', port=None,
|
|
||||||
port_specified=False, domain='www.example.com',
|
|
||||||
domain_specified=False, domain_initial_dot=False, path='/',
|
|
||||||
path_specified=True, secure=False, expires=None,
|
|
||||||
discard=True, comment=None, comment_url=None,
|
|
||||||
rest={'HttpOnly': None}, rfc2109=False)
|
|
||||||
|
|
||||||
self.client.cookie_jar.set_cookie(c)
|
|
||||||
r = self._get('/')
|
|
||||||
self.assertNotIn('BadSignature', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class MongoEngineSecurityTests(DefaultSecurityTests):
|
|
||||||
|
|
||||||
def _create_app(self, auth_config, **kwargs):
|
|
||||||
from tests.test_app.mongoengine import create_app
|
|
||||||
return create_app(auth_config, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class PeeweeSecurityTests(DefaultSecurityTests):
|
|
||||||
|
|
||||||
def _create_app(self, auth_config, **kwargs):
|
|
||||||
from tests.test_app.peewee_app import create_app
|
|
||||||
return create_app(auth_config, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultDatastoreTests(SecurityTest):
|
|
||||||
|
|
||||||
def test_add_role_to_user(self):
|
|
||||||
r = self._get('/coverage/add_role_to_user')
|
|
||||||
self.assertIn('success', r.data)
|
|
||||||
|
|
||||||
def test_remove_role_from_user(self):
|
|
||||||
r = self._get('/coverage/remove_role_from_user')
|
|
||||||
self.assertIn('success', r.data)
|
|
||||||
|
|
||||||
def test_activate_user(self):
|
|
||||||
r = self._get('/coverage/activate_user')
|
|
||||||
self.assertIn('success', r.data)
|
|
||||||
|
|
||||||
def test_deactivate_user(self):
|
|
||||||
r = self._get('/coverage/deactivate_user')
|
|
||||||
self.assertIn('success', r.data)
|
|
||||||
|
|
||||||
def test_invalid_role(self):
|
|
||||||
r = self._get('/coverage/invalid_role')
|
|
||||||
self.assertIn('success', r.data)
|
|
||||||
|
|
||||||
|
|
||||||
class MongoEngineDatastoreTests(DefaultDatastoreTests):
|
|
||||||
|
|
||||||
def _create_app(self, auth_config, **kwargs):
|
|
||||||
from tests.test_app.mongoengine import create_app
|
|
||||||
return create_app(auth_config, **kwargs)
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
from flask_security.utils import capture_registrations, \
|
|
||||||
capture_reset_password_requests, capture_signals
|
|
||||||
from flask_security.signals import user_registered, user_confirmed, \
|
|
||||||
confirm_instructions_sent, login_instructions_sent, \
|
|
||||||
password_reset, password_changed, reset_password_instructions_sent
|
|
||||||
from tests import SecurityTest
|
|
||||||
|
|
||||||
|
|
||||||
def compare_user(a, b):
|
|
||||||
"""Helper to compare two users."""
|
|
||||||
return a.id == b.id and a.email == b.email and a.password == b.password
|
|
||||||
|
|
||||||
|
|
||||||
class SignalTest(SecurityTest):
|
|
||||||
|
|
||||||
def _create_app(self, auth_config, **kwargs):
|
|
||||||
from tests.test_app.mongoengine import create_app
|
|
||||||
return create_app(auth_config, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class RegisterableSignalsTests(SignalTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CONFIRMABLE': True,
|
|
||||||
'SECURITY_REGISTERABLE': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_register(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self.register(e)
|
|
||||||
user = self.app.security.datastore.find_user(email='dude@lp.com')
|
|
||||||
self.assertEqual(mocks.signals_sent(), set([user_registered]))
|
|
||||||
calls = mocks[user_registered]
|
|
||||||
self.assertEqual(len(calls), 1)
|
|
||||||
args, kwargs = calls[0]
|
|
||||||
self.assertTrue(compare_user(kwargs['user'], user))
|
|
||||||
self.assertIn('confirm_token', kwargs)
|
|
||||||
self.assertEqual(args[0], self.app)
|
|
||||||
|
|
||||||
def test_register_without_password(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self.register(e, password='')
|
|
||||||
self.assertEqual(mocks.signals_sent(), set())
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmableSignalsTests(SignalTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CONFIRMABLE': True,
|
|
||||||
'SECURITY_REGISTERABLE': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_confirm(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
with capture_registrations() as registrations:
|
|
||||||
self.register(e)
|
|
||||||
token = registrations[0]['confirm_token']
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self.client.get('/confirm/' + token, follow_redirects=True)
|
|
||||||
user = self.app.security.datastore.find_user(email='dude@lp.com')
|
|
||||||
self.assertTrue(mocks.signals_sent(), set([user_confirmed]))
|
|
||||||
calls = mocks[user_confirmed]
|
|
||||||
self.assertEqual(len(calls), 1)
|
|
||||||
args, kwargs = calls[0]
|
|
||||||
self.assertEqual(args[0], self.app)
|
|
||||||
self.assertTrue(compare_user(kwargs['user'], user))
|
|
||||||
|
|
||||||
def test_confirm_bad_token(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
with capture_registrations():
|
|
||||||
self.register(e)
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self.client.get('/confirm/bogus', follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set())
|
|
||||||
|
|
||||||
def test_confirm_twice(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
with capture_registrations() as registrations:
|
|
||||||
self.register(e)
|
|
||||||
token = registrations[0]['confirm_token']
|
|
||||||
self.client.get('/confirm/' + token, follow_redirects=True)
|
|
||||||
self.logout()
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self.client.get('/confirm/' + token, follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set([user_confirmed]))
|
|
||||||
# TODO: is that the desired behaviour?
|
|
||||||
|
|
||||||
def test_resend_confirmation(self):
|
|
||||||
e = 'dude@lp.com'
|
|
||||||
self.register(e)
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self._post('/confirm', data={'email': e})
|
|
||||||
user = self.app.security.datastore.find_user(email='dude@lp.com')
|
|
||||||
self.assertEqual(mocks.signals_sent(), set([confirm_instructions_sent]))
|
|
||||||
calls = mocks[confirm_instructions_sent]
|
|
||||||
self.assertEqual(len(calls), 1)
|
|
||||||
args, kwargs = calls[0]
|
|
||||||
self.assertTrue(compare_user(kwargs['user'], user))
|
|
||||||
self.assertEqual(args[0], self.app)
|
|
||||||
|
|
||||||
def test_send_confirmation_bad_email(self):
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self._post('/confirm', data=dict(email='bogus@bogus.com'))
|
|
||||||
self.assertEqual(mocks.signals_sent(), set())
|
|
||||||
|
|
||||||
|
|
||||||
class RecoverableSignalsTests(SignalTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_RECOVERABLE': True,
|
|
||||||
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
|
|
||||||
'SECURITY_POST_FORGOT_VIEW': '/'
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_reset_password_request(self):
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self._post('/reset', data=dict(email='joe@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set([reset_password_instructions_sent]))
|
|
||||||
user = self.app.security.datastore.find_user(email='joe@lp.com')
|
|
||||||
calls = mocks[reset_password_instructions_sent]
|
|
||||||
self.assertEqual(len(calls), 1)
|
|
||||||
args, kwargs = calls[0]
|
|
||||||
self.assertTrue(compare_user(kwargs['user'], user))
|
|
||||||
self.assertIn('token', kwargs)
|
|
||||||
self.assertEqual(args[0], self.app)
|
|
||||||
|
|
||||||
def test_reset_password(self):
|
|
||||||
with capture_reset_password_requests() as requests:
|
|
||||||
self._post('/reset', data=dict(email='joe@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
token = requests[0]['token']
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
data = dict(password='newpassword', password_confirm='newpassword')
|
|
||||||
self._post('/reset/' + token, data, follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set([password_reset]))
|
|
||||||
user = self.app.security.datastore.find_user(email='joe@lp.com')
|
|
||||||
calls = mocks[password_reset]
|
|
||||||
self.assertEqual(len(calls), 1)
|
|
||||||
args, kwargs = calls[0]
|
|
||||||
self.assertTrue(compare_user(kwargs['user'], user))
|
|
||||||
self.assertEqual(args[0], self.app)
|
|
||||||
|
|
||||||
def test_reset_password_invalid_emails(self):
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self._post('/reset', data=dict(email='nobody@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set())
|
|
||||||
|
|
||||||
def test_reset_password_invalid_token(self):
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
data = dict(password='newpassword', password_confirm='newpassword')
|
|
||||||
self._post('/reset/bogus', data, follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set())
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeableSignalsTests(SignalTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_CHANGEABLE': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_change_password(self):
|
|
||||||
self.authenticate('joe@lp.com')
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
with self.client as client:
|
|
||||||
client.post('/change',
|
|
||||||
data=dict(password='password',
|
|
||||||
new_password='newpassword',
|
|
||||||
new_password_confirm='newpassword',
|
|
||||||
csrf_token=self.csrf_token))
|
|
||||||
self.assertEqual(mocks.signals_sent(), set([password_changed]))
|
|
||||||
user = self.app.security.datastore.find_user(email='joe@lp.com')
|
|
||||||
calls = mocks[password_changed]
|
|
||||||
self.assertEqual(len(calls), 1)
|
|
||||||
args, kwargs = calls[0]
|
|
||||||
self.assertTrue(compare_user(args[0], user))
|
|
||||||
self.assertEqual(kwargs['app'], self.app)
|
|
||||||
|
|
||||||
def test_change_password_invalid_password(self):
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self.client.post('/change',
|
|
||||||
data=dict(password='notpassword',
|
|
||||||
new_password='newpassword',
|
|
||||||
new_password_confirm='newpassword'),
|
|
||||||
follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set())
|
|
||||||
|
|
||||||
def test_change_password_bad_password(self):
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self.client.post('/change',
|
|
||||||
data=dict(password='notpassword',
|
|
||||||
new_password='a',
|
|
||||||
new_password_confirm='a'),
|
|
||||||
follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set())
|
|
||||||
|
|
||||||
def test_change_password_mismatch_password(self):
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self.client.post('/change',
|
|
||||||
data=dict(password='password',
|
|
||||||
new_password='newpassword',
|
|
||||||
new_password_confirm='notnewpassword'),
|
|
||||||
follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set())
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordlessTests(SignalTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_PASSWORDLESS': True
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_login_request_for_inactive_user(self):
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self._post('/login', data=dict(email='tiya@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set())
|
|
||||||
|
|
||||||
def test_login_request_for_invalid_email(self):
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self._post('/login', data=dict(email='nobody@lp.com'),
|
|
||||||
follow_redirects=True)
|
|
||||||
self.assertEqual(mocks.signals_sent(), set())
|
|
||||||
|
|
||||||
def test_request_login_token_sends_email_and_can_login(self):
|
|
||||||
e = 'matt@lp.com'
|
|
||||||
|
|
||||||
with capture_signals() as mocks:
|
|
||||||
self._post('/login', data=dict(email=e), follow_redirects=True)
|
|
||||||
|
|
||||||
self.assertEqual(mocks.signals_sent(), set([login_instructions_sent]))
|
|
||||||
user = self.app.security.datastore.find_user(email='matt@lp.com')
|
|
||||||
calls = mocks[login_instructions_sent]
|
|
||||||
self.assertEqual(len(calls), 1)
|
|
||||||
args, kwargs = calls[0]
|
|
||||||
self.assertTrue(compare_user(kwargs['user'], user))
|
|
||||||
self.assertIn('login_token', kwargs)
|
|
||||||
self.assertEqual(args[0], self.app)
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{%- if current_user.is_authenticated() -%}
|
{%- if current_user.is_authenticated -%}
|
||||||
<p>Hello {{ current_user.email }}</p>
|
<p>Hello {{ current_user.email }}</p>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<ul>
|
<ul>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<li><a href="{{ url_for('admin_or_editor') }}">Admin or Editor</a></li>
|
<li><a href="{{ url_for('admin_or_editor') }}">Admin or Editor</a></li>
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
<li>
|
<li>
|
||||||
{%- if current_user.is_authenticated() -%}
|
{%- if current_user.is_authenticated -%}
|
||||||
<a href="{{ url_for('security.logout') }}">Log out</a>
|
<a href="{{ url_for('security.logout') }}">Log out</a>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
<a href="{{ url_for('security.login') }}">Log in</a>
|
<a href="{{ url_for('security.login') }}">Log in</a>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
CUSTOM CHANGE PASSWORD
|
||||||
|
{{ global }}
|
||||||
|
{{ foo }}
|
||||||
+2
@@ -1 +1,3 @@
|
|||||||
CUSTOM FORGOT PASSWORD
|
CUSTOM FORGOT PASSWORD
|
||||||
|
{{ global }}
|
||||||
|
{{ foo }}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
CUSTOM LOGIN USER
|
||||||
|
{{ global }}
|
||||||
|
{{ foo }}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
CUSTOM REGISTER USER
|
||||||
|
{{ global }}
|
||||||
|
{{ foo }}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
CUSTOM RESET PASSWORD
|
||||||
|
{{ global }}
|
||||||
|
{{ foo }}
|
||||||
+2
@@ -1 +1,3 @@
|
|||||||
CUSTOM SEND CONFIRMATION
|
CUSTOM SEND CONFIRMATION
|
||||||
|
{{ global }}
|
||||||
|
{{ foo }}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
CUSTOM SEND LOGIN
|
||||||
|
{{ global }}
|
||||||
|
{{ foo }}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
CUSTOM RESET INSTRUCTIONS
|
||||||
|
{{ global }}
|
||||||
|
{{ foo }}
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from flask import Flask, render_template, current_app
|
|
||||||
from flask.ext.mail import Mail
|
|
||||||
from flask.ext.security import login_required, roles_required, roles_accepted
|
|
||||||
from flask.ext.security.decorators import http_auth_required, \
|
|
||||||
auth_token_required, auth_required
|
|
||||||
from flask.ext.security.utils import encrypt_password
|
|
||||||
from werkzeug.local import LocalProxy
|
|
||||||
|
|
||||||
ds = LocalProxy(lambda: current_app.extensions['security'].datastore)
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(config):
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.debug = True
|
|
||||||
app.config['SECRET_KEY'] = 'secret'
|
|
||||||
app.config['TESTING'] = True
|
|
||||||
|
|
||||||
for key, value in config.items():
|
|
||||||
app.config[key] = value
|
|
||||||
|
|
||||||
mail = Mail(app)
|
|
||||||
app.extensions['mail'] = mail
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
return render_template('index.html', content='Home Page')
|
|
||||||
|
|
||||||
@app.route('/profile')
|
|
||||||
@login_required
|
|
||||||
def profile():
|
|
||||||
return render_template('index.html', content='Profile Page')
|
|
||||||
|
|
||||||
@app.route('/post_login')
|
|
||||||
@login_required
|
|
||||||
def post_login():
|
|
||||||
return render_template('index.html', content='Post Login')
|
|
||||||
|
|
||||||
@app.route('/http')
|
|
||||||
@http_auth_required
|
|
||||||
def http():
|
|
||||||
return 'HTTP Authentication'
|
|
||||||
|
|
||||||
@app.route('/http_custom_realm')
|
|
||||||
@http_auth_required('My Realm')
|
|
||||||
def http_custom_realm():
|
|
||||||
return render_template('index.html', content='HTTP Authentication')
|
|
||||||
|
|
||||||
@app.route('/token')
|
|
||||||
@auth_token_required
|
|
||||||
def token():
|
|
||||||
return render_template('index.html', content='Token Authentication')
|
|
||||||
|
|
||||||
@app.route('/multi_auth')
|
|
||||||
@auth_required('session', 'token', 'basic')
|
|
||||||
def multi_auth():
|
|
||||||
return render_template('index.html', content='Session, Token, Basic auth')
|
|
||||||
|
|
||||||
@app.route('/post_logout')
|
|
||||||
def post_logout():
|
|
||||||
return render_template('index.html', content='Post Logout')
|
|
||||||
|
|
||||||
@app.route('/post_register')
|
|
||||||
def post_register():
|
|
||||||
return render_template('index.html', content='Post Register')
|
|
||||||
|
|
||||||
@app.route('/admin')
|
|
||||||
@roles_required('admin')
|
|
||||||
def admin():
|
|
||||||
return render_template('index.html', content='Admin Page')
|
|
||||||
|
|
||||||
@app.route('/admin_and_editor')
|
|
||||||
@roles_required('admin', 'editor')
|
|
||||||
def admin_and_editor():
|
|
||||||
return render_template('index.html', content='Admin and Editor Page')
|
|
||||||
|
|
||||||
@app.route('/admin_or_editor')
|
|
||||||
@roles_accepted('admin', 'editor')
|
|
||||||
def admin_or_editor():
|
|
||||||
return render_template('index.html', content='Admin or Editor Page')
|
|
||||||
|
|
||||||
@app.route('/unauthorized')
|
|
||||||
def unauthorized():
|
|
||||||
return render_template('unauthorized.html')
|
|
||||||
|
|
||||||
@app.route('/coverage/add_role_to_user')
|
|
||||||
def add_role_to_user():
|
|
||||||
u = ds.find_user(email='joe@lp.com')
|
|
||||||
r = ds.find_role('admin')
|
|
||||||
ds.add_role_to_user(u, r)
|
|
||||||
return 'success'
|
|
||||||
|
|
||||||
@app.route('/coverage/remove_role_from_user')
|
|
||||||
def remove_role_from_user():
|
|
||||||
u = ds.find_user(email='matt@lp.com')
|
|
||||||
ds.remove_role_from_user(u, 'admin')
|
|
||||||
return 'success'
|
|
||||||
|
|
||||||
@app.route('/coverage/deactivate_user')
|
|
||||||
def deactivate_user():
|
|
||||||
u = ds.find_user(email='matt@lp.com')
|
|
||||||
ds.deactivate_user(u)
|
|
||||||
return 'success'
|
|
||||||
|
|
||||||
@app.route('/coverage/activate_user')
|
|
||||||
def activate_user():
|
|
||||||
u = ds.find_user(email='tiya@lp.com')
|
|
||||||
ds.activate_user(u)
|
|
||||||
return 'success'
|
|
||||||
|
|
||||||
@app.route('/coverage/invalid_role')
|
|
||||||
def invalid_role():
|
|
||||||
return 'success' if ds.find_role('bogus') is None else 'failure'
|
|
||||||
|
|
||||||
@app.route('/page1')
|
|
||||||
def page_1():
|
|
||||||
return 'Page 1'
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
def create_roles():
|
|
||||||
for role in ('admin', 'editor', 'author'):
|
|
||||||
ds.create_role(name=role)
|
|
||||||
ds.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def create_users(count=None):
|
|
||||||
users = [('matt@lp.com', 'password', ['admin'], True),
|
|
||||||
('joe@lp.com', 'password', ['editor'], True),
|
|
||||||
('dave@lp.com', 'password', ['admin', 'editor'], True),
|
|
||||||
('jill@lp.com', 'password', ['author'], True),
|
|
||||||
('tiya@lp.com', 'password', [], False)]
|
|
||||||
count = count or len(users)
|
|
||||||
|
|
||||||
for u in users[:count]:
|
|
||||||
pw = encrypt_password(u[1])
|
|
||||||
ds.create_user(email=u[0], password=pw,
|
|
||||||
roles=u[2], active=u[3])
|
|
||||||
ds.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def populate_data(user_count=None):
|
|
||||||
create_roles()
|
|
||||||
create_users(user_count)
|
|
||||||
|
|
||||||
|
|
||||||
def add_context_processors(s):
|
|
||||||
@s.context_processor
|
|
||||||
def for_all():
|
|
||||||
return dict()
|
|
||||||
|
|
||||||
@s.forgot_password_context_processor
|
|
||||||
def forgot_password():
|
|
||||||
return dict()
|
|
||||||
|
|
||||||
@s.login_context_processor
|
|
||||||
def login():
|
|
||||||
return dict()
|
|
||||||
|
|
||||||
@s.register_context_processor
|
|
||||||
def register():
|
|
||||||
return dict()
|
|
||||||
|
|
||||||
@s.reset_password_context_processor
|
|
||||||
def reset_password():
|
|
||||||
return dict()
|
|
||||||
|
|
||||||
@s.send_confirmation_context_processor
|
|
||||||
def send_confirmation():
|
|
||||||
return dict()
|
|
||||||
|
|
||||||
@s.send_login_context_processor
|
|
||||||
def send_login():
|
|
||||||
return dict()
|
|
||||||
|
|
||||||
@s.mail_context_processor
|
|
||||||
def mail():
|
|
||||||
return dict()
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
sys.path.pop(0)
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
|
|
||||||
from flask.ext.mongoengine import MongoEngine
|
|
||||||
from flask.ext.security import Security, UserMixin, RoleMixin, \
|
|
||||||
MongoEngineUserDatastore
|
|
||||||
|
|
||||||
from tests.test_app import create_app as create_base_app, populate_data, \
|
|
||||||
add_context_processors
|
|
||||||
|
|
||||||
def create_app(config, **kwargs):
|
|
||||||
app = create_base_app(config)
|
|
||||||
|
|
||||||
app.config['MONGODB_SETTINGS'] = dict(
|
|
||||||
db='flask_security_test',
|
|
||||||
host='localhost',
|
|
||||||
port=27017
|
|
||||||
)
|
|
||||||
|
|
||||||
db = MongoEngine(app)
|
|
||||||
|
|
||||||
class Role(db.Document, RoleMixin):
|
|
||||||
name = db.StringField(required=True, unique=True, max_length=80)
|
|
||||||
description = db.StringField(max_length=255)
|
|
||||||
|
|
||||||
class User(db.Document, UserMixin):
|
|
||||||
email = db.StringField(unique=True, max_length=255)
|
|
||||||
password = db.StringField(required=True, max_length=255)
|
|
||||||
last_login_at = db.DateTimeField()
|
|
||||||
current_login_at = db.DateTimeField()
|
|
||||||
last_login_ip = db.StringField(max_length=100)
|
|
||||||
current_login_ip = db.StringField(max_length=100)
|
|
||||||
login_count = db.IntField()
|
|
||||||
active = db.BooleanField(default=True)
|
|
||||||
confirmed_at = db.DateTimeField()
|
|
||||||
roles = db.ListField(db.ReferenceField(Role), default=[])
|
|
||||||
|
|
||||||
@app.before_first_request
|
|
||||||
def before_first_request():
|
|
||||||
User.drop_collection()
|
|
||||||
Role.drop_collection()
|
|
||||||
populate_data(app.config.get('USER_COUNT', None))
|
|
||||||
|
|
||||||
app.security = Security(app, datastore=MongoEngineUserDatastore(db, User, Role), **kwargs)
|
|
||||||
|
|
||||||
add_context_processors(app.security)
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
create_app({}).run()
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
sys.path.pop(0)
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
|
|
||||||
from flask_peewee.db import Database
|
|
||||||
from peewee import *
|
|
||||||
from flask.ext.security import Security, UserMixin, RoleMixin, \
|
|
||||||
PeeweeUserDatastore
|
|
||||||
|
|
||||||
from tests.test_app import create_app as create_base_app, populate_data, \
|
|
||||||
add_context_processors
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(config, **kwargs):
|
|
||||||
app = create_base_app(config)
|
|
||||||
app.config['DATABASE'] = {
|
|
||||||
'name': 'example2.db',
|
|
||||||
'engine': 'peewee.SqliteDatabase'
|
|
||||||
}
|
|
||||||
db = Database(app)
|
|
||||||
|
|
||||||
class Role(db.Model, RoleMixin):
|
|
||||||
name = TextField(unique=True)
|
|
||||||
description = TextField(null=True)
|
|
||||||
|
|
||||||
class User(db.Model, UserMixin):
|
|
||||||
email = TextField()
|
|
||||||
password = TextField()
|
|
||||||
last_login_at = DateTimeField(null=True)
|
|
||||||
current_login_at = DateTimeField(null=True)
|
|
||||||
last_login_ip = TextField(null=True)
|
|
||||||
current_login_ip = TextField(null=True)
|
|
||||||
login_count = IntegerField(null=True)
|
|
||||||
active = BooleanField(default=True)
|
|
||||||
confirmed_at = DateTimeField(null=True)
|
|
||||||
|
|
||||||
class UserRoles(db.Model):
|
|
||||||
""" Peewee does not have built-in many-to-many support, so we have to
|
|
||||||
create this mapping class to link users to roles."""
|
|
||||||
user = ForeignKeyField(User, related_name='roles')
|
|
||||||
role = ForeignKeyField(Role, related_name='users')
|
|
||||||
name = property(lambda self: self.role.name)
|
|
||||||
description = property(lambda self: self.role.description)
|
|
||||||
|
|
||||||
@app.before_first_request
|
|
||||||
def before_first_request():
|
|
||||||
for Model in (Role, User, UserRoles):
|
|
||||||
Model.drop_table(fail_silently=True)
|
|
||||||
Model.create_table(fail_silently=True)
|
|
||||||
populate_data(app.config.get('USER_COUNT', None))
|
|
||||||
|
|
||||||
app.security = Security(app, datastore=PeeweeUserDatastore(db, User, Role, UserRoles), **kwargs)
|
|
||||||
|
|
||||||
add_context_processors(app.security)
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
create_app({}).run()
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
sys.path.pop(0)
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
|
|
||||||
|
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
|
||||||
from flask.ext.security import Security, UserMixin, RoleMixin, \
|
|
||||||
SQLAlchemyUserDatastore
|
|
||||||
|
|
||||||
from tests.test_app import create_app as create_base_app, populate_data, \
|
|
||||||
add_context_processors
|
|
||||||
|
|
||||||
def create_app(config, **kwargs):
|
|
||||||
app = create_base_app(config)
|
|
||||||
|
|
||||||
#app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
|
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
|
||||||
|
|
||||||
roles_users = db.Table('roles_users',
|
|
||||||
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
|
|
||||||
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
|
|
||||||
|
|
||||||
class Role(db.Model, RoleMixin):
|
|
||||||
id = db.Column(db.Integer(), primary_key=True)
|
|
||||||
name = db.Column(db.String(80), unique=True)
|
|
||||||
description = db.Column(db.String(255))
|
|
||||||
|
|
||||||
class User(db.Model, UserMixin):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
email = db.Column(db.String(255), unique=True)
|
|
||||||
password = db.Column(db.String(255))
|
|
||||||
last_login_at = db.Column(db.DateTime())
|
|
||||||
current_login_at = db.Column(db.DateTime())
|
|
||||||
last_login_ip = db.Column(db.String(100))
|
|
||||||
current_login_ip = db.Column(db.String(100))
|
|
||||||
login_count = db.Column(db.Integer)
|
|
||||||
active = db.Column(db.Boolean())
|
|
||||||
confirmed_at = db.Column(db.DateTime())
|
|
||||||
roles = db.relationship('Role', secondary=roles_users,
|
|
||||||
backref=db.backref('users', lazy='dynamic'))
|
|
||||||
|
|
||||||
@app.before_first_request
|
|
||||||
def before_first_request():
|
|
||||||
db.drop_all()
|
|
||||||
db.create_all()
|
|
||||||
populate_data(app.config.get('USER_COUNT', None))
|
|
||||||
|
|
||||||
app.security = Security(app, datastore=SQLAlchemyUserDatastore(db, User, Role), **kwargs)
|
|
||||||
|
|
||||||
add_context_processors(app.security)
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
create_app({}).run()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
CUSTOM LOGIN USER
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
CUSTOM REGISTER USER
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
CUSTOM RESET PASSWORD
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
CUSTOM SEND LOGIN
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_changeable
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Changeable tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_security.core import UserMixin
|
||||||
|
from flask_security.signals import password_changed
|
||||||
|
|
||||||
|
from utils import authenticate
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.changeable()
|
||||||
|
|
||||||
|
|
||||||
|
def test_recoverable_flag(app, client, get_message):
|
||||||
|
recorded = []
|
||||||
|
|
||||||
|
@password_changed.connect_via(app)
|
||||||
|
def on_password_changed(app, user):
|
||||||
|
assert isinstance(app, Flask)
|
||||||
|
assert isinstance(user, UserMixin)
|
||||||
|
recorded.append(user)
|
||||||
|
|
||||||
|
authenticate(client)
|
||||||
|
|
||||||
|
# Test change view
|
||||||
|
response = client.get('/change', follow_redirects=True)
|
||||||
|
assert b'Change password' in response.data
|
||||||
|
|
||||||
|
# Test wrong original password
|
||||||
|
response = client.post('/change', data={
|
||||||
|
'password': 'notpassword',
|
||||||
|
'new_password': 'newpassword',
|
||||||
|
'new_password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
assert get_message('INVALID_PASSWORD') in response.data
|
||||||
|
|
||||||
|
# Test mismatch
|
||||||
|
response = client.post('/change', data={
|
||||||
|
'password': 'password',
|
||||||
|
'new_password': 'newpassword',
|
||||||
|
'new_password_confirm': 'notnewpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
assert get_message('RETYPE_PASSWORD_MISMATCH') in response.data
|
||||||
|
|
||||||
|
# Test missing password
|
||||||
|
response = client.post('/change', data={
|
||||||
|
'password': ' ',
|
||||||
|
'new_password': '',
|
||||||
|
'new_password_confirm': ''
|
||||||
|
}, follow_redirects=True)
|
||||||
|
assert get_message('PASSWORD_NOT_PROVIDED') in response.data
|
||||||
|
|
||||||
|
# Test bad password
|
||||||
|
response = client.post('/change', data={
|
||||||
|
'password': 'password',
|
||||||
|
'new_password': 'a',
|
||||||
|
'new_password_confirm': 'a'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
assert get_message('PASSWORD_INVALID_LENGTH') in response.data
|
||||||
|
|
||||||
|
# Test same as previous
|
||||||
|
response = client.post('/change', data={
|
||||||
|
'password': 'password',
|
||||||
|
'new_password': 'password',
|
||||||
|
'new_password_confirm': 'password'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
assert get_message('PASSWORD_IS_THE_SAME') in response.data
|
||||||
|
|
||||||
|
# Test successful submit sends email notification
|
||||||
|
with app.mail.record_messages() as outbox:
|
||||||
|
response = client.post('/change', data={
|
||||||
|
'password': 'password',
|
||||||
|
'new_password': 'newpassword',
|
||||||
|
'new_password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
|
||||||
|
assert get_message('PASSWORD_CHANGE') in response.data
|
||||||
|
assert b'Home Page' in response.data
|
||||||
|
assert len(recorded) == 1
|
||||||
|
assert len(outbox) == 1
|
||||||
|
assert "Your password has been changed" in outbox[0].html
|
||||||
|
|
||||||
|
# Test JSON
|
||||||
|
data = ('{"password": "newpassword", "new_password": "newpassword2", '
|
||||||
|
'"new_password_confirm": "newpassword2"}')
|
||||||
|
response = client.post('/change', data=data, headers={'Content-Type': 'application/json'})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers['Content-Type'] == 'application/json'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(change_url='/custom_change')
|
||||||
|
def test_custom_change_url(client):
|
||||||
|
authenticate(client)
|
||||||
|
response = client.get('/custom_change')
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(change_password_template='custom_security/change_password.html')
|
||||||
|
def test_custom_change_template(client):
|
||||||
|
authenticate(client)
|
||||||
|
response = client.get('/change')
|
||||||
|
assert b'CUSTOM CHANGE PASSWORD' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(send_password_change_email=False)
|
||||||
|
def test_disable_change_emails(app, client):
|
||||||
|
with app.mail.record_messages() as outbox:
|
||||||
|
client.post('/change', data={
|
||||||
|
'password': 'password',
|
||||||
|
'new_password': 'newpassword',
|
||||||
|
'new_password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
assert len(outbox) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(post_change_view='/profile')
|
||||||
|
def test_custom_post_change_view(client):
|
||||||
|
authenticate(client)
|
||||||
|
response = client.post('/change', data={
|
||||||
|
'password': 'password',
|
||||||
|
'new_password': 'newpassword',
|
||||||
|
'new_password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
|
||||||
|
assert b'Profile Page' in response.data
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_common
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Test common functionality
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cookielib import Cookie
|
||||||
|
except ImportError:
|
||||||
|
from http.cookiejar import Cookie
|
||||||
|
|
||||||
|
from utils import authenticate, json_authenticate, logout
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_view(client):
|
||||||
|
response = client.get('/login')
|
||||||
|
assert b'<h1>Login</h1>' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate(client):
|
||||||
|
response = authenticate(client)
|
||||||
|
assert response.status_code == 302
|
||||||
|
response = authenticate(client, follow_redirects=True)
|
||||||
|
assert b'Hello matt@lp.com' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate_with_next(client):
|
||||||
|
data = dict(email='matt@lp.com', password='password')
|
||||||
|
response = client.post('/login?next=/page1', data=data, follow_redirects=True)
|
||||||
|
assert b'Page 1' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate_with_invalid_next(client, get_message):
|
||||||
|
data = dict(email='matt@lp.com', password='password')
|
||||||
|
response = client.post('/login?next=http://google.com', data=data)
|
||||||
|
assert get_message('INVALID_REDIRECT') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate_with_invalid_malformed_next(client, get_message):
|
||||||
|
data = dict(email='matt@lp.com', password='password')
|
||||||
|
response = client.post('/login?next=http:///google.com', data=data)
|
||||||
|
assert get_message('INVALID_REDIRECT') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate_case_insensitive_email(app, client):
|
||||||
|
response = authenticate(client, 'MATT@lp.com', follow_redirects=True)
|
||||||
|
assert b'Hello matt@lp.com' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_unprovided_username(client, get_message):
|
||||||
|
response = authenticate(client, "")
|
||||||
|
assert get_message('EMAIL_NOT_PROVIDED') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_unprovided_password(client, get_message):
|
||||||
|
response = authenticate(client, password="")
|
||||||
|
assert get_message('PASSWORD_NOT_PROVIDED') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_user(client, get_message):
|
||||||
|
response = authenticate(client, email="bogus@bogus.com")
|
||||||
|
assert get_message('USER_DOES_NOT_EXIST') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_password(client, get_message):
|
||||||
|
response = authenticate(client, password="bogus")
|
||||||
|
assert get_message('INVALID_PASSWORD') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_inactive_user(client, get_message):
|
||||||
|
response = authenticate(client, "tiya@lp.com", "password")
|
||||||
|
assert get_message('DISABLED_ACCOUNT') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_unset_password(client, get_message):
|
||||||
|
response = authenticate(client, "jess@lp.com", "password")
|
||||||
|
assert get_message('PASSWORD_NOT_SET') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_logout(client):
|
||||||
|
authenticate(client)
|
||||||
|
response = logout(client, follow_redirects=True)
|
||||||
|
assert b'Home Page' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_session_access(client, get_message):
|
||||||
|
response = client.get('/profile', follow_redirects=True)
|
||||||
|
assert get_message('LOGIN') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_session_access(client):
|
||||||
|
authenticate(client)
|
||||||
|
response = client.get("/profile", follow_redirects=True)
|
||||||
|
assert b'profile' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_authorized_access(client):
|
||||||
|
authenticate(client)
|
||||||
|
response = client.get("/admin")
|
||||||
|
assert b'Admin Page' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_unauthorized_access(client, get_message):
|
||||||
|
authenticate(client, "joe@lp.com")
|
||||||
|
response = client.get("/admin", follow_redirects=True)
|
||||||
|
assert get_message('UNAUTHORIZED') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_roles_accepted(client):
|
||||||
|
for user in ("matt@lp.com", "joe@lp.com"):
|
||||||
|
authenticate(client, user)
|
||||||
|
response = client.get("/admin_or_editor")
|
||||||
|
assert b'Admin or Editor Page' in response.data
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
authenticate(client, "jill@lp.com")
|
||||||
|
response = client.get("/admin_or_editor", follow_redirects=True)
|
||||||
|
assert b'Home Page' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_unauthenticated_role_required(client, get_message):
|
||||||
|
response = client.get('/admin', follow_redirects=True)
|
||||||
|
assert get_message('UNAUTHORIZED') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_role_required(client):
|
||||||
|
for user in ("matt@lp.com", "joe@lp.com"):
|
||||||
|
authenticate(client, user)
|
||||||
|
response = client.get("/admin_and_editor", follow_redirects=True)
|
||||||
|
assert b'Home Page' in response.data
|
||||||
|
client.get('/logout')
|
||||||
|
|
||||||
|
authenticate(client, 'dave@lp.com')
|
||||||
|
response = client.get("/admin_and_editor", follow_redirects=True)
|
||||||
|
assert b'Admin and Editor Page' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_ok_json_auth(client):
|
||||||
|
response = json_authenticate(client)
|
||||||
|
assert response.jdata['meta']['code'] == 200
|
||||||
|
assert 'authentication_token' in response.jdata['response']['user']
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_json_auth(client):
|
||||||
|
response = json_authenticate(client, password='junk')
|
||||||
|
assert b'"code": 400' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_token_auth_via_querystring_valid_token(client):
|
||||||
|
response = json_authenticate(client)
|
||||||
|
token = response.jdata['response']['user']['authentication_token']
|
||||||
|
response = client.get('/token?auth_token=' + token)
|
||||||
|
assert b'Token Authentication' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_token_auth_via_header_valid_token(client):
|
||||||
|
response = json_authenticate(client)
|
||||||
|
token = response.jdata['response']['user']['authentication_token']
|
||||||
|
headers = {"Authentication-Token": token}
|
||||||
|
response = client.get('/token', headers=headers)
|
||||||
|
assert b'Token Authentication' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_token_auth_via_querystring_invalid_token(client):
|
||||||
|
response = client.get('/token?auth_token=X')
|
||||||
|
assert 401 == response.status_code
|
||||||
|
|
||||||
|
|
||||||
|
def test_token_auth_via_header_invalid_token(client):
|
||||||
|
response = client.get('/token', headers={"Authentication-Token": 'X'})
|
||||||
|
assert 401 == response.status_code
|
||||||
|
|
||||||
|
|
||||||
|
def test_http_auth(client):
|
||||||
|
response = client.get('/http', headers={
|
||||||
|
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:password").decode('utf-8')
|
||||||
|
})
|
||||||
|
assert b'HTTP Authentication' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_http_auth_no_authorization(client):
|
||||||
|
response = client.get('/http', headers={})
|
||||||
|
assert b'<h1>Unauthorized</h1>' in response.data
|
||||||
|
assert 'WWW-Authenticate' in response.headers
|
||||||
|
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_http_auth_invalid_username(client):
|
||||||
|
response = client.get('/http', headers={
|
||||||
|
'Authorization': 'Basic %s' % base64.b64encode(b"bogus:bogus").decode('utf-8')
|
||||||
|
})
|
||||||
|
assert b'<h1>Unauthorized</h1>' in response.data
|
||||||
|
assert 'WWW-Authenticate' in response.headers
|
||||||
|
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_http_auth_bad_password(client):
|
||||||
|
response = client.get('/http', headers={
|
||||||
|
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus").decode('utf-8')
|
||||||
|
})
|
||||||
|
assert b'<h1>Unauthorized</h1>' in response.data
|
||||||
|
assert 'WWW-Authenticate' in response.headers
|
||||||
|
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_http_auth_realm(client):
|
||||||
|
response = client.get('/http_custom_realm', headers={
|
||||||
|
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus").decode('utf-8')
|
||||||
|
})
|
||||||
|
assert b'<h1>Unauthorized</h1>' in response.data
|
||||||
|
assert 'WWW-Authenticate' in response.headers
|
||||||
|
assert 'Basic realm="My Realm"' == response.headers['WWW-Authenticate']
|
||||||
|
|
||||||
|
|
||||||
|
def test_multi_auth_basic(client):
|
||||||
|
response = client.get('/multi_auth', headers={
|
||||||
|
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:password").decode('utf-8')
|
||||||
|
})
|
||||||
|
assert b'Basic' in response.data
|
||||||
|
|
||||||
|
response = client.get('/multi_auth')
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_multi_auth_basic_invalid(client):
|
||||||
|
response = client.get('/multi_auth', headers={
|
||||||
|
'Authorization': 'Basic %s' % base64.b64encode(b"bogus:bogus").decode('utf-8')
|
||||||
|
})
|
||||||
|
assert b'<h1>Unauthorized</h1>' in response.data
|
||||||
|
assert 'WWW-Authenticate' in response.headers
|
||||||
|
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
|
||||||
|
|
||||||
|
response = client.get('/multi_auth')
|
||||||
|
print(response.headers)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_multi_auth_token(client):
|
||||||
|
response = json_authenticate(client)
|
||||||
|
token = response.jdata['response']['user']['authentication_token']
|
||||||
|
response = client.get('/multi_auth?auth_token=' + token)
|
||||||
|
assert b'Token' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_multi_auth_session(client):
|
||||||
|
authenticate(client, )
|
||||||
|
response = client.get('/multi_auth')
|
||||||
|
assert b'Session' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_deleted_during_session_reverts_to_anonymous_user(app, client):
|
||||||
|
authenticate(client)
|
||||||
|
|
||||||
|
with app.test_request_context('/'):
|
||||||
|
user = app.security.datastore.find_user(email='matt@lp.com')
|
||||||
|
app.security.datastore.delete_user(user)
|
||||||
|
app.security.datastore.commit()
|
||||||
|
|
||||||
|
response = client.get('/')
|
||||||
|
assert b'Hello matt@lp.com' not in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_remember_token(client):
|
||||||
|
response = authenticate(client, follow_redirects=False)
|
||||||
|
client.cookie_jar.clear_session_cookies()
|
||||||
|
response = client.get('/profile')
|
||||||
|
assert b'profile' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_token_loader_does_not_fail_with_invalid_token(client):
|
||||||
|
c = Cookie(version=0, name='remember_token', value='None', port=None,
|
||||||
|
port_specified=False, domain='www.example.com',
|
||||||
|
domain_specified=False, domain_initial_dot=False, path='/',
|
||||||
|
path_specified=True, secure=False, expires=None,
|
||||||
|
discard=True, comment=None, comment_url=None,
|
||||||
|
rest={'HttpOnly': None}, rfc2109=False)
|
||||||
|
|
||||||
|
client.cookie_jar.set_cookie(c)
|
||||||
|
response = client.get('/')
|
||||||
|
assert b'BadSignature' not in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_sending_auth_token_with_json(client):
|
||||||
|
response = json_authenticate(client)
|
||||||
|
token = response.jdata['response']['user']['authentication_token']
|
||||||
|
data = '{"auth_token": "%s"}' % token
|
||||||
|
response = client.post('/token', data=data, headers={'Content-Type': 'application/json'})
|
||||||
|
assert b'Token Authentication' in response.data
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_configuration
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Basic configuration tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from utils import authenticate, logout
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(
|
||||||
|
logout_url='/custom_logout',
|
||||||
|
login_url='/custom_login',
|
||||||
|
post_login_view='/post_login',
|
||||||
|
post_logout_view='/post_logout',
|
||||||
|
default_http_auth_realm='Custom Realm')
|
||||||
|
def test_view_configuration(client):
|
||||||
|
response = client.get('/custom_login')
|
||||||
|
assert b"<h1>Login</h1>" in response.data
|
||||||
|
|
||||||
|
response = authenticate(client, endpoint='/custom_login')
|
||||||
|
assert 'location' in response.headers
|
||||||
|
assert response.headers['Location'] == 'http://localhost/post_login'
|
||||||
|
|
||||||
|
response = logout(client, endpoint='/custom_logout')
|
||||||
|
assert 'location' in response.headers
|
||||||
|
assert response.headers['Location'] == 'http://localhost/post_logout'
|
||||||
|
|
||||||
|
response = client.get('/http', headers={
|
||||||
|
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus")
|
||||||
|
})
|
||||||
|
assert b'<h1>Unauthorized</h1>' in response.data
|
||||||
|
assert 'WWW-Authenticate' in response.headers
|
||||||
|
assert 'Basic realm="Custom Realm"' == response.headers['WWW-Authenticate']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(login_user_template='custom_security/login_user.html')
|
||||||
|
def test_template_configuration(client):
|
||||||
|
response = client.get('/login')
|
||||||
|
assert b'CUSTOM LOGIN USER' in response.data
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_confirmable
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Confirmable tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_security.core import UserMixin
|
||||||
|
from flask_security.signals import user_confirmed, confirm_instructions_sent
|
||||||
|
from flask_security.utils import capture_registrations
|
||||||
|
|
||||||
|
from utils import authenticate, logout
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.confirmable()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.registerable()
|
||||||
|
def test_confirmable_flag(app, client, sqlalchemy_datastore, get_message):
|
||||||
|
recorded_confirms = []
|
||||||
|
recorded_instructions_sent = []
|
||||||
|
|
||||||
|
@user_confirmed.connect_via(app)
|
||||||
|
def on_confirmed(app, user):
|
||||||
|
assert isinstance(app, Flask)
|
||||||
|
assert isinstance(user, UserMixin)
|
||||||
|
recorded_confirms.append(user)
|
||||||
|
|
||||||
|
@confirm_instructions_sent.connect_via(app)
|
||||||
|
def on_instructions_sent(app, user):
|
||||||
|
assert isinstance(app, Flask)
|
||||||
|
assert isinstance(user, UserMixin)
|
||||||
|
recorded_instructions_sent.append(user)
|
||||||
|
|
||||||
|
# Test login before confirmation
|
||||||
|
email = 'dude@lp.com'
|
||||||
|
|
||||||
|
with capture_registrations() as registrations:
|
||||||
|
data = dict(email=email, password='password', next='')
|
||||||
|
response = client.post('/register', data=data)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
response = authenticate(client, email=email)
|
||||||
|
assert get_message('CONFIRMATION_REQUIRED') in response.data
|
||||||
|
|
||||||
|
# Test invalid token
|
||||||
|
response = client.get('/confirm/bogus', follow_redirects=True)
|
||||||
|
assert get_message('INVALID_CONFIRMATION_TOKEN') in response.data
|
||||||
|
|
||||||
|
# Test JSON
|
||||||
|
response = client.post('/confirm', data='{"email": "matt@lp.com"}', headers={
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers['Content-Type'] == 'application/json'
|
||||||
|
assert 'user' in response.jdata['response']
|
||||||
|
assert len(recorded_instructions_sent) == 1
|
||||||
|
|
||||||
|
# Test ask for instructions with invalid email
|
||||||
|
response = client.post('/confirm', data=dict(email='bogus@bogus.com'))
|
||||||
|
assert get_message('USER_DOES_NOT_EXIST') in response.data
|
||||||
|
|
||||||
|
# Test resend instructions
|
||||||
|
response = client.post('/confirm', data=dict(email=email))
|
||||||
|
assert get_message('CONFIRMATION_REQUEST', email=email) in response.data
|
||||||
|
assert len(recorded_instructions_sent) == 2
|
||||||
|
|
||||||
|
# Test confirm
|
||||||
|
token = registrations[0]['confirm_token']
|
||||||
|
response = client.get('/confirm/' + token, follow_redirects=True)
|
||||||
|
assert get_message('EMAIL_CONFIRMED') in response.data
|
||||||
|
assert len(recorded_confirms) == 1
|
||||||
|
|
||||||
|
# Test already confirmed
|
||||||
|
response = client.get('/confirm/' + token, follow_redirects=True)
|
||||||
|
assert get_message('ALREADY_CONFIRMED') in response.data
|
||||||
|
|
||||||
|
# Test already confirmed when asking for confirmation instructions
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
response = client.get('/confirm')
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response = client.post('/confirm', data=dict(email=email))
|
||||||
|
assert get_message('ALREADY_CONFIRMED') in response.data
|
||||||
|
|
||||||
|
# Test user was deleted before confirmation
|
||||||
|
with capture_registrations() as registrations:
|
||||||
|
data = dict(email='mary@lp.com', password='password', next='')
|
||||||
|
client.post('/register', data=data)
|
||||||
|
|
||||||
|
user = registrations[0]['user']
|
||||||
|
token = registrations[0]['confirm_token']
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
sqlalchemy_datastore.delete(user)
|
||||||
|
sqlalchemy_datastore.commit()
|
||||||
|
|
||||||
|
response = client.get('/confirm/' + token, follow_redirects=True)
|
||||||
|
assert get_message('INVALID_CONFIRMATION_TOKEN') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.registerable()
|
||||||
|
@pytest.mark.settings(confirm_email_within='1 milliseconds')
|
||||||
|
def test_expired_confirmation_token(client, get_message):
|
||||||
|
with capture_registrations() as registrations:
|
||||||
|
data = dict(email='mary@lp.com', password='password', next='')
|
||||||
|
client.post('/register', data=data, follow_redirects=True)
|
||||||
|
|
||||||
|
user = registrations[0]['user']
|
||||||
|
token = registrations[0]['confirm_token']
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
response = client.get('/confirm/' + token, follow_redirects=True)
|
||||||
|
msg = get_message('CONFIRMATION_EXPIRED', within='1 milliseconds', email=user.email)
|
||||||
|
assert msg in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.registerable()
|
||||||
|
@pytest.mark.settings(login_without_confirmation=True)
|
||||||
|
def test_login_when_unconfirmed(client, get_message):
|
||||||
|
data = dict(email='mary@lp.com', password='password', next='')
|
||||||
|
response = client.post('/register', data=data, follow_redirects=True)
|
||||||
|
assert b'mary@lp.com' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.registerable()
|
||||||
|
@pytest.mark.settings(login_without_confirmation=True)
|
||||||
|
def test_confirmation_different_user_when_logged_in(client, get_message):
|
||||||
|
e1 = 'dude@lp.com'
|
||||||
|
e2 = 'lady@lp.com'
|
||||||
|
|
||||||
|
with capture_registrations() as registrations:
|
||||||
|
for e in e1, e2:
|
||||||
|
data = dict(email=e, password='password', next='')
|
||||||
|
client.post('/register', data=data)
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
token1 = registrations[0]['confirm_token']
|
||||||
|
token2 = registrations[1]['confirm_token']
|
||||||
|
|
||||||
|
client.get('/confirm/' + token1, follow_redirects=True)
|
||||||
|
logout(client)
|
||||||
|
authenticate(client, email=e1)
|
||||||
|
|
||||||
|
response = client.get('/confirm/' + token2, follow_redirects=True)
|
||||||
|
assert get_message('EMAIL_CONFIRMED') in response.data
|
||||||
|
assert b'Hello lady@lp.com' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.registerable()
|
||||||
|
@pytest.mark.settings(recoverable=True)
|
||||||
|
def test_cannot_reset_password_when_email_is_not_confirmed(client, get_message):
|
||||||
|
email = 'dude@lp.com'
|
||||||
|
|
||||||
|
data = dict(email=email, password='password', next='')
|
||||||
|
response = client.post('/register', data=data, follow_redirects=True)
|
||||||
|
|
||||||
|
response = client.post('/reset', data=dict(email=email), follow_redirects=True)
|
||||||
|
assert get_message('CONFIRMATION_REQUIRED') in response.data
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_context_processors
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Context processor tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from utils import authenticate
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.recoverable()
|
||||||
|
@pytest.mark.registerable()
|
||||||
|
@pytest.mark.confirmable()
|
||||||
|
@pytest.mark.changeable()
|
||||||
|
@pytest.mark.settings(
|
||||||
|
login_without_confirmation=True,
|
||||||
|
change_password_template='custom_security/change_password.html',
|
||||||
|
login_user_template='custom_security/login_user.html',
|
||||||
|
reset_password_template='custom_security/reset_password.html',
|
||||||
|
forgot_password_template='custom_security/forgot_password.html',
|
||||||
|
send_confirmation_template='custom_security/send_confirmation.html',
|
||||||
|
register_user_template='custom_security/register_user.html')
|
||||||
|
def test_context_processors(client, app):
|
||||||
|
@app.security.context_processor
|
||||||
|
def default_ctx_processor():
|
||||||
|
return {'global': 'global'}
|
||||||
|
|
||||||
|
@app.security.forgot_password_context_processor
|
||||||
|
def forgot_password():
|
||||||
|
return {'foo': 'bar'}
|
||||||
|
|
||||||
|
response = client.get('/reset')
|
||||||
|
assert b'global' in response.data
|
||||||
|
assert b'bar' in response.data
|
||||||
|
|
||||||
|
@app.security.login_context_processor
|
||||||
|
def login():
|
||||||
|
return {'foo': 'bar'}
|
||||||
|
|
||||||
|
response = client.get('/login')
|
||||||
|
assert b'global' in response.data
|
||||||
|
assert b'bar' in response.data
|
||||||
|
|
||||||
|
@app.security.register_context_processor
|
||||||
|
def register():
|
||||||
|
return {'foo': 'bar'}
|
||||||
|
|
||||||
|
response = client.get('/register')
|
||||||
|
assert b'global' in response.data
|
||||||
|
assert b'bar' in response.data
|
||||||
|
|
||||||
|
@app.security.reset_password_context_processor
|
||||||
|
def reset_password():
|
||||||
|
return {'foo': 'bar'}
|
||||||
|
|
||||||
|
response = client.get('/reset')
|
||||||
|
assert b'global' in response.data
|
||||||
|
assert b'bar' in response.data
|
||||||
|
|
||||||
|
@app.security.change_password_context_processor
|
||||||
|
def change_password():
|
||||||
|
return {'foo': 'bar'}
|
||||||
|
|
||||||
|
authenticate(client)
|
||||||
|
response = client.get('/change')
|
||||||
|
assert b'global' in response.data
|
||||||
|
assert b'bar' in response.data
|
||||||
|
|
||||||
|
@app.security.send_confirmation_context_processor
|
||||||
|
def send_confirmation():
|
||||||
|
return {'foo': 'bar'}
|
||||||
|
|
||||||
|
response = client.get('/confirm')
|
||||||
|
assert b'global' in response.data
|
||||||
|
assert b'bar' in response.data
|
||||||
|
|
||||||
|
@app.security.mail_context_processor
|
||||||
|
def mail():
|
||||||
|
return {'foo': 'bar'}
|
||||||
|
|
||||||
|
client.get('/logout')
|
||||||
|
|
||||||
|
with app.mail.record_messages() as outbox:
|
||||||
|
client.post('/reset', data=dict(email='matt@lp.com'))
|
||||||
|
|
||||||
|
email = outbox[0]
|
||||||
|
assert 'global' in email.html
|
||||||
|
assert 'bar' in email.html
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.passwordless()
|
||||||
|
@pytest.mark.settings(send_login_template='custom_security/send_login.html')
|
||||||
|
def test_passwordless_login_context_processor(app, client):
|
||||||
|
@app.security.send_login_context_processor
|
||||||
|
def send_login():
|
||||||
|
return {'foo': 'bar'}
|
||||||
|
|
||||||
|
response = client.get('/login')
|
||||||
|
assert b'bar' in response.data
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_datastore
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Datastore tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
from flask_security import UserMixin, RoleMixin
|
||||||
|
from flask_security.datastore import Datastore, UserDatastore
|
||||||
|
|
||||||
|
from utils import init_app_with_options
|
||||||
|
|
||||||
|
|
||||||
|
class User(UserMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Role(RoleMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_unimplemented_datastore_methods():
|
||||||
|
datastore = Datastore(None)
|
||||||
|
assert datastore.db is None
|
||||||
|
with raises(NotImplementedError):
|
||||||
|
datastore.put(None)
|
||||||
|
with raises(NotImplementedError):
|
||||||
|
datastore.delete(None)
|
||||||
|
assert not datastore.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_unimplemented_user_datastore_methods():
|
||||||
|
datastore = UserDatastore(None, None)
|
||||||
|
with raises(NotImplementedError):
|
||||||
|
datastore.find_user(None)
|
||||||
|
with raises(NotImplementedError):
|
||||||
|
datastore.find_role(None)
|
||||||
|
with raises(NotImplementedError):
|
||||||
|
datastore.get_user(None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_toggle_active():
|
||||||
|
datastore = UserDatastore(None, None)
|
||||||
|
user = User()
|
||||||
|
user.active = True
|
||||||
|
assert datastore.toggle_active(user) is True
|
||||||
|
assert not user.active
|
||||||
|
assert datastore.toggle_active(user) is True
|
||||||
|
assert user.active is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_deactivate_user():
|
||||||
|
datastore = UserDatastore(None, None)
|
||||||
|
user = User()
|
||||||
|
user.active = True
|
||||||
|
assert datastore.deactivate_user(user) is True
|
||||||
|
assert not user.active
|
||||||
|
|
||||||
|
|
||||||
|
def test_activate_user():
|
||||||
|
datastore = UserDatastore(None, None)
|
||||||
|
user = User()
|
||||||
|
user.active = False
|
||||||
|
assert datastore.activate_user(user) is True
|
||||||
|
assert user.active is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_deactivate_returns_false_if_already_false():
|
||||||
|
datastore = UserDatastore(None, None)
|
||||||
|
user = User()
|
||||||
|
user.active = False
|
||||||
|
assert not datastore.deactivate_user(user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_activate_returns_false_if_already_true():
|
||||||
|
datastore = UserDatastore(None, None)
|
||||||
|
user = User()
|
||||||
|
user.active = True
|
||||||
|
assert not datastore.activate_user(user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user(app, datastore):
|
||||||
|
init_app_with_options(app, datastore, **{
|
||||||
|
'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username')
|
||||||
|
})
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
user_id = datastore.find_user(email='matt@lp.com').id
|
||||||
|
|
||||||
|
user = datastore.get_user(user_id)
|
||||||
|
assert user is not None
|
||||||
|
|
||||||
|
user = datastore.get_user('matt@lp.com')
|
||||||
|
assert user is not None
|
||||||
|
|
||||||
|
user = datastore.get_user('matt')
|
||||||
|
assert user is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_role(app, datastore):
|
||||||
|
init_app_with_options(app, datastore)
|
||||||
|
|
||||||
|
role = datastore.find_role('admin')
|
||||||
|
assert role is not None
|
||||||
|
|
||||||
|
role = datastore.find_role('bogus')
|
||||||
|
assert role is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_role_to_user(app, datastore):
|
||||||
|
init_app_with_options(app, datastore)
|
||||||
|
|
||||||
|
# Test with user object
|
||||||
|
user = datastore.find_user(email='matt@lp.com')
|
||||||
|
assert user.has_role('editor') is False
|
||||||
|
assert datastore.add_role_to_user(user, 'editor') is True
|
||||||
|
assert datastore.add_role_to_user(user, 'editor') is False
|
||||||
|
assert user.has_role('editor') is True
|
||||||
|
|
||||||
|
# Test with email
|
||||||
|
assert datastore.add_role_to_user('jill@lp.com', 'editor') is True
|
||||||
|
user = datastore.find_user(email='jill@lp.com')
|
||||||
|
assert user.has_role('editor') is True
|
||||||
|
|
||||||
|
# Test remove role
|
||||||
|
assert datastore.remove_role_from_user(user, 'editor') is True
|
||||||
|
assert datastore.remove_role_from_user(user, 'editor') is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_user_with_roles(app, datastore):
|
||||||
|
init_app_with_options(app, datastore)
|
||||||
|
|
||||||
|
role = datastore.find_role('admin')
|
||||||
|
datastore.commit()
|
||||||
|
|
||||||
|
user = datastore.create_user(email='dude@lp.com', username='dude',
|
||||||
|
password='password', roles=[role])
|
||||||
|
datastore.commit()
|
||||||
|
user = datastore.find_user(email='dude@lp.com')
|
||||||
|
assert user.has_role('admin') is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_user(app, datastore):
|
||||||
|
init_app_with_options(app, datastore)
|
||||||
|
|
||||||
|
user = datastore.find_user(email='matt@lp.com')
|
||||||
|
datastore.delete_user(user)
|
||||||
|
datastore.commit()
|
||||||
|
user = datastore.find_user(email='matt@lp.com')
|
||||||
|
assert user is None
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_entities
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Entity tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask_security import RoleMixin, UserMixin, AnonymousUser
|
||||||
|
|
||||||
|
|
||||||
|
class Role(RoleMixin):
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class User(UserMixin):
|
||||||
|
def __init__(self, roles):
|
||||||
|
self.roles = roles
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_mixin_equal():
|
||||||
|
admin1 = Role('admin')
|
||||||
|
admin2 = Role('admin')
|
||||||
|
assert admin1 == admin2
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_mixin_not_equal():
|
||||||
|
admin = Role('admin')
|
||||||
|
editor = Role('editor')
|
||||||
|
assert admin != editor
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_mixin_has_role_with_string():
|
||||||
|
admin = Role('admin')
|
||||||
|
editor = Role('editor')
|
||||||
|
user = User([admin, editor])
|
||||||
|
assert user.has_role('admin') is True
|
||||||
|
assert user.has_role('editor') is True
|
||||||
|
assert user.has_role(admin) is True
|
||||||
|
assert user.has_role(editor) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_anonymous_user_has_no_roles():
|
||||||
|
user = AnonymousUser()
|
||||||
|
assert not user.has_role('admin')
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_hashing
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
hashing tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
from flask_security.utils import verify_password, encrypt_password
|
||||||
|
|
||||||
|
from utils import authenticate, init_app_with_options
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_password_bcrypt(app, sqlalchemy_datastore):
|
||||||
|
init_app_with_options(app, sqlalchemy_datastore, **{
|
||||||
|
'SECURITY_PASSWORD_HASH': 'bcrypt',
|
||||||
|
'SECURITY_PASSWORD_SALT': 'salty'
|
||||||
|
})
|
||||||
|
with app.app_context():
|
||||||
|
assert verify_password('pass', encrypt_password('pass'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_with_bcrypt_enabled(app, sqlalchemy_datastore):
|
||||||
|
init_app_with_options(app, sqlalchemy_datastore, **{
|
||||||
|
'SECURITY_PASSWORD_HASH': 'bcrypt',
|
||||||
|
'SECURITY_PASSWORD_SALT': 'salty'
|
||||||
|
})
|
||||||
|
response = authenticate(app.test_client(), follow_redirects=True)
|
||||||
|
assert b'Home Page' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_hash_salt_option(app, sqlalchemy_datastore):
|
||||||
|
with raises(RuntimeError):
|
||||||
|
init_app_with_options(app, sqlalchemy_datastore, **{
|
||||||
|
'SECURITY_PASSWORD_HASH': 'bcrypt',
|
||||||
|
})
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_emails
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Email functionality tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flask_security import Security
|
||||||
|
from flask_security.forms import LoginForm, RegisterForm, ConfirmRegisterForm, \
|
||||||
|
SendConfirmationForm, PasswordlessLoginForm, ForgotPasswordForm, ResetPasswordForm, \
|
||||||
|
ChangePasswordForm, StringField, PasswordField, email_required, email_validator, \
|
||||||
|
valid_user_email
|
||||||
|
from flask_security.utils import capture_reset_password_requests, md5, string_types
|
||||||
|
|
||||||
|
from utils import authenticate, init_app_with_options, populate_data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.recoverable()
|
||||||
|
def test_async_email_task(app, client):
|
||||||
|
app.mail_sent = False
|
||||||
|
|
||||||
|
@app.security.send_mail_task
|
||||||
|
def send_email(msg):
|
||||||
|
app.mail_sent = True
|
||||||
|
|
||||||
|
client.post('/reset', data=dict(email='matt@lp.com'))
|
||||||
|
assert app.mail_sent is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_blueprint_flag(app, sqlalchemy_datastore):
|
||||||
|
app.security = Security(app, datastore=Security, register_blueprint=False)
|
||||||
|
client = app.test_client()
|
||||||
|
response = client.get('/login')
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.registerable()
|
||||||
|
@pytest.mark.recoverable()
|
||||||
|
@pytest.mark.changeable()
|
||||||
|
def test_basic_custom_forms(app, sqlalchemy_datastore):
|
||||||
|
class MyLoginForm(LoginForm):
|
||||||
|
email = StringField('My Login Email Address Field')
|
||||||
|
|
||||||
|
class MyRegisterForm(RegisterForm):
|
||||||
|
email = StringField('My Register Email Address Field')
|
||||||
|
|
||||||
|
class MyForgotPasswordForm(ForgotPasswordForm):
|
||||||
|
email = StringField('My Forgot Email Address Field',
|
||||||
|
validators=[email_required, email_validator, valid_user_email])
|
||||||
|
|
||||||
|
class MyResetPasswordForm(ResetPasswordForm):
|
||||||
|
password = StringField('My Reset Password Field')
|
||||||
|
|
||||||
|
class MyChangePasswordForm(ChangePasswordForm):
|
||||||
|
password = PasswordField('My Change Password Field')
|
||||||
|
|
||||||
|
app.security = Security(app,
|
||||||
|
datastore=sqlalchemy_datastore,
|
||||||
|
login_form=MyLoginForm,
|
||||||
|
register_form=MyRegisterForm,
|
||||||
|
forgot_password_form=MyForgotPasswordForm,
|
||||||
|
reset_password_form=MyResetPasswordForm,
|
||||||
|
change_password_form=MyChangePasswordForm)
|
||||||
|
|
||||||
|
populate_data(app)
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get('/login')
|
||||||
|
assert b'My Login Email Address Field' in response.data
|
||||||
|
|
||||||
|
response = client.get('/register')
|
||||||
|
assert b'My Register Email Address Field' in response.data
|
||||||
|
|
||||||
|
response = client.get('/reset')
|
||||||
|
assert b'My Forgot Email Address Field' in response.data
|
||||||
|
|
||||||
|
with capture_reset_password_requests() as requests:
|
||||||
|
response = client.post('/reset', data=dict(email='matt@lp.com'))
|
||||||
|
|
||||||
|
token = requests[0]['token']
|
||||||
|
response = client.get('/reset/' + token)
|
||||||
|
assert b'My Reset Password Field' in response.data
|
||||||
|
|
||||||
|
authenticate(client)
|
||||||
|
|
||||||
|
response = client.get('/change')
|
||||||
|
assert b'My Change Password Field' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.registerable()
|
||||||
|
@pytest.mark.confirmable()
|
||||||
|
def test_confirmable_custom_form(app, sqlalchemy_datastore):
|
||||||
|
app.config['SECURITY_REGISTERABLE'] = True
|
||||||
|
app.config['SECURITY_CONFIRMABLE'] = True
|
||||||
|
|
||||||
|
class MyRegisterForm(ConfirmRegisterForm):
|
||||||
|
email = StringField('My Register Email Address Field')
|
||||||
|
|
||||||
|
class MySendConfirmationForm(SendConfirmationForm):
|
||||||
|
email = StringField('My Send Confirmation Email Address Field')
|
||||||
|
|
||||||
|
app.security = Security(app,
|
||||||
|
datastore=sqlalchemy_datastore,
|
||||||
|
send_confirmation_form=MySendConfirmationForm,
|
||||||
|
confirm_register_form=MyRegisterForm)
|
||||||
|
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get('/register')
|
||||||
|
assert b'My Register Email Address Field' in response.data
|
||||||
|
|
||||||
|
response = client.get('/confirm')
|
||||||
|
assert b'My Send Confirmation Email Address Field' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_passwordless_custom_form(app, sqlalchemy_datastore):
|
||||||
|
app.config['SECURITY_PASSWORDLESS'] = True
|
||||||
|
|
||||||
|
class MyPasswordlessLoginForm(PasswordlessLoginForm):
|
||||||
|
email = StringField('My Passwordless Email Address Field')
|
||||||
|
|
||||||
|
app.security = Security(app,
|
||||||
|
datastore=sqlalchemy_datastore,
|
||||||
|
passwordless_login_form=MyPasswordlessLoginForm)
|
||||||
|
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get('/login')
|
||||||
|
assert b'My Passwordless Email Address Field' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_addition_identity_attributes(app, sqlalchemy_datastore):
|
||||||
|
init_app_with_options(app, sqlalchemy_datastore, **{
|
||||||
|
'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username')
|
||||||
|
})
|
||||||
|
client = app.test_client()
|
||||||
|
response = authenticate(client, email='matt', follow_redirects=True)
|
||||||
|
assert b'Hello matt@lp.com' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_flash_messages_off(app, sqlalchemy_datastore, get_message):
|
||||||
|
init_app_with_options(app, sqlalchemy_datastore, **{
|
||||||
|
'SECURITY_FLASH_MESSAGES': False
|
||||||
|
})
|
||||||
|
client = app.test_client()
|
||||||
|
response = client.get('/profile')
|
||||||
|
assert get_message('LOGIN') not in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_hash_scheme(app, sqlalchemy_datastore, get_message):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
init_app_with_options(app, sqlalchemy_datastore, **{
|
||||||
|
'SECURITY_PASSWORD_HASH': 'bogus'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_hash_type(app, sqlalchemy_datastore):
|
||||||
|
init_app_with_options(app, sqlalchemy_datastore, **{
|
||||||
|
'SECURITY_PASSWORD_SCHEMES': ['bcrypt', 'plaintext']
|
||||||
|
})
|
||||||
|
|
||||||
|
app.config['SECURITY_PASSWORD_HASH'] = 'bcrypt'
|
||||||
|
app.config['SECURITY_PASSWORD_SALT'] = 'salty'
|
||||||
|
|
||||||
|
app.security = Security(app, datastore=sqlalchemy_datastore, register_blueprint=False)
|
||||||
|
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post('/login', data=dict(email='matt@lp.com', password='password'))
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
response = client.get('/logout')
|
||||||
|
|
||||||
|
response = client.post('/login', data=dict(email='matt@lp.com', password='password'))
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
|
||||||
|
def test_md5():
|
||||||
|
data = md5(b'hello')
|
||||||
|
assert isinstance(data, string_types)
|
||||||
|
data = md5(u'hellö')
|
||||||
|
assert isinstance(data, string_types)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(password_salt=u'öööööööööööööööööööööööööööööööööö',
|
||||||
|
password_hash='bcrypt')
|
||||||
|
def test_password_unicode_password_salt(client):
|
||||||
|
response = authenticate(client)
|
||||||
|
assert response.status_code == 302
|
||||||
|
response = authenticate(client, follow_redirects=True)
|
||||||
|
assert b'Hello matt@lp.com' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_unauthorized_handler(app, client):
|
||||||
|
@app.security.unauthorized_handler
|
||||||
|
def unauthorized():
|
||||||
|
app.unauthorized_handler_set = True
|
||||||
|
return 'unauthorized-handler-set', 401
|
||||||
|
|
||||||
|
app.unauthorized_handler_set = False
|
||||||
|
|
||||||
|
authenticate(client, "joe@lp.com")
|
||||||
|
response = client.get("/admin", follow_redirects=True)
|
||||||
|
|
||||||
|
assert app.unauthorized_handler_set is True
|
||||||
|
assert b'unauthorized-handler-set' in response.data
|
||||||
|
assert response.status_code == 401
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_passwordless
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Passwordless tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_security.core import UserMixin
|
||||||
|
from flask_security.signals import login_instructions_sent
|
||||||
|
from flask_security.utils import capture_passwordless_login_requests, string_types
|
||||||
|
|
||||||
|
from utils import logout
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.passwordless()
|
||||||
|
|
||||||
|
|
||||||
|
def test_trackable_flag(app, client, get_message):
|
||||||
|
recorded = []
|
||||||
|
|
||||||
|
@login_instructions_sent.connect_via(app)
|
||||||
|
def on_instructions_sent(app, user, login_token):
|
||||||
|
assert isinstance(app, Flask)
|
||||||
|
assert isinstance(user, UserMixin)
|
||||||
|
assert isinstance(login_token, string_types)
|
||||||
|
recorded.append(user)
|
||||||
|
|
||||||
|
# Test disabled account
|
||||||
|
response = client.post('/login', data=dict(email='tiya@lp.com'), follow_redirects=True)
|
||||||
|
assert get_message('DISABLED_ACCOUNT') in response.data
|
||||||
|
|
||||||
|
# Test login with json and valid email
|
||||||
|
data = '{"email": "matt@lp.com", "password": "password"}'
|
||||||
|
response = client.post('/login', data=data, headers={'Content-Type': 'application/json'})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(recorded) == 1
|
||||||
|
|
||||||
|
# Test login with json and invalid email
|
||||||
|
data = '{"email": "nobody@lp.com", "password": "password"}'
|
||||||
|
response = client.post('/login', data=data, headers={'Content-Type': 'application/json'})
|
||||||
|
assert b'errors' in response.data
|
||||||
|
|
||||||
|
# Test sends email and shows appropriate response
|
||||||
|
with capture_passwordless_login_requests() as requests:
|
||||||
|
with app.mail.record_messages() as outbox:
|
||||||
|
response = client.post('/login', data=dict(email='matt@lp.com'), follow_redirects=True)
|
||||||
|
|
||||||
|
assert len(recorded) == 2
|
||||||
|
assert len(requests) == 1
|
||||||
|
assert len(outbox) == 1
|
||||||
|
assert 'user' in requests[0]
|
||||||
|
assert 'login_token' in requests[0]
|
||||||
|
|
||||||
|
user = requests[0]['user']
|
||||||
|
assert get_message('LOGIN_EMAIL_SENT', email=user.email) in response.data
|
||||||
|
|
||||||
|
token = requests[0]['login_token']
|
||||||
|
response = client.get('/login/' + token, follow_redirects=True)
|
||||||
|
assert get_message('PASSWORDLESS_LOGIN_SUCCESSFUL') in response.data
|
||||||
|
|
||||||
|
# Test already authenticated
|
||||||
|
response = client.get('/login/' + token, follow_redirects=True)
|
||||||
|
assert get_message('PASSWORDLESS_LOGIN_SUCCESSFUL') not in response.data
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
# Test invalid token
|
||||||
|
response = client.get('/login/bogus', follow_redirects=True)
|
||||||
|
assert get_message('INVALID_LOGIN_TOKEN') in response.data
|
||||||
|
|
||||||
|
# Test login request with invalid email
|
||||||
|
response = client.post('/login', data=dict(email='bogus@bogus.com'))
|
||||||
|
assert get_message('USER_DOES_NOT_EXIST') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(login_within='1 milliseconds')
|
||||||
|
def test_expired_login_token(client, app, get_message):
|
||||||
|
e = 'matt@lp.com'
|
||||||
|
|
||||||
|
with capture_passwordless_login_requests() as requests:
|
||||||
|
client.post('/login', data=dict(email=e), follow_redirects=True)
|
||||||
|
|
||||||
|
token = requests[0]['login_token']
|
||||||
|
user = requests[0]['user']
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
response = client.get('/login/' + token, follow_redirects=True)
|
||||||
|
assert get_message('LOGIN_EXPIRED', within='1 milliseconds', email=user.email) in response.data
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_recoverable
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Recoverable functionality tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_security.core import UserMixin
|
||||||
|
from flask_security.signals import reset_password_instructions_sent, password_reset
|
||||||
|
from flask_security.utils import capture_reset_password_requests, string_types
|
||||||
|
|
||||||
|
from utils import authenticate, logout
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.recoverable()
|
||||||
|
|
||||||
|
|
||||||
|
def test_recoverable_flag(app, client, get_message):
|
||||||
|
recorded_resets = []
|
||||||
|
recorded_instructions_sent = []
|
||||||
|
|
||||||
|
@password_reset.connect_via(app)
|
||||||
|
def on_password_reset(app, user):
|
||||||
|
recorded_resets.append(user)
|
||||||
|
|
||||||
|
@reset_password_instructions_sent.connect_via(app)
|
||||||
|
def on_instructions_sent(app, user, token):
|
||||||
|
assert isinstance(app, Flask)
|
||||||
|
assert isinstance(user, UserMixin)
|
||||||
|
assert isinstance(token, string_types)
|
||||||
|
recorded_instructions_sent.append(user)
|
||||||
|
|
||||||
|
# Test the reset view
|
||||||
|
response = client.get('/reset')
|
||||||
|
assert b'<h1>Send password reset instructions</h1>' in response.data
|
||||||
|
|
||||||
|
# Test submitting email to reset password creates a token and sends email
|
||||||
|
with capture_reset_password_requests() as requests:
|
||||||
|
with app.mail.record_messages() as outbox:
|
||||||
|
response = client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
|
||||||
|
|
||||||
|
assert len(recorded_instructions_sent) == 1
|
||||||
|
assert len(outbox) == 1
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert get_message('PASSWORD_RESET_REQUEST', email='joe@lp.com') in response.data
|
||||||
|
token = requests[0]['token']
|
||||||
|
|
||||||
|
# Test view for reset token
|
||||||
|
response = client.get('/reset/' + token)
|
||||||
|
assert b'<h1>Reset password</h1>' in response.data
|
||||||
|
|
||||||
|
# Test submitting a new password
|
||||||
|
response = client.post('/reset/' + token, data={
|
||||||
|
'password': 'newpassword',
|
||||||
|
'password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
|
||||||
|
assert get_message('PASSWORD_RESET') in response.data
|
||||||
|
assert len(recorded_resets) == 1
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
# Test logging in with the new password
|
||||||
|
response = authenticate(client, 'joe@lp.com', 'newpassword', follow_redirects=True)
|
||||||
|
assert b'Hello joe@lp.com' in response.data
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
# Test submitting JSON
|
||||||
|
response = client.post('/reset', data='{"email": "joe@lp.com"}', headers={
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
assert response.headers['Content-Type'] == 'application/json'
|
||||||
|
assert 'user' not in response.jdata['response']
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
# Test invalid email
|
||||||
|
response = client.post('/reset', data=dict(email='bogus@lp.com'), follow_redirects=True)
|
||||||
|
assert get_message('USER_DOES_NOT_EXIST') in response.data
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
# Test invalid token
|
||||||
|
response = client.post('/reset/bogus', data={
|
||||||
|
'password': 'newpassword',
|
||||||
|
'password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
assert get_message('INVALID_RESET_PASSWORD_TOKEN') in response.data
|
||||||
|
|
||||||
|
# Test mangled token
|
||||||
|
token = ("WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd.BZEw_Q.lQyo3npdPZtcJ"
|
||||||
|
"_sNHVHP103syjM&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d")
|
||||||
|
response = client.post('/reset/' + token, data={
|
||||||
|
'password': 'newpassword',
|
||||||
|
'password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
assert get_message('INVALID_RESET_PASSWORD_TOKEN') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(reset_password_within='1 milliseconds')
|
||||||
|
def test_expired_reset_token(client, get_message):
|
||||||
|
with capture_reset_password_requests() as requests:
|
||||||
|
client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
|
||||||
|
|
||||||
|
user = requests[0]['user']
|
||||||
|
token = requests[0]['token']
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
response = client.post('/reset/' + token, data={
|
||||||
|
'password': 'newpassword',
|
||||||
|
'password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
|
||||||
|
msg = get_message('PASSWORD_RESET_EXPIRED', within='1 milliseconds', email=user.email)
|
||||||
|
assert msg in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_used_reset_token(client, get_message):
|
||||||
|
with capture_reset_password_requests() as requests:
|
||||||
|
client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
|
||||||
|
|
||||||
|
token = requests[0]['token']
|
||||||
|
|
||||||
|
# use the token
|
||||||
|
response = client.post('/reset/' + token, data={
|
||||||
|
'password': 'newpassword',
|
||||||
|
'password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
|
||||||
|
assert get_message('PASSWORD_RESET') in response.data
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
# attempt to use it a second time
|
||||||
|
response2 = client.post('/reset/' + token, data={
|
||||||
|
'password': 'otherpassword',
|
||||||
|
'password_confirm': 'otherpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
|
||||||
|
msg = get_message('INVALID_RESET_PASSWORD_TOKEN')
|
||||||
|
assert msg in response2.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_passwordless_user(client, get_message):
|
||||||
|
with capture_reset_password_requests() as requests:
|
||||||
|
client.post('/reset', data=dict(email='jess@lp.com'), follow_redirects=True)
|
||||||
|
|
||||||
|
token = requests[0]['token']
|
||||||
|
|
||||||
|
# use the token
|
||||||
|
response = client.post('/reset/' + token, data={
|
||||||
|
'password': 'newpassword',
|
||||||
|
'password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
|
||||||
|
assert get_message('PASSWORD_RESET') in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(reset_url='/custom_reset')
|
||||||
|
def test_custom_reset_url(client):
|
||||||
|
response = client.get('/custom_reset')
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(reset_password_template='custom_security/reset_password.html',
|
||||||
|
forgot_password_template='custom_security/forgot_password.html')
|
||||||
|
def test_custom_reset_templates(client):
|
||||||
|
response = client.get('/reset')
|
||||||
|
assert b'CUSTOM FORGOT PASSWORD' in response.data
|
||||||
|
|
||||||
|
with capture_reset_password_requests() as requests:
|
||||||
|
client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
|
||||||
|
token = requests[0]['token']
|
||||||
|
|
||||||
|
response = client.get('/reset/' + token)
|
||||||
|
assert b'CUSTOM RESET PASSWORD' in response.data
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_registerable
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Registerable tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_security.core import UserMixin
|
||||||
|
from flask_security.signals import user_registered
|
||||||
|
|
||||||
|
from utils import authenticate, logout
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.registerable()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(post_register_view='/post_register')
|
||||||
|
def test_registerable_flag(client, app, get_message):
|
||||||
|
recorded = []
|
||||||
|
|
||||||
|
# Test the register view
|
||||||
|
response = client.get('/register')
|
||||||
|
assert b"<h1>Register</h1>" in response.data
|
||||||
|
|
||||||
|
# Test registering is successful, sends email, and fires signal
|
||||||
|
@user_registered.connect_via(app)
|
||||||
|
def on_user_registerd(app, user, confirm_token):
|
||||||
|
assert isinstance(app, Flask)
|
||||||
|
assert isinstance(user, UserMixin)
|
||||||
|
assert confirm_token is None
|
||||||
|
recorded.append(user)
|
||||||
|
|
||||||
|
data = dict(
|
||||||
|
email='dude@lp.com', password='password', password_confirm='password',
|
||||||
|
next=''
|
||||||
|
)
|
||||||
|
with app.mail.record_messages() as outbox:
|
||||||
|
response = client.post('/register', data=data, follow_redirects=True)
|
||||||
|
|
||||||
|
assert len(recorded) == 1
|
||||||
|
assert len(outbox) == 1
|
||||||
|
assert b'Post Register' in response.data
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
# Test user can login after registering
|
||||||
|
response = authenticate(client, email='dude@lp.com', password='password')
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
# Test registering with an existing email
|
||||||
|
data = dict(
|
||||||
|
email='dude@lp.com', password='password', password_confirm='password',
|
||||||
|
next=''
|
||||||
|
)
|
||||||
|
response = client.post('/register', data=data, follow_redirects=True)
|
||||||
|
assert get_message('EMAIL_ALREADY_ASSOCIATED', email='dude@lp.com') in response.data
|
||||||
|
|
||||||
|
# Test registering with an existing email but case insensitive
|
||||||
|
data = dict(
|
||||||
|
email='Dude@lp.com', password='password', password_confirm='password',
|
||||||
|
next=''
|
||||||
|
)
|
||||||
|
response = client.post('/register', data=data, follow_redirects=True)
|
||||||
|
assert get_message('EMAIL_ALREADY_ASSOCIATED', email='Dude@lp.com') in response.data
|
||||||
|
|
||||||
|
# Test registering with JSON
|
||||||
|
data = '{ "email": "dude2@lp.com", "password": "password"}'
|
||||||
|
response = client.post('/register', data=data, headers={'Content-Type': 'application/json'})
|
||||||
|
assert response.headers['content-type'] == 'application/json'
|
||||||
|
assert response.jdata['meta']['code'] == 200
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
# Test registering with invalid JSON
|
||||||
|
data = '{ "email": "bogus", "password": "password"}'
|
||||||
|
response = client.post('/register', data=data, headers={'Content-Type': 'application/json'})
|
||||||
|
assert response.headers['content-type'] == 'application/json'
|
||||||
|
assert response.jdata['meta']['code'] == 400
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
# Test ?next param
|
||||||
|
data = dict(email='dude3@lp.com',
|
||||||
|
password='password',
|
||||||
|
password_confirm='password',
|
||||||
|
next='')
|
||||||
|
|
||||||
|
response = client.post('/register?next=/page1', data=data, follow_redirects=True)
|
||||||
|
assert b'Page 1' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(register_url='/custom_register', post_register_view='/post_register')
|
||||||
|
def test_custom_register_url(client):
|
||||||
|
response = client.get('/custom_register')
|
||||||
|
assert b"<h1>Register</h1>" in response.data
|
||||||
|
|
||||||
|
data = dict(email='dude@lp.com',
|
||||||
|
password='password',
|
||||||
|
password_confirm='password',
|
||||||
|
next='')
|
||||||
|
|
||||||
|
response = client.post('/custom_register', data=data, follow_redirects=True)
|
||||||
|
assert b'Post Register' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(register_user_template='custom_security/register_user.html')
|
||||||
|
def test_custom_register_tempalate(client):
|
||||||
|
response = client.get('/register')
|
||||||
|
assert b'CUSTOM REGISTER USER' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.settings(send_register_email=False)
|
||||||
|
def test_disable_register_emails(client, app):
|
||||||
|
data = dict(
|
||||||
|
email='dude@lp.com', password='password', password_confirm='password',
|
||||||
|
next=''
|
||||||
|
)
|
||||||
|
with app.mail.record_messages() as outbox:
|
||||||
|
client.post('/register', data=data, follow_redirects=True)
|
||||||
|
assert len(outbox) == 0
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_trackable
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Trackable tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from utils import authenticate, logout
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.trackable()
|
||||||
|
|
||||||
|
|
||||||
|
def test_trackable_flag(app, client):
|
||||||
|
e = 'matt@lp.com'
|
||||||
|
authenticate(client, email=e)
|
||||||
|
logout(client)
|
||||||
|
authenticate(client, email=e, headers={'X-Forwarded-For': '127.0.0.1'})
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
user = app.security.datastore.find_user(email=e)
|
||||||
|
assert user.last_login_at is not None
|
||||||
|
assert user.current_login_at is not None
|
||||||
|
assert user.last_login_ip == 'untrackable'
|
||||||
|
assert user.current_login_ip == '127.0.0.1'
|
||||||
|
assert user.login_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_trackable_with_multiple_ips_in_headers(app, client):
|
||||||
|
e = 'matt@lp.com'
|
||||||
|
authenticate(client, email=e)
|
||||||
|
logout(client)
|
||||||
|
authenticate(client, email=e, headers={
|
||||||
|
'X-Forwarded-For': '99.99.99.99, 88.88.88.88'})
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
user = app.security.datastore.find_user(email=e)
|
||||||
|
assert user.last_login_at is not None
|
||||||
|
assert user.current_login_at is not None
|
||||||
|
assert user.last_login_ip == 'untrackable'
|
||||||
|
assert user.current_login_ip == '88.88.88.88'
|
||||||
|
assert user.login_count == 2
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from flask_security import RoleMixin, UserMixin, AnonymousUser
|
|
||||||
from flask_security.datastore import Datastore, UserDatastore
|
|
||||||
|
|
||||||
|
|
||||||
class Role(RoleMixin):
|
|
||||||
def __init__(self, name):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin):
|
|
||||||
def __init__(self, email, roles):
|
|
||||||
self.email = email
|
|
||||||
self.roles = roles
|
|
||||||
|
|
||||||
admin = Role('admin')
|
|
||||||
admin2 = Role('admin')
|
|
||||||
editor = Role('editor')
|
|
||||||
|
|
||||||
user = User('matt@lp.com', [admin, editor])
|
|
||||||
|
|
||||||
|
|
||||||
class SecurityEntityTests(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_role_mixin_equal(self):
|
|
||||||
self.assertEqual(admin, admin2)
|
|
||||||
|
|
||||||
def test_role_mixin_not_equal(self):
|
|
||||||
self.assertNotEqual(admin, editor)
|
|
||||||
|
|
||||||
def test_user_mixin_has_role_with_string(self):
|
|
||||||
self.assertTrue(user.has_role('admin'))
|
|
||||||
|
|
||||||
def test_user_mixin_has_role_with_role_obj(self):
|
|
||||||
self.assertTrue(user.has_role(Role('admin')))
|
|
||||||
|
|
||||||
def test_anonymous_user_has_no_roles(self):
|
|
||||||
au = AnonymousUser()
|
|
||||||
self.assertEqual(0, len(au.roles))
|
|
||||||
self.assertFalse(au.has_role('admin'))
|
|
||||||
|
|
||||||
|
|
||||||
class DatastoreTests(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(DatastoreTests, self).setUp()
|
|
||||||
self.ds = UserDatastore(None, None)
|
|
||||||
|
|
||||||
def test_unimplemented_datastore_methods(self):
|
|
||||||
ds = Datastore(None)
|
|
||||||
self.assertRaises(NotImplementedError, ds.put, None)
|
|
||||||
self.assertRaises(NotImplementedError, ds.delete, None)
|
|
||||||
|
|
||||||
def test_unimplemented_user_datastore_methods(self):
|
|
||||||
self.assertRaises(NotImplementedError, self.ds.find_user, None)
|
|
||||||
self.assertRaises(NotImplementedError, self.ds.find_role, None)
|
|
||||||
|
|
||||||
def test_toggle_active(self):
|
|
||||||
user.active = True
|
|
||||||
rv = self.ds.toggle_active(user)
|
|
||||||
self.assertTrue(rv)
|
|
||||||
self.assertFalse(user.active)
|
|
||||||
rv = self.ds.toggle_active(user)
|
|
||||||
self.assertTrue(rv)
|
|
||||||
self.assertTrue(user.active)
|
|
||||||
|
|
||||||
def test_deactivate_user(self):
|
|
||||||
user.active = True
|
|
||||||
rv = self.ds.deactivate_user(user)
|
|
||||||
self.assertTrue(rv)
|
|
||||||
self.assertFalse(user.active)
|
|
||||||
|
|
||||||
def test_activate_user(self):
|
|
||||||
ds = UserDatastore(None, None)
|
|
||||||
user.active = False
|
|
||||||
ds.activate_user(user)
|
|
||||||
self.assertTrue(user.active)
|
|
||||||
|
|
||||||
def test_deactivate_returns_false_if_already_false(self):
|
|
||||||
user.active = False
|
|
||||||
self.assertFalse(self.ds.deactivate_user(user))
|
|
||||||
|
|
||||||
def test_activate_returns_false_if_already_true(self):
|
|
||||||
user.active = True
|
|
||||||
self.assertFalse(self.ds.activate_user(user))
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
utils
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
Test utils
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Response as BaseResponse, json
|
||||||
|
|
||||||
|
from flask_security import Security
|
||||||
|
from flask_security.utils import encrypt_password
|
||||||
|
|
||||||
|
_missing = object
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(client, email="matt@lp.com", password="password", endpoint=None, **kwargs):
|
||||||
|
data = dict(email=email, password=password, remember='y')
|
||||||
|
return client.post(endpoint or '/login', data=data, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def json_authenticate(client, email="matt@lp.com", password="password", endpoint=None):
|
||||||
|
data = '{"email": "%s", "password": "%s"}' % (email, password)
|
||||||
|
return client.post(endpoint or '/login', content_type="application/json", data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def logout(client, endpoint=None, **kwargs):
|
||||||
|
return client.get(endpoint or '/logout', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_roles(ds):
|
||||||
|
for role in ('admin', 'editor', 'author'):
|
||||||
|
ds.create_role(name=role)
|
||||||
|
ds.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def create_users(ds, count=None):
|
||||||
|
users = [('matt@lp.com', 'matt', 'password', ['admin'], True),
|
||||||
|
('joe@lp.com', 'joe', 'password', ['editor'], True),
|
||||||
|
('dave@lp.com', 'dave', 'password', ['admin', 'editor'], True),
|
||||||
|
('jill@lp.com', 'jill', 'password', ['author'], True),
|
||||||
|
('tiya@lp.com', 'tiya', 'password', [], False),
|
||||||
|
('jess@lp.com', 'jess', None, [], True)]
|
||||||
|
count = count or len(users)
|
||||||
|
|
||||||
|
for u in users[:count]:
|
||||||
|
pw = u[2]
|
||||||
|
if pw is not None:
|
||||||
|
pw = encrypt_password(pw)
|
||||||
|
roles = [ds.find_or_create_role(rn) for rn in u[3]]
|
||||||
|
ds.commit()
|
||||||
|
user = ds.create_user(email=u[0], username=u[1], password=pw, active=u[4])
|
||||||
|
ds.commit()
|
||||||
|
for role in roles:
|
||||||
|
ds.add_role_to_user(user, role)
|
||||||
|
ds.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def populate_data(app, user_count=None):
|
||||||
|
ds = app.security.datastore
|
||||||
|
with app.app_context():
|
||||||
|
create_roles(ds)
|
||||||
|
create_users(ds, user_count)
|
||||||
|
|
||||||
|
|
||||||
|
class Response(BaseResponse): # pragma: no cover
|
||||||
|
|
||||||
|
@property
|
||||||
|
def jdata(self):
|
||||||
|
rv = getattr(self, '_cached_jdata', _missing)
|
||||||
|
if rv is not _missing:
|
||||||
|
return rv
|
||||||
|
try:
|
||||||
|
self._cached_jdata = json.loads(self.data)
|
||||||
|
except ValueError:
|
||||||
|
raise Exception('Invalid JSON response')
|
||||||
|
return self._cached_jdata
|
||||||
|
|
||||||
|
|
||||||
|
def init_app_with_options(app, datastore, **options):
|
||||||
|
security_args = options.pop('security_args', {})
|
||||||
|
app.config.update(**options)
|
||||||
|
app.security = Security(app, datastore=datastore, **security_args)
|
||||||
|
populate_data(app)
|
||||||
Reference in New Issue
Block a user