mirror of
https://github.com/wassname/flask-security.git
synced 2026-08-07 11:22:21 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f46f35981 | ||
|
|
c0d9eecf10 | ||
|
|
04bb2c4041 | ||
|
|
8eeb832d2e | ||
|
|
8f760aadbd | ||
|
|
03d27cd600 | ||
|
|
d30a27b3bb | ||
|
|
78903fa2e5 | ||
|
|
514de64303 | ||
|
|
bad63265f8 | ||
|
|
66a9dcd2e6 | ||
|
|
df1647f1f9 | ||
|
|
89ecded480 | ||
|
|
2b35b37a66 | ||
|
|
4d8a813004 | ||
|
|
06312ef50f | ||
|
|
c3ad5b2fa6 | ||
|
|
637fc913cd | ||
|
|
e00522f331 | ||
|
|
3d34d87a97 | ||
|
|
461ace9303 | ||
|
|
d19bb98abd | ||
|
|
c24af5ca6e | ||
|
|
26045fc4dc | ||
|
|
bf260d4b7e | ||
|
|
db56ff74a9 | ||
|
|
e03efe0b34 | ||
|
|
c587988a3a | ||
|
|
f2d5245bd8 | ||
|
|
97e1960abd | ||
|
|
e749b77ca7 | ||
|
|
6f3c163ee7 | ||
|
|
3b81ec57ea | ||
|
|
38874433c7 | ||
|
|
4eda3e756f | ||
|
|
4815b1afed | ||
|
|
99ac732d10 | ||
|
|
e8b0c62818 | ||
|
|
1108f1670c | ||
|
|
3575a2df18 | ||
|
|
c84c485493 | ||
|
|
8298ac461e | ||
|
|
105d04768e | ||
|
|
f1cca43d9c | ||
|
|
e8352fa265 | ||
|
|
37d84ddd73 | ||
|
|
6f9869e9c2 | ||
|
|
abc061ba46 | ||
|
|
95c80e5677 | ||
|
|
ba1758e5c7 | ||
|
|
36198c1993 | ||
|
|
8708fd8514 | ||
|
|
44a320ee74 | ||
|
|
520b8ecef4 | ||
|
|
34aa43ead3 | ||
|
|
8ecc3b9a78 | ||
|
|
c0d1d0566f | ||
|
|
2e01cab3f8 | ||
|
|
bbe99b5436 | ||
|
|
246ab41479 | ||
|
|
48dd3fa5bf | ||
|
|
7db5fe32a8 | ||
|
|
38a1dfa336 | ||
|
|
8b41b531a6 | ||
|
|
4a048a4918 | ||
|
|
ae64370478 | ||
|
|
b03f355fae | ||
|
|
8085e0031e | ||
|
|
dbc2dcc625 | ||
|
|
ad6227006e | ||
|
|
4f414cf70f | ||
|
|
adb2680289 | ||
|
|
f1f621d178 | ||
|
|
996e162d11 | ||
|
|
840f72a589 | ||
|
|
eca83f83ac | ||
|
|
0f070b494a | ||
|
|
0ea1e0c03d | ||
|
|
c49d9b57ed | ||
|
|
8d7e0f5190 | ||
|
|
012781103a | ||
|
|
34b3bf9e80 | ||
|
|
4c203a4777 | ||
|
|
02c49ee423 | ||
|
|
3f243d1c11 | ||
|
|
cf37d59d47 | ||
|
|
75c10c75d9 | ||
|
|
51e06bdbb0 | ||
|
|
462fb1ae7e | ||
|
|
aea5b91649 | ||
|
|
e3e96d546a | ||
|
|
46c2355a7e | ||
|
|
423e430e04 | ||
|
|
70b11d9015 | ||
|
|
5687f2f5a9 | ||
|
|
b82a8d681d | ||
|
|
29af22bd6e | ||
|
|
68f8127286 | ||
|
|
647e1a06d5 | ||
|
|
84759b5dbd | ||
|
|
39f62374aa | ||
|
|
4de2cbcf9f | ||
|
|
3f9ca423bd | ||
|
|
bbed019ca5 | ||
|
|
3081d76787 | ||
|
|
1a0ddff82b | ||
|
|
e9b40a12c8 | ||
|
|
3adb4afd60 | ||
|
|
c7af67f3a3 | ||
|
|
cca9298e74 | ||
|
|
4f9e23e0bc | ||
|
|
ded62a556b | ||
|
|
508f4d1b52 | ||
|
|
050ccb847a | ||
|
|
b67e61d625 | ||
|
|
9a47ec1ed9 | ||
|
|
c5c27768f2 | ||
|
|
4dd944a8c1 | ||
|
|
e22aff51a4 | ||
|
|
6adc26a897 | ||
|
|
9726726e5a | ||
|
|
6202abb75c | ||
|
|
3c65923d33 | ||
|
|
daed6e1064 | ||
|
|
f566f41fb3 | ||
|
|
e4190a0315 | ||
|
|
ca0d1d0b50 | ||
|
|
4e57226a1b | ||
|
|
a89b76d648 | ||
|
|
d0497fc886 | ||
|
|
a1c007599f | ||
|
|
81040a57a6 | ||
|
|
09fe5a2cb7 | ||
|
|
d760aa41c5 | ||
|
|
53576c6013 | ||
|
|
029466830d | ||
|
|
31595196fc | ||
|
|
2a0b582911 | ||
|
|
63fb30daf8 | ||
|
|
675b29b4fe | ||
|
|
075ccf6a82 | ||
|
|
ae6f3b6753 | ||
|
|
1a87a4cd0c | ||
|
|
f83092865b | ||
|
|
b15736accd | ||
|
|
79ce6f00d3 | ||
|
|
7f43acc167 | ||
|
|
f8fbd6cec8 | ||
|
|
9d11dd0787 | ||
|
|
4c31b6b689 | ||
|
|
d468fae0ea | ||
|
|
3a5a1b4f52 | ||
|
|
0724bd12a5 | ||
|
|
c7e3e642fa | ||
|
|
ee4c8f2a3f | ||
|
|
1092ffc9ea | ||
|
|
f4b6eb9869 | ||
|
|
5e49a4e05b | ||
|
|
adc50f07eb | ||
|
|
874c758340 | ||
|
|
ed031f01ba | ||
|
|
4cbae25ac0 | ||
|
|
7f002ac612 | ||
|
|
9e8fcbc879 | ||
|
|
c8a24a8647 | ||
|
|
e83a970b3a | ||
|
|
514d27fd66 | ||
|
|
16976fce4e | ||
|
|
3dc2304d6e | ||
|
|
7a04a857f2 | ||
|
|
a107cd98db | ||
|
|
d7f52af371 | ||
|
|
f96feaffa1 | ||
|
|
14b3d80360 | ||
|
|
aad042d7ad | ||
|
|
0154cce46c | ||
|
|
a269930ec3 | ||
|
|
6b80aae7d1 | ||
|
|
e1dbed816c | ||
|
|
e423390050 | ||
|
|
338f04b2f2 | ||
|
|
fca7120210 | ||
|
|
826cc0d685 | ||
|
|
90b4c58457 | ||
|
|
857f135748 | ||
|
|
a0ed846a59 | ||
|
|
6c189f331f | ||
|
|
1f8fb48727 | ||
|
|
96f2be056d | ||
|
|
8bdd464239 | ||
|
|
c1141b57fa | ||
|
|
364646fc6b | ||
|
|
fdcce53823 | ||
|
|
d8e6ae41f1 | ||
|
|
2042b8aa4c | ||
|
|
f928db298d | ||
|
|
da9f683c22 | ||
|
|
4e41f4ec5e | ||
|
|
7ddc132af5 | ||
|
|
23cc774f96 | ||
|
|
bac04a0f3c | ||
|
|
f1c52d01aa | ||
|
|
b052e09cd6 | ||
|
|
bdf61b146c | ||
|
|
6322b4cbe1 | ||
|
|
6e754ed356 | ||
|
|
5a4fb94be3 | ||
|
|
57595bbab4 | ||
|
|
b0b09aea49 | ||
|
|
1af774dcb7 | ||
|
|
b65b717fbc | ||
|
|
c55993fe88 | ||
|
|
7361114ccb | ||
|
|
68648c299f | ||
|
|
8465fc4818 | ||
|
|
dc39eb58c7 | ||
|
|
5964a99e57 | ||
|
|
53257c17a9 | ||
|
|
86adcf0653 | ||
|
|
2471ba0db8 | ||
|
|
17416cb535 | ||
|
|
eec0e23620 | ||
|
|
d3c23c1994 | ||
|
|
58685f2bb4 | ||
|
|
25e9d02a8a | ||
|
|
66c565a72f | ||
|
|
24f02a76f5 | ||
|
|
828a973339 | ||
|
|
f2d5028d7c | ||
|
|
705b73afc1 | ||
|
|
8c533ff12c | ||
|
|
bebaac49e3 | ||
|
|
ebe34005a1 | ||
|
|
101fa42e55 | ||
|
|
332575e53b | ||
|
|
fa4668aa3f | ||
|
|
3a07970216 | ||
|
|
a3f350f905 | ||
|
|
8919129c95 | ||
|
|
f339018a5e | ||
|
|
433b88e579 | ||
|
|
a4356d786e | ||
|
|
c36fee7fda | ||
|
|
9c189f9083 | ||
|
|
2bd19f999d | ||
|
|
a39f46854e | ||
|
|
b7d71f8c59 | ||
|
|
beff7a246d | ||
|
|
adb550a9f2 | ||
|
|
1c31728a26 | ||
|
|
f00f2eff21 | ||
|
|
dec858dae9 | ||
|
|
d87676027e | ||
|
|
704af1011a | ||
|
|
96e11916af | ||
|
|
009671090f | ||
|
|
592003ecb6 | ||
|
|
eeace79ef9 | ||
|
|
8b139890b4 | ||
|
|
df08355b1d | ||
|
|
81ba459fc3 | ||
|
|
1d8b2f8342 | ||
|
|
1d378a6827 | ||
|
|
2fcfb80e8e | ||
|
|
ac674ecd86 | ||
|
|
bb91c4a81a | ||
|
|
1b505d0f39 | ||
|
|
ec05b30106 | ||
|
|
80144c85a1 | ||
|
|
f4d305cc03 | ||
|
|
72ecb56250 | ||
|
|
b712be346c | ||
|
|
bee5f1cbe9 | ||
|
|
8c107dcb7d | ||
|
|
7554a52732 | ||
|
|
022836c43c | ||
|
|
64467b0338 | ||
|
|
318cb3dc6e | ||
|
|
ce6c5dcf31 | ||
|
|
0524271573 | ||
|
|
f3dff9e496 | ||
|
|
4daefcf1f7 | ||
|
|
4b31de057c | ||
|
|
68b0410d1b | ||
|
|
6f7d5378e7 | ||
|
|
05bd2a5aae | ||
|
|
e9adf91a27 | ||
|
|
404e797cd9 | ||
|
|
58e2856612 | ||
|
|
f6bb01545d | ||
|
|
49bebcdd7b | ||
|
|
4238a7f704 | ||
|
|
782d6d7e89 | ||
|
|
7355a741b3 | ||
|
|
a9727ceaa6 | ||
|
|
39670ac84e | ||
|
|
78b2eb8b18 | ||
|
|
a89f43a1b7 | ||
|
|
8829692295 | ||
|
|
eb388ad04f | ||
|
|
aee3d30e50 | ||
|
|
dbd0b7b23b | ||
|
|
68dd972bfa | ||
|
|
1c606a242a | ||
|
|
e23581e260 | ||
|
|
d5c6e4eb58 | ||
|
|
3899980b2b | ||
|
|
f170cb434c | ||
|
|
54da59f003 | ||
|
|
507de82aba | ||
|
|
fcab270f25 | ||
|
|
42bffc4234 | ||
|
|
7db68da8c6 | ||
|
|
0a0b5ecade | ||
|
|
7212e20a76 | ||
|
|
e570997ea0 | ||
|
|
4337534437 | ||
|
|
7b624b8622 | ||
|
|
3cd20f0e33 | ||
|
|
cc9832653a | ||
|
|
1d86d33b0b | ||
|
|
18c7a838b0 | ||
|
|
3df4c4d14a | ||
|
|
ed8082868f | ||
|
|
aba98a3a03 | ||
|
|
e9b49b8e9e | ||
|
|
dcdfb4d3e7 | ||
|
|
a2d31d1d8d | ||
|
|
2e9c62b4f8 | ||
|
|
dfcb3cdcc6 | ||
|
|
8d1bdca1de | ||
|
|
da031b8d15 | ||
|
|
0befa34dc8 | ||
|
|
ddc503d296 | ||
|
|
e7bd1533c4 | ||
|
|
f35fa7f4d0 | ||
|
|
5e1d18c9e8 | ||
|
|
b3de4a76d5 | ||
|
|
b9a6a9c5a8 | ||
|
|
703487e608 | ||
|
|
338a9cd717 | ||
|
|
815c8695fc | ||
|
|
00860c29d0 | ||
|
|
49d3789f98 | ||
|
|
ab97e736d9 | ||
|
|
4d5fa05711 | ||
|
|
4f557dac4c | ||
|
|
86447ab0c9 | ||
|
|
9bba330f2c | ||
|
|
2ed0ea48e6 | ||
|
|
74e94b2628 | ||
|
|
2ea835ec9f | ||
|
|
53dd4f0b1b | ||
|
|
d7dd454044 | ||
|
|
24cd4938a5 | ||
|
|
e67cbee1d5 | ||
|
|
0a8ee87319 | ||
|
|
7f2a05c364 | ||
|
|
5034e7b4f6 | ||
|
|
10322602da | ||
|
|
a7016c1cfa | ||
|
|
442afd07f6 | ||
|
|
022a6e5159 | ||
|
|
a4622002d9 | ||
|
|
babcb6e987 | ||
|
|
e61b4e47f5 | ||
|
|
3044d8a5ba | ||
|
|
a902530773 | ||
|
|
1f7c2625f7 | ||
|
|
1731891113 | ||
|
|
efc93979d1 | ||
|
|
421cf410a2 | ||
|
|
f8cb2a413f | ||
|
|
c20f244d66 | ||
|
|
cd35384ae8 | ||
|
|
2655d67f52 | ||
|
|
b3a9364a97 | ||
|
|
5fdfa544db | ||
|
|
c123e32ddc | ||
|
|
5b7160c492 | ||
|
|
0141b240bc | ||
|
|
0ddbcdca06 | ||
|
|
727c19b9c0 | ||
|
|
edf41096d7 | ||
|
|
90d52c6561 | ||
|
|
c4ada0d998 | ||
|
|
ca25c253d5 | ||
|
|
7263388b7c | ||
|
|
e0c7a3deb2 | ||
|
|
d3202c03ba | ||
|
|
2d0c49f4b4 | ||
|
|
97d49395ba | ||
|
|
112c419403 | ||
|
|
a47c6b9d55 | ||
|
|
08582b7f82 | ||
|
|
20abde5542 | ||
|
|
2a9e91dfe8 | ||
|
|
09aa7e113c | ||
|
|
67b806860e | ||
|
|
19cc30a3c1 | ||
|
|
0f914d1ea6 | ||
|
|
17ea120032 | ||
|
|
903b07bfbb | ||
|
|
4f61f58b0d | ||
|
|
4e7663079b | ||
|
|
4c1a16e2ee | ||
|
|
1a8e6f5cb6 | ||
|
|
2b587f7047 | ||
|
|
1ae711279e | ||
|
|
5e8b53ef46 | ||
|
|
d6beaa43e3 | ||
|
|
3ce907db79 | ||
|
|
201d3bbffd | ||
|
|
4625e9bdf3 | ||
|
|
d285764592 | ||
|
|
9b513ce4ab | ||
|
|
ed7beb529a | ||
|
|
44fb7cfe38 | ||
|
|
5c9bc541f9 | ||
|
|
f7147648ba | ||
|
|
30b72cb7f4 | ||
|
|
ab08abcaf9 | ||
|
|
0227d987fe | ||
|
|
5df01af720 | ||
|
|
0acf4bca0d | ||
|
|
2e37af8c01 | ||
|
|
245914cc9a | ||
|
|
201ee5b708 | ||
|
|
744ded7f1b | ||
|
|
5a7f4dff47 | ||
|
|
dd6ba0995d | ||
|
|
5697f7c953 | ||
|
|
144b22a3b9 | ||
|
|
3c57e169d1 |
+32
-5
@@ -1,6 +1,33 @@
|
|||||||
.DS_Store
|
*.py[co]
|
||||||
*.pyc
|
|
||||||
|
# Packages
|
||||||
*.egg
|
*.egg
|
||||||
.project
|
*.egg-info
|
||||||
.pydevproject
|
dist
|
||||||
.settings
|
*build
|
||||||
|
eggs
|
||||||
|
parts
|
||||||
|
bin
|
||||||
|
var
|
||||||
|
sdist
|
||||||
|
develop-eggs
|
||||||
|
.installed.cfg
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
.coverage
|
||||||
|
.tox
|
||||||
|
|
||||||
|
#Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
#Mr Developer
|
||||||
|
.mr.developer.cfg
|
||||||
|
|
||||||
|
#Virtualenv
|
||||||
|
env/
|
||||||
|
|
||||||
|
#Editor temporaries
|
||||||
|
*~
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
language: python
|
||||||
|
|
||||||
|
python:
|
||||||
|
- "2.6"
|
||||||
|
- "2.7"
|
||||||
|
- "pypy"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- pip install . --quiet
|
||||||
|
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib --quiet --use-mirrors; fi"
|
||||||
|
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee py-bcrypt MySQL-python --quiet
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- mysql -e 'create database flask_security_test;'
|
||||||
|
|
||||||
|
services:
|
||||||
|
- mongodb
|
||||||
|
|
||||||
|
script: nosetests
|
||||||
|
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- develop
|
||||||
@@ -3,15 +3,158 @@ Flask-Security Changelog
|
|||||||
|
|
||||||
Here you can see the full list of changes between each Flask-Security release.
|
Here you can see the full list of changes between each Flask-Security release.
|
||||||
|
|
||||||
|
Version 1.6.7
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released July 11th 2013
|
||||||
|
|
||||||
|
- Made password length form error message configurable
|
||||||
|
- Fixed email confirmation bug that prevented logged in users from confirming their email
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.6
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 28th 2013
|
||||||
|
|
||||||
|
- Fixed dependency versions
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.5
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 20th 2013
|
||||||
|
|
||||||
|
- Fixed bug in `flask.ext.security.confirmable.generate_confirmation_link`
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.4
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 18th 2013
|
||||||
|
|
||||||
|
- Added `SECURITY_DEFAULT_REMEMBER_ME` configuration value to unify behavior between endpoints
|
||||||
|
- Fixed Flask-Login dependency problem
|
||||||
|
- Added optional `next` parameter to registration endpoint, similar to that of login
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released May 8th 2013
|
||||||
|
|
||||||
|
- Fixed bug in regards to imports with latest version of MongoEngine
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released April 4th 2013
|
||||||
|
|
||||||
|
- Fixed bug with http basic auth
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released April 3rd 2013
|
||||||
|
|
||||||
|
- Fixed bug with signals
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released March 13th 2013
|
||||||
|
|
||||||
|
- Added Flask-Pewee support
|
||||||
|
- Password hashing is now more flexible and can be changed to a different type at will
|
||||||
|
- Flask-Login messages are configurable
|
||||||
|
- AJAX requests must now send a CSRF token for security reasons
|
||||||
|
- Form messages are now configurable
|
||||||
|
- Forms can now be extended with more fields
|
||||||
|
- Added change password endpoint
|
||||||
|
- Added the user to the request context when successfully authenticated via http basic and token auth
|
||||||
|
- The Flask-Security blueprint subdomain is now configurable
|
||||||
|
- Redirects to other domains are now not allowed during requests that may redirect
|
||||||
|
- Template paths can be configured
|
||||||
|
- The welcome/register email can now optionally be sent to the user
|
||||||
|
- Passwords can now contain non-latin characters
|
||||||
|
- Fixed a bug when confirming an account but the account has been deleted
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.4
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released January 6th 2013
|
||||||
|
|
||||||
|
- Fix bug in forms with `csrf_enabled` parameter not accounting attempts to login using JSON data
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released December 23rd 2012
|
||||||
|
|
||||||
|
- Change dependency requirement
|
||||||
|
|
||||||
|
Version 1.5.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released December 11th 2012
|
||||||
|
|
||||||
|
- Fix a small bug in `flask_security.utils.login_user` method
|
||||||
|
|
||||||
|
Version 1.5.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released November 26th 2012
|
||||||
|
|
||||||
|
- Fixed bug with `next` form variable
|
||||||
|
- Added better documentation regarding Flask-Mail configuration
|
||||||
|
- Added ability to configure email subjects
|
||||||
|
|
||||||
|
Version 1.5.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released October 11th 2012
|
||||||
|
|
||||||
|
- Major release. Upgrading from previous versions will require a bit of work to
|
||||||
|
accomodate API changes. See documentation for a list of new features and for
|
||||||
|
help on how to upgrade.
|
||||||
|
|
||||||
|
Version 1.2.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released June 12th 2012
|
||||||
|
|
||||||
|
- Fixed a bug in the RoleMixin eq/ne functions
|
||||||
|
|
||||||
|
Version 1.2.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released April 27th 2012
|
||||||
|
|
||||||
|
- Fixed bug where `roles_required` and `roles_accepted` did not pass the next
|
||||||
|
argument to the login view
|
||||||
|
|
||||||
|
Version 1.2.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released March 28th 2012
|
||||||
|
|
||||||
|
- Added optional user model mixin parameter for datastores
|
||||||
|
- Added CreateRoleCommand to available Flask-Script commands
|
||||||
|
|
||||||
Version 1.2.0
|
Version 1.2.0
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Released March 12th, 2012
|
Released March 12th 2012
|
||||||
|
|
||||||
- Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a
|
- Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a
|
||||||
boolean value to specify if Flask-Security should flash messages or not.
|
boolean value to specify if Flask-Security should flash messages or not.
|
||||||
|
|
||||||
Version 1.1.0
|
Version 1.1.0
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Initial release
|
Initial release
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (C) 2012 by Matt Wright
|
Copyright (C) 2012 by Matthew Wright
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
+2
-1
@@ -1 +1,2 @@
|
|||||||
include tests/*.py
|
recursive-include tests *.py
|
||||||
|
recursive-include flask_security/templates *.*
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# Flask-Security
|
|
||||||
|
|
||||||
Simple security for Flask applications combining [Flask-Login](http://packages.python.org/Flask-Login/), [Flask-Principal](http://packages.python.org/Flask-Principal/), [Flask-WTF](http://packages.python.org/Flask-WTF/), [passlib](http://packages.python.org/passlib/), and your choice of datastore. Currently [SQLAlchemy](http://www.sqlalchemy.org) via [Flask-SQLAlchemy](http://packages.python.org/Flask-SQLAlchemy/) and [MongoEngine](http://www.mongoengine.org) via [Flask-MongoEngine](https://github.com/sbook/flask-mongoengine) are supported out of the box. You will need to install the necessary Flask extensions that you'll be using. Additionally, you may need to install an encryption library such as [py-bcrypt](http://www.mindrot.org/projects/py-bcrypt/) to support bcrypt passwords.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Flask-Security does a few things that Flask-Login and Flask-Principal don't provide out of the box. They are:
|
|
||||||
|
|
||||||
1. Setting up login and logout endpoints
|
|
||||||
2. Authenticating users based on username or email
|
|
||||||
3. Limiting access based on user 'roles'
|
|
||||||
4. User and role creation
|
|
||||||
5. Password encryption
|
|
||||||
|
|
||||||
That being said, you can still hook into things such as the Flask-Login and Flask-Principal signals if need be.
|
|
||||||
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
First, install Flask-Security:
|
|
||||||
|
|
||||||
$ mkvirtualenv app-name
|
|
||||||
$ pip install https://github.com/mattupstate/flask-security/tarball/master
|
|
||||||
|
|
||||||
Then install your datastore requirement.
|
|
||||||
|
|
||||||
SQLAlchemy:
|
|
||||||
|
|
||||||
$ pip install Flask-SQLAlchemy
|
|
||||||
|
|
||||||
MongoEngine:
|
|
||||||
|
|
||||||
$ pip install https://github.com/sbook/flask-mongoengine/tarball/master
|
|
||||||
|
|
||||||
Beyond this, the best place to get started at the moment is to look at the example application(s) and corresponding tests. The example apps are currently used to test Flask-Security as well so they are solid examples of most, if not all, features. Configuration options are illustrated in the tests as well. To run the example run do the following:
|
|
||||||
|
|
||||||
$ mkvirtualenv flask-security
|
|
||||||
$ git clone git://github.com/mattupstate/flask-security.git
|
|
||||||
$ cd flask-security
|
|
||||||
$ pip install Flask Flask-Login Flask-Principal Flask-SQLALchemy passlib
|
|
||||||
$ pip install https://github.com/sbook/flask-mongoengine/tarball/master
|
|
||||||
$ python example/app.py
|
|
||||||
|
|
||||||
## Code Examples
|
|
||||||
|
|
||||||
If you don't want to checkout the example quite yet, here are some hypothetical examples to give you a sense of how Flask-Security works:
|
|
||||||
|
|
||||||
### Setup SQLAlchemy
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask.ext.security import Security
|
|
||||||
from flask.ext.security.datastore.sqlalchemy import SQLAlchemyDatastore
|
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.config['SECRET_KEY'] = 'something'
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
|
||||||
|
|
||||||
db = SQLALchemy(app)
|
|
||||||
Security(app, SQLAlchemyDatastore(db))
|
|
||||||
|
|
||||||
### Require a logged in user:
|
|
||||||
|
|
||||||
from flask import render_template
|
|
||||||
from flask.ext.security import login_required
|
|
||||||
|
|
||||||
… application setup …
|
|
||||||
|
|
||||||
@app.route('/profile')
|
|
||||||
@login_required
|
|
||||||
def profile():
|
|
||||||
return render_template('profile.html')
|
|
||||||
|
|
||||||
### Require an admin:
|
|
||||||
|
|
||||||
from flask import render_template
|
|
||||||
from flask.ext.security import roles_required
|
|
||||||
|
|
||||||
… application setup …
|
|
||||||
|
|
||||||
@app.route('/admin')
|
|
||||||
@roles_required('admin')
|
|
||||||
def admin():
|
|
||||||
return render_template('admin/index.html')
|
|
||||||
|
|
||||||
### Require any of the specified roles:
|
|
||||||
|
|
||||||
from flask import render_template
|
|
||||||
from flask.ext.security import roles_accepted
|
|
||||||
|
|
||||||
… application setup …
|
|
||||||
|
|
||||||
@app.route('/admin')
|
|
||||||
@roles_accepted('admin', 'editor', 'author')
|
|
||||||
def admin():
|
|
||||||
return render_template('admin/index.html')
|
|
||||||
|
|
||||||
### Showing a link in a template only for an admin:
|
|
||||||
|
|
||||||
{% if current_user.has_role('admin') %}
|
|
||||||
<a href="{{ url_for('admin.index') }}">Admin Panel</a>
|
|
||||||
{$ endif %}
|
|
||||||
|
|
||||||
## Flask-Script Commands
|
|
||||||
|
|
||||||
Flask-Security comes packed with a few Flask-Script commands. They are:
|
|
||||||
|
|
||||||
* `flask.ext.security.script.CreateUserCommand`
|
|
||||||
* `flask.ext.security.script.AddRoleCommand`
|
|
||||||
* `flask.ext.security.script.RemoveRoleCommand`
|
|
||||||
* `flask.ext.security.script.DeactivateUserCommand`
|
|
||||||
* `flask.ext.security.script.ActivateUserCommand`
|
|
||||||
|
|
||||||
Register these on your script manager for pure convenience.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Feel free to fork and contribute. If you decided to do so, just be sure to include relevant tests that you feel are necessary. To run the tests, please provide instructions for any requirements. For instance, if you write a new datastore implementation, please provide instructions on how best to setup a connection when testing.
|
|
||||||
|
|
||||||
If you plan on running all the provided tests you'll need a local installation of MongoDB running on the standard port 27017 without username/password protection.
|
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
Flask-Security
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. image:: https://secure.travis-ci.org/mattupstate/flask-security.png?branch=develop
|
||||||
|
|
||||||
|
Flask-Security quickly adds security features to your Flask application.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `Documentation <http://packages.python.org/Flask-Security/>`_
|
||||||
|
- `Issue Tracker <http://github.com/mattupstate/flask-security/issues>`_
|
||||||
|
- `Code <http://github.com/mattupstate/flask-security/>`_
|
||||||
|
- `Development Version
|
||||||
|
<http://github.com/mattupstate/flask-security/zipball/develop#egg=Flask-Security-dev>`_
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="184.621px" height="267.873px" viewBox="0 0 184.621 267.873" style="enable-background:new 0 0 184.621 267.873;"
|
||||||
|
xml:space="preserve">
|
||||||
|
<path d="M42.3,6.712c0.374-3.417,6.743-3.01,6.24,0C48.174,8.905,43.253,8.469,42.3,6.712z M46.14,7.432
|
||||||
|
c0.102-0.539,1.273-0.006,1.44-0.48c-0.011-0.331,0.113-0.527,0.24-0.72c-0.386-1.446-4.505-1.467-4.32,0.479
|
||||||
|
c0.463,0.287,1.217,0.915,1.68,0.48c0.409-0.099-0.051-0.31,0-0.72c0.689-0.053,1.668-1.192,1.92-0.24
|
||||||
|
C47.161,6.872,45.517,7.023,46.14,7.432z"/>
|
||||||
|
<path d="M61.02,8.872c-1.514,1.428-5.308,0.636-6.239-0.72C54.424,4.301,62.875,4.691,61.02,8.872z M55.98,8.152
|
||||||
|
c0.547,0.174,1.227,0.214,1.681,0.48c-0.31-1.429,1.293-0.947,2.16-1.2c0.022,0.377-0.032,0.832,0.479,0.72
|
||||||
|
C60.766,5.738,55.542,5.836,55.98,8.152z"/>
|
||||||
|
<path d="M75.18,11.272c-2.44,1.066-5.342-0.024-6.479-1.68C69,5.478,77.3,6.38,75.18,11.272z M70.14,9.352
|
||||||
|
c0.348,0.453,0.536,1.064,1.44,0.96c-0.42-1.412,1.778-1.107,2.16-0.96c-0.09,0.79-1.276,0.484-1.2,1.44
|
||||||
|
c1.447,0.248,1.821-0.578,2.16-1.44C74.528,7.553,70.227,7.19,70.14,9.352z"/>
|
||||||
|
<path d="M38.701,9.352c-0.632,0.249-1.473,0.287-2.4,0.24C36.181,8.614,38.749,8.071,38.701,9.352z"/>
|
||||||
|
<path d="M44.94,9.352c-0.336,1.452-3.19,0.276-4.319,0.24C41.392,8.351,43.547,9.639,44.94,9.352z"/>
|
||||||
|
<path d="M51.661,10.792c-2.162,0.242-3.118-0.722-5.28-0.48C46.883,9.482,51.405,9.765,51.661,10.792z"/>
|
||||||
|
<path d="M86.22,16.072c-2.685,0.164-5.312-0.956-5.28-2.88C81.011,8.895,91.422,11.543,86.22,16.072z M82.14,13.192
|
||||||
|
c0.572,1.569,2.509,2.415,4.32,1.44c0.099-1.059-0.065-1.855-0.48-2.4C84.701,11.79,82.343,11.814,82.14,13.192z"/>
|
||||||
|
<path d="M60.3,11.992c-1.742,0.618-4.228-0.089-5.76-0.48C55.288,10.927,59.201,11.604,60.3,11.992z"/>
|
||||||
|
<path d="M66.781,13.672c-1.624-0.216-3.132-0.548-4.561-0.96C63.243,11.997,65.987,12.678,66.781,13.672z"/>
|
||||||
|
<path d="M85.98,13.432c0.126,1.215-1.898,1.554-2.399,0.72C83.832,13.513,85.72,13.266,85.98,13.432z"/>
|
||||||
|
<path d="M70.14,14.392c3.491-0.13,7.434,1.367,11.04,1.92c-0.615,0.954-2.605-0.277-3.6-0.48
|
||||||
|
C74.54,15.913,72.676,14.816,70.14,14.392z"/>
|
||||||
|
<path d="M84.3,16.792c1.247,0.273,2.713,0.327,3.84,0.72C87.493,17.593,84.601,17.856,84.3,16.792z"/>
|
||||||
|
<path d="M43.5,20.632c-0.992,0.528-1.736,1.304-1.68,2.88c-0.07,1.03,1.131,0.789,1.199,1.68
|
||||||
|
C40.576,24.951,40.535,20.329,43.5,20.632z"/>
|
||||||
|
<path d="M43.98,20.872c2.421-0.874,2.685,4.746-0.479,4.08c0.299-0.661,1.226-0.694,1.68-1.2
|
||||||
|
C45.374,22.199,45.004,21.208,43.98,20.872z"/>
|
||||||
|
<path d="M32.46,29.752c-0.48,0.342,0.184,1.561,0.72,1.68c-1.511,0.933-1.827-2.226-0.96-3.12c3.597-2.01,5.532,3.849,1.44,3.6
|
||||||
|
C33.964,30.488,32.242,31.091,32.46,29.752z"/>
|
||||||
|
<path d="M25.741,33.112c1.15,0.084-0.513,0.719-0.48,1.2c0.018,1.183-0.203,2.604,0.96,2.64c-0.592-0.841,0.243-2.458,0.72-3.6
|
||||||
|
c2.731,0.658,1.781,4.199-0.239,4.32C24.472,37.806,23.329,34.219,25.741,33.112z"/>
|
||||||
|
<path d="M38.22,42.712c-0.253-1.106-1.293-1.427-0.96-3.12c-0.61,0.029-0.931,0.349-0.96,0.96
|
||||||
|
C35.887,35.461,44.143,41.79,38.22,42.712z"/>
|
||||||
|
<path d="M47.1,46.072c-1.321-2.109-1.041-5.914,2.88-4.8C52.478,43.065,50.231,46.405,47.1,46.072z M46.861,42.472
|
||||||
|
c0.049,0.751-0.162,1.762,0.239,2.16c0.739,0.179,0.462-0.659,1.2-0.48c-0.267,0.753-0.112,0.634,0.24,1.2
|
||||||
|
C52.363,44.767,48.946,39.561,46.861,42.472z"/>
|
||||||
|
<path d="M37.5,42.232C36.934,43.004,36.53,41.388,37.5,42.232L37.5,42.232z"/>
|
||||||
|
<path d="M30.781,42.952c0.56,0,1.12,0,1.68,0c1.986,1.451,0.51,4.935-2.4,3.6c-0.156-0.835-0.882-2.003-0.239-2.88
|
||||||
|
c0.47,0.97-0.006,2.885,1.68,2.64C31.005,45.534,30.001,44.231,30.781,42.952z"/>
|
||||||
|
<path d="M4.861,47.512c0.233,0.006,0.186,0.293,0.239,0.48c-0.204,1.471-1.334,2.713-1.92,4.08c-0.557,1.3-0.519,3.166-1.92,3.84
|
||||||
|
C1.854,52.506,3.317,49.968,4.861,47.512z"/>
|
||||||
|
<path d="M54.3,62.632c2.747,3.82-5.6,6.558-4.08,0.72C50.871,62.01,52.984,62.126,54.3,62.632z M53.821,63.592
|
||||||
|
c-1.709-2.567-5,1.958-2.16,1.92c-0.536-0.474,1.761-1.205,1.68,0C53.614,64.986,53.959,64.531,53.821,63.592z"/>
|
||||||
|
<path d="M53.821,85.192c0.891-1.578,4.269-2.012,5.279-0.24C60.584,90.521,52.591,90.472,53.821,85.192z M57.901,84.952
|
||||||
|
c-0.284-0.116-0.545-0.256-0.721-0.48c-2.651-0.412-3.296,2.625-1.68,3.84C57.95,88.96,58.699,86.766,57.901,84.952z"/>
|
||||||
|
<path d="M56.701,86.152c0.16,0,0.319,0,0.479,0c0,0.16,0,0.32,0,0.48c-0.16,0-0.319,0-0.479,0
|
||||||
|
C56.701,86.472,56.701,86.312,56.701,86.152z"/>
|
||||||
|
<path d="M55.5,87.352c0.621-0.306,1.539-0.306,2.16,0C57.558,88.531,55.73,88.054,55.5,87.352z"/>
|
||||||
|
<path d="M47.821,95.272c0.654,3.768-3.346,3.171-3.601,1.2C43.964,94.484,46.394,93.683,47.821,95.272z M45.901,97.432
|
||||||
|
c0.159-0.334-0.05-1.905-0.721-2.16C44.746,95.882,44.954,97.453,45.901,97.432z"/>
|
||||||
|
<path d="M40.861,98.632c0.105,1.306-0.608,1.792-0.96,2.64c-0.801,0-1.601,0-2.4,0C35.321,99.4,38.75,95.462,40.861,98.632z
|
||||||
|
M38.94,101.032c-0.208-0.752-0.594-1.325-1.199-1.68C37.674,100.378,38.437,100.576,38.94,101.032z"/>
|
||||||
|
<path d="M61.5,108.232c0.545,1.2,0.064,2.764-0.24,3.84C55.917,115.484,55.839,104.695,61.5,108.232z M57.661,110.632
|
||||||
|
c0.652,3.805,5.875-1.417,1.439-2.4C58.46,108.886,57.509,109.749,57.661,110.632z"/>
|
||||||
|
<path d="M40.621,113.512c-0.878-0.801-1.726-1.134-1.44-2.64c0.344-1.811,3.449-2.23,3.84-0.24
|
||||||
|
C43.258,111.84,42.221,112.906,40.621,113.512z M40.861,112.552c-0.223-0.578-0.438-1.162-0.721-1.68
|
||||||
|
c-0.075,0.325-0.342,0.458-0.239,0.96C40.671,111.621,40.229,112.623,40.861,112.552z"/>
|
||||||
|
<path d="M59.581,109.672c0.295,0.105,0.493,0.308,0.479,0.72c-0.388,0.068-0.413-0.227-0.72-0.24
|
||||||
|
C59.386,109.958,59.582,109.913,59.581,109.672z"/>
|
||||||
|
<path d="M58.621,110.632c0.744-0.024,1.146,0.293,1.68,0.48C60.05,111.75,58.254,111.922,58.621,110.632z"/>
|
||||||
|
<path d="M64.621,135.353c0.086-1.606-0.272-2.769-0.96-3.601c-3.562-0.15-7.859,0.044-11.28,0.72c0.133,0.987,0.688,1.552,0.72,2.64
|
||||||
|
c-0.504,0.299-1.229,1.1-1.92,0.479c-0.299-1.02,0.799-0.642,0.96-1.2c0.019-1.458-0.993-1.886-0.72-3.6
|
||||||
|
c1.929,0.088,2.346-0.335,3.36,0.24c0.645,0.165,0.464-0.496,0.72-0.72c0.646-0.165,0.464,0.496,0.72,0.72
|
||||||
|
c0.461-0.957,1.359-0.047,1.681-0.96c0.645-0.166,0.463,0.496,0.72,0.72c0.422-0.058,0.547-0.413,0.479-0.96
|
||||||
|
c0.505,0.528,0.937,0.528,1.44,0c0.523-0.043,0.764,0.196,0.72,0.72c0.946-0.028,0.734-1.043,1.68-0.479
|
||||||
|
c0.631-0.161,0.505,0.614,0.721,0.72c0.215,0.105,0.423-0.477,0.479-0.48c0.373-0.021,0.549,1.291,1.92,0.72
|
||||||
|
c0.171,1.864,0.423,2.749,0.721,4.08c3.811,0.014,0.37-4.601,0-6.24c-5.258-0.676-12.689,0.141-17.521,1.44
|
||||||
|
c0.228,1.728,1.729,3.344,1.2,5.041c-1.065-1.496-2.048-3.073-2.4-5.28c0.233-0.007,0.187-0.294,0.24-0.48
|
||||||
|
c4.899-1.854,12.457-2.218,18.48-2.16c0.611,0.645,1.104,1.897,1.68,3.36c0.457,1.163,1.41,2.914,1.2,3.84
|
||||||
|
C69.249,136.444,66.248,137.033,64.621,135.353z"/>
|
||||||
|
<path d="M44.22,138.231c0.101,2.525-3.294,3.207-5.04,2.4C39.008,138.113,42.058,137.625,44.22,138.231z M40.14,140.394
|
||||||
|
c1.207,0.246,1.316-0.604,1.92-0.961c0.345,0.366,1.251,0.233,1.2-0.479C42.089,138.349,39.939,138.825,40.14,140.394z"/>
|
||||||
|
<path d="M64.38,138.474c3.496-0.535,7.481,0.518,10.08,1.68c-0.366,1.195-1.776,0.125-2.4,0
|
||||||
|
C69.596,139.659,66.998,138.842,64.38,138.474z"/>
|
||||||
|
<path d="M95.581,143.271c-2.942,0.644-5.514-1.002-6-3.6C91.774,136.823,96.583,138.774,95.581,143.271z M91.98,139.192
|
||||||
|
c-0.631,0.644-1.162,2.258,0.24,2.398c-0.14-0.859,0.711-0.729,1.44-0.72c0.058,0.343,0.395,0.405,0.479,0.72
|
||||||
|
c-0.166,0.475-1.339-0.059-1.438,0.48c0.669,0.305,1.187,0.592,1.92,0.24C94.852,140.16,93.672,139.42,91.98,139.192z"/>
|
||||||
|
<path d="M32.22,145.192c-0.032,2.367-1.528,3.271-3.84,3.359C27.594,146.242,29.819,143.58,32.22,145.192z M29.1,147.832
|
||||||
|
c1.404-0.115,1.685-1.354,2.4-2.16C30.131,144.871,28.847,146.213,29.1,147.832z"/>
|
||||||
|
<path d="M86.701,146.151c1.792,0.688,3.44,1.52,4.56,2.881C89.319,148.494,87.486,147.847,86.701,146.151z"/>
|
||||||
|
<path d="M114.061,170.151c-0.939,0.369-1.713-1.262-2.16-2.159c-3-4.039-7.217-6.863-10.319-10.8
|
||||||
|
c-3.623-1.817-5.706-5.176-9.36-6.961c0.085-0.274,0.423-0.607,0.48-0.239c3.505,1.541,6.238,4.271,9.119,6.72
|
||||||
|
c1.355,1.153,2.771,2.489,4.08,3.841C108.777,163.521,112.146,165.974,114.061,170.151z"/>
|
||||||
|
<path d="M114.541,155.271c-2.948-0.092-4.854-1.227-4.8-4.318C112.334,147.631,117.725,152.144,114.541,155.271z M110.939,151.433
|
||||||
|
c0.082,1.918,1.41,2.592,3.121,2.881c0.135-0.347,0.375-0.586,0.721-0.722C115.197,151.412,112.15,149.688,110.939,151.433z"/>
|
||||||
|
<path d="M111.9,152.151c0.914,0.046,1.58,0.342,1.681,1.199C112.934,153.824,111.773,153.204,111.9,152.151z"/>
|
||||||
|
<path d="M130.621,163.912c-2.326,0.858-4.162-0.562-4.32-2.642C127.866,158.646,132.686,160.926,130.621,163.912z M127.741,161.992
|
||||||
|
c1.332-0.946,1.198,0.989,1.68,1.2c0.062-0.338,0.526-0.274,0.72-0.48c-0.092-0.788-0.701-1.059-0.959-1.68
|
||||||
|
C128.372,160.879,127.077,161.467,127.741,161.992z"/>
|
||||||
|
<path d="M146.939,179.992c-3.486,2.58-6.062-3.995-3.358-5.04C146.005,174.017,147.825,177.625,146.939,179.992z M145.741,179.512
|
||||||
|
c1.25-1.596-0.574-3.557-1.921-4.079C143.225,177.119,143.824,179.551,145.741,179.512z"/>
|
||||||
|
<path d="M144.061,177.353c0.805-0.164,0.877,0.402,0.959,0.961C144.27,178.424,143.949,178.104,144.061,177.353z"/>
|
||||||
|
<path d="M153.42,206.151c0.619,0.422,0.499,1.582,0.721,2.4C153.458,208.192,153.473,207.14,153.42,206.151z"/>
|
||||||
|
<path d="M170.701,263.271c1.24,0.039,1.096,1.465,2.64,1.201c0.423-0.059,0.548-0.414,0.479-0.961c0.088-1.207-2.1-0.139-1.68-1.68
|
||||||
|
c1.793-0.824,3.146,0.895,3.119,1.92C175.193,266.483,169.829,266.367,170.701,263.271z"/>
|
||||||
|
<path d="M166.621,263.271c0.523,0.037,0.516,0.605,0.479,1.201C166.387,264.522,166.254,263.616,166.621,263.271z"/>
|
||||||
|
<path d="M29.581,13.432c-1.118,1.762-2.535,3.225-4.561,4.08c-0.978,1.865-3.021,2.788-4.319,4.32
|
||||||
|
c-0.634,0.748-1.043,1.827-1.681,2.64c-0.617,0.788-1.547,1.365-2.159,2.16c-1.095,1.42-1.889,3.176-2.881,4.8
|
||||||
|
c-2.798,4.579-5.413,9.826-8.159,14.88c-0.323-1.007,0.498-2.206,0.96-3.12c1.375-2.724,2.946-5.975,4.56-8.64
|
||||||
|
c1.747-4.172,4.961-6.879,6.96-10.8c2.189-1.661,3.682-4.135,5.76-6c0.666-0.597,1.669-0.798,2.4-1.44
|
||||||
|
C27.522,15.38,27.938,13.633,29.581,13.432z"/>
|
||||||
|
<path d="M150.061,192.231c1.83,1.771,1.967,5.232,2.399,8.4C151.748,197.745,150.889,195.005,150.061,192.231z"/>
|
||||||
|
<path d="M152.46,200.872c0.647,1.271,0.92,2.92,0.96,4.8C152.854,204.317,152.702,202.551,152.46,200.872z"/>
|
||||||
|
<path d="M48.541,110.872c-3.429-3.012,2.593-5.527,2.88-2.16C51.547,110.201,50.337,110.811,48.541,110.872z M48.3,109.672
|
||||||
|
c0.668,0.385,0.664-1.036,0.24-0.96C48.465,109.037,48.199,109.17,48.3,109.672z"/>
|
||||||
|
<path d="M21.42,160.792c1.934-1.906,2.754-4.927,5.28-6.239c-2.298,3.544-5.058,6.622-6.48,11.039
|
||||||
|
c3.271-4.889,6.15-10.17,10.561-13.92c0.353-0.912-1.073-0.047-0.721-0.96c2.43-1.57,3.76-4.24,6.96-5.04
|
||||||
|
c-0.101,0.779-0.997,0.763-1.199,1.44c2.234-1.125,3.954-2.766,6.479-3.602c0.562-0.078,0.233-1.047,0.72-1.199
|
||||||
|
c3.039-1.44,6.383-2.578,10.561-2.881c0.624,0.633-0.388,0.189-0.24,0.961c0.72,0,1.44,0,2.16,0c-1.44-1.789-1.267-3.211,0.24-5.76
|
||||||
|
c0.573-0.972,1.111-2.216,1.92-2.4c0.204,2.628-2.214,3.833-1.92,5.76c1.9,0.694,4.79-0.966,6.96,0
|
||||||
|
c-0.317,1.442-3.182,0.339-4.32,0.96c6.704,0.176,12.813,0.945,17.521,3.12c3.945-1.339,7.001,2.024,9.6,3.6
|
||||||
|
c0.481,0.488,0.062,0.64-0.24,0.961c4.866,3.396,9.953,6.079,14.4,10.079c2.279,2.051,4.136,3.671,6.24,6
|
||||||
|
c0.603,0.668,1.541,1.234,2.158,1.921c0.623,0.689,0.996,1.632,1.682,2.399c1.046,1.172,2.52,1.681,2.88,3.601
|
||||||
|
c3.112,1.927,6.022,4.056,10.08,5.039c-2.345-1.815-5.804-2.518-8.16-4.319c1.046-0.662,1.854,0.629,2.64,0.961
|
||||||
|
c0.989,0.416,2.129,0.528,3.12,0.959c1.798,0.779,3.322,1.97,5.04,2.881c4.528,2.402,9.151,4.309,14.16,6.479
|
||||||
|
c-0.441,1-0.904,0.837,0.24,1.439c0.377-0.503,0.219-2.104,0-3.12c-0.719-3.334-2.582-6.436-4.08-9.602
|
||||||
|
c-1.439-3.043-3.422-6.012-3.84-9.6c0.725-1.034,2-1.52,3.359-1.92c-1.265-7.012-1.57-15.238,0.239-22.32
|
||||||
|
c0.979-3.827,2.716-7.71,4.08-11.76c1.356-4.028,2.086-8.338,3.36-12.48c1.813-5.897,4.24-11.626,6-17.76
|
||||||
|
c0.576-2.014,1.359-3.847,1.92-5.76c0.439-1.503,0.457-3.101,0.96-4.56c2.241-6.504,4.963-13.056,7.92-19.2
|
||||||
|
c1.229-2.554,2.055-5.326,3.358-7.44c0.334-0.541,1.066-0.881,1.44-1.44c0.547-0.818,0.683-1.998,1.2-2.88
|
||||||
|
c0.468-0.798,1.158-1.408,1.68-2.16c1.143-1.652,2.04-3.624,3.12-4.8c2.771-3.019,6.896-4.626,9.12-6.96
|
||||||
|
c-1.417-0.79-3.008-0.504-4.561-0.72c-15.819-2.198-31.188-5.902-45.119-10.08c-6.028-1.02-12.305-2.654-18-4.32
|
||||||
|
c-4.121-1.205-7.994-2.168-12-3.36c-1.084-0.322-2.811-0.442-3.36-1.68c-1.199,0.555-2.575-0.564-3.84-0.72
|
||||||
|
c-1.944-2.305-1.838-4.593-3.36-6.96c-2.579-4.011-10.535-4.885-16.8-6c-5.283-0.94-11.398-1.612-17.28-1.92
|
||||||
|
c-2.405-0.126-4.836,0.255-6.72,0c-0.96-0.13-1.744-0.689-2.64-0.72c-4.317-0.151-7.787,3.14-10.801,4.08
|
||||||
|
c1.681,8.378,5.883,16.13,8.16,24.72c0.181,0.679,0.077,1.48,0.24,2.16c1.192,4.97,2.028,11.915,2.88,18.24
|
||||||
|
c0.325,2.417,0.567,4.839,0.96,6.96c0.995,5.377,1.769,10.711,2.64,16.08c0.621,3.823,1.868,7.249,1.921,11.28
|
||||||
|
c0.023,1.867,0.833,3.842,1.199,5.76c1.145,5.99,1.941,11.323,3.12,17.76c0.664,3.625,1.143,6.605,1.92,10.56
|
||||||
|
c0.213,1.082,0.807,3.903,0.48,5.76c-1.362-0.676-0.612-2.755-0.72-3.84c-0.321-3.231-1.526-6.321-2.16-9.6
|
||||||
|
c-0.812-4.2-1.122-8.545-1.92-12.96c-0.743-4.109-1.776-8.241-2.4-12.48c-0.624-4.238-1.288-8.525-2.16-12.72
|
||||||
|
c-0.891-4.285-1.224-8.516-1.92-12.72c-1.09-6.581-2.213-13.001-2.88-19.44c-1.357-13.099-6.619-24.24-10.8-34.56
|
||||||
|
c-1.124,0.297-2.572,1.345-3.601,0.96c1.537-1.967,4.458-2.744,7.2-4.08c3.129-1.523,6.393-3.895,10.32-2.64
|
||||||
|
c9.006-3.378,18.614-3.365,29.04-2.88c3.564,0.166,6.843,0.097,10.08,0.72c2.062,0.397,4.294,0.803,6.479,1.44
|
||||||
|
c0.57,0.166,1.107,0.608,1.681,0.72c0.588,0.115,1.326-0.156,1.92,0c3.017,0.792,6.084,2.503,9.121,3.6
|
||||||
|
c9.119,3.295,17.252,8.793,24,14.16c13.316,2.752,27.009,5.719,39.84,9.12c3.088,0.819,6.115,2.127,9.119,3.12
|
||||||
|
c2.354,0.778,4.222,0.81,5.761,2.4c0,1.12,0,2.24,0,3.36c-1.306,1.416-2.587,2.854-4.8,3.36c-1.762,1.354-3.555,2.489-5.28,3.84
|
||||||
|
c-4.033,3.158-6.948,7.572-9.601,12.24c-2.646,4.656-5.75,9.431-6.961,14.88c-3.103,4.097-3.572,10.826-6,15.6
|
||||||
|
c-0.846,4.674-2.132,8.907-3.84,12.72c-3.023,12.093-7.494,23.404-10.8,36c-1.067,4.07-2.05,8.055-1.68,12.721
|
||||||
|
c0.192,2.447,1.304,4.592,0.96,7.92c2.38,2.41,5.91,1.207,6.479,4.32c0.149,0.815-0.469,1.732-0.238,2.641
|
||||||
|
c0.31,1.228,1.4,1.582,2.158,2.64c0.367,0.513,0.401,1.11,0.721,1.681c0.6,1.071,1.973,1.904,1.921,3.12
|
||||||
|
c-1.71-1.116-5.939-0.12-6.722-2.641c-0.326-1.056-0.051-2.962,0.961-3.84c-0.551-0.81-1.742-0.979-2.399-1.681
|
||||||
|
c-0.479,0.642-0.043,2.196-0.96,2.399c-1.686,0.006-2.42-0.94-3.36-1.681c2.378,6.023,5.962,10.84,7.2,18
|
||||||
|
c-0.499,0.521-0.775-0.028-1.2-0.239c0.568,1.512,1.103,3.06,2.642,3.601c0.24-0.32,0.396-0.725,0.72-0.961
|
||||||
|
c-0.04-1.24-2.19-0.369-1.92-1.92c2.896,1.111,8.071,0.932,6.72,5.521c1.604-0.086,1.508-1.227,1.439-2.16
|
||||||
|
c-0.116-1.637-1.732-1.745-2.159-2.641c2.247-0.848,2.809,1.961,3.6,3.359c1.896-1.439-0.931-3.092-0.72-4.08
|
||||||
|
c2.275-0.436,1.814,1.866,2.88,2.641c0.519-1.938-1.698-2.217-0.96-3.6c-1.312-0.848-1.551-2.77-2.16-4.32
|
||||||
|
c-0.871-0.131-1.943,0.078-2.88-0.479c-0.745-1.229-1.812-3.401-0.72-5.04c1.875-0.539,3.267,0.706,3.84,1.92
|
||||||
|
c0.294,0.623,0.039,1.396,0.24,2.16c0.467,1.771,1.71,3.521,1.92,5.04c1.044,0.637,2.349,1.011,2.88,2.16
|
||||||
|
c-0.806,3.459-0.021,6.384,0.96,10.319c0.884,3.547,1.48,6.951,2.4,10.319c0.781,2.868,1.963,7.187,2.64,10.56
|
||||||
|
c0.11,0.556-0.071,1.119,0,1.682c0.508,3.995,2.243,8.225,3.36,12.479c1.8,6.861,2.722,13.881,4.318,21.121
|
||||||
|
c0.688,3.112,2.145,5.914,2.4,8.879c6.355-0.617,5.865,10.313-1.44,8.16c-0.606-0.666-2.256-1.324-1.438-2.64
|
||||||
|
c1.234,1.05,1.816,1.95,3.6,1.681c1.979-0.301,3.092-2.835,1.92-4.801c-1.634-1.834-3.564,0.15-5.279-0.24
|
||||||
|
c-0.351-1.471,1.483-0.756,1.439-1.92c0.104-1.543-0.771-2.107-0.96-3.359c-0.397,0.242-0.261,1.021-0.479,1.439
|
||||||
|
c-1.474-0.113-1.64,1.081-2.881,1.2c-0.815-1.983-1.211-4.39-1.68-6.72c0.596-0.604,2.02-0.381,2.16-1.44
|
||||||
|
c0.73-0.011,0.934,0.507,1.439,0.72c0.184-1.303-0.297-1.943-0.24-3.119c-0.743,0.317-1.233,0.744-1.92,1.199
|
||||||
|
c-0.512,0.342-0.961,1.149-1.92,0.961c-0.873-1.607-0.926-4.035-1.199-6.24c1.027-0.469,2.085-1.998,3.6-1.199
|
||||||
|
c-0.049-2.191-0.803-3.678-0.96-5.762c-0.661,0.756,0.343,1.889,0,3.121c-0.632,0.172-0.909,1.42-1.68,0.721
|
||||||
|
c-0.278,0.762-0.327,1.752-1.44,1.68c-0.63-1.93-1.523-3.596-1.439-6.239c1.488-0.493,2.322-2.764,4.08-1.2
|
||||||
|
c-0.291-2.67-1.105-4.814-1.681-7.199c-0.632,0.961,0.533,2.18,0.24,3.6c-0.776,1.223-2.252,1.748-2.88,3.12
|
||||||
|
c-1.539-1.251-1.508-4.423-1.92-6.72c0.602-0.373,0.804-0.975,1.68-0.48c0.105-0.693,0.506-1.094,1.2-1.199
|
||||||
|
c0.582,0.297,1.116,0.643,1.438,1.199c-0.136-1.464-0.621-2.578-0.959-3.84c-1.062,0.138-0.719,1.682-1.92,1.68
|
||||||
|
c-0.482,0.398-0.183,1.578-0.961,1.68c-2.101-0.4-2.123-4.084-2.16-5.76c0.31-1.292,1.816-1.384,2.642-2.16
|
||||||
|
c1.34,0.42,1.255,2.267,1.92,3.36c-0.037-1.646-0.894-4.545-1.2-6.72c-1.124,0.044-3.02,1.266-3.6,2.641
|
||||||
|
c-0.644,0.646-0.913-0.307-1.44-0.48c0.653,3.693,1.729,7.789,2.64,11.761c0.703,3.063,1.023,6.019,2.4,7.92
|
||||||
|
c0.721,8.088,4.225,15.449,5.28,24c-1.283-1.156-1.265-3.088-1.681-4.562c-2.156-7.615-4.125-15.852-5.761-24.238
|
||||||
|
c-1.569-8.059-4.251-15.672-5.04-23.762c1.043,0.799,0.825,2.855,1.2,4.32c0.996-0.684,2.788-0.572,3.12-1.92
|
||||||
|
c0.891-0.17,1.029,0.41,1.92,0.24c-1.221-6.25-2.62-12.502-4.08-18.721c-0.438-1.863-0.727-4.033-1.92-5.521
|
||||||
|
c-1.1,0.021-1.098,1.144-1.92,1.44c-2.129-0.365-2.719,1.443-5.28,1.68c1.589,9.702,4.788,19.036,6.479,28.8
|
||||||
|
c0.302,1.738,0.367,3.38,0.721,5.041c0.557,2.615,1.475,5.364,2.16,8.64c2.146,10.254,6.004,21.727,8.16,32.399
|
||||||
|
c2.027,0.351,2.928-0.432,4.318-0.72c0.418,2.652-3.131,2.361-5.039,1.92c-6.931-24.35-12.082-50.479-18-75.84
|
||||||
|
c-2.002-1.438-2.99-3.891-4.801-5.521c-0.678-0.197-0.621,0.34-1.199,0.24c-4.386-2.818-9.349-4.729-14.16-7.199
|
||||||
|
c-4.744-2.436-9.889-4.223-14.159-7.201c-1.263-3.646-4.685-6.091-7.681-9.119c-2.334-2.357-4.869-4.594-7.439-6.721
|
||||||
|
c-2.421-2.001-5.087-3.658-8.4-5.76c-2.435-1.543-6.654-4.709-9.12-4.8c-1.204-0.044-2.162,0.894-3.359,1.44
|
||||||
|
c-1.265,0.575-2.666,0.76-3.84,1.438c-2.599,1.504-6.572,4.528-8.641,6.721c-0.7,0.742-1.223,2.046-1.92,3.119
|
||||||
|
c-1.686,2.597-2.355,3.652-3.6,6.961c-1.01,2.685-1.321,5.697-2.16,9.119c-0.304,1.24-0.88,3.11-0.96,5.28
|
||||||
|
c-0.161,4.354-0.081,9.812,0.479,14.16c0.47,3.643,1.634,5.873-1.439,7.199c1.128,5.896-2.103,8.493-7.2,7.92
|
||||||
|
c-1.755-0.196-3.433-1.703-4.8-1.439c-0.772,0.148-1.158,1.165-1.68,1.199c-0.012,2.816,3.443,8.568,3.119,12
|
||||||
|
c-0.345,3.664-5.084,4.781-8.159,2.4c-2.018-5.104-3.257-10.984-5.04-16.32c-1.104-1.088-2.422-0.971-4.32-1.68
|
||||||
|
c-0.448-0.168-0.741-0.642-1.2-0.721c-2.181-0.375-4.735,0.471-6.479-0.24c-1.671-0.68-1.605-2.477-2.881-3.359
|
||||||
|
c-2.689-13.207-0.238-23.942,3.36-34.08c0.355-1,1.099-1.922,1.44-2.879c0.316-0.893,0.148-1.949,0.479-2.881
|
||||||
|
c0.616-1.729,1.61-3.226,2.16-5.041c1.552-5.117,3.843-11.199,4.8-17.278c1.435-9.108,0.173-19.464-1.439-27.84
|
||||||
|
c-0.581-3.016-0.713-5.869-1.44-8.64c-0.86-3.279-2.544-6.25-3.36-9.36c-0.188-0.718,0.001-1.275-0.239-1.92
|
||||||
|
c-0.484-1.298-1.316-2.454-1.92-3.84c-0.254-0.583-0.681-1.277-0.96-1.92c-0.551-1.265-1.039-2.307-1.44-4.08
|
||||||
|
c-1-1.933-1.815-3.799-2.88-5.52c-3.057-4.945-6.939-9.452-10.561-14.16c-1.982-1.733-3.234-2.961-2.88-6.96
|
||||||
|
c0.488-5.49,3.643-10.933,6-16.32c2.413-5.511,5.669-10.261,8.16-13.92c-0.681-3.26,0.508-5.816,4.32-5.28
|
||||||
|
c1.648-1.083,0.856-4.006,2.88-4.8c0.818-0.321,1.811,0.003,2.4-0.24c1.817-0.749,1.167-2.514,2.399-3.36
|
||||||
|
c1.08-0.742,2.506-0.357,3.601,0c-0.578,2.316-2.125,4.626-5.04,3.84c0.284,2.28-1.472,3.962-3.12,5.04
|
||||||
|
c-0.904,0.519-1.017-0.376-1.92-0.479c-0.671,0.613,1.168,1.048,0.239,1.92c-0.883,2.076-2.336,3.584-4.56,4.32
|
||||||
|
c-3.07,3.984-6.422,8.912-8.64,14.16c-0.396,0.937-0.553,2.007-0.96,2.88c-0.777,1.662-1.557,3.044-2.4,5.28
|
||||||
|
c-0.337,0.892-0.643,1.788-0.96,2.88c-0.519,1.786-1.834,4.843-1.68,6.24c0.31,2.806,5.646,6.404,7.439,8.4
|
||||||
|
c1.528,1.702,3.668,5.2,5.28,7.68c1.484,2.283,3.561,5.287,4.8,8.16c0.76,1.76,1.024,3.335,1.68,4.8
|
||||||
|
c0.614,1.371,1.233,2.71,2.16,4.32c1.305,6.888,4.467,14.05,5.761,21.84c0.322,1.945,0.454,4.176,0.72,6.24
|
||||||
|
C30.615,132.855,26.544,148.64,21.42,160.792z M127.5,20.152c-5.646-3.555-10.348-8.052-17.521-10.08
|
||||||
|
c-0.875-1.489-2.273-2.128-4.078-2.88c-3.35-1.396-7.562-3.075-10.801-2.64c-1.82-1.175-4.289-0.83-6.721-1.2
|
||||||
|
c-2.28-0.347-4.444-1.439-6.72-1.68c-1.344-0.142-2.785,0.07-4.32,0c-7.497-0.34-13.854-0.484-19.68,0.48
|
||||||
|
c-1.943,0.321-3.91,0.105-4.8,1.2c12.866,0.814,25.084,2.275,35.04,6c2.174,1.906,3.231,4.928,4.56,7.68
|
||||||
|
c3.756,0.281,7.337,0.812,11.04,1.44c3.771,0.638,7.745,0.647,11.28,1.44c1.232,0.276,2.434,0.909,3.6,1.2
|
||||||
|
c1.477,0.369,2.948,0.386,4.32,0.72c3.857,0.938,7.084,3.071,10.8,3.6C132.611,22.562,128.557,22.856,127.5,20.152z M26.701,10.552
|
||||||
|
c-0.081,0.799-0.695,1.064-0.48,2.16c1.457-0.143,2.016-1.185,2.4-2.4C28.04,9.76,27.455,10.557,26.701,10.552z M21.18,17.752
|
||||||
|
c1.42-0.58,2.396-1.604,2.641-3.36C21.856,12.896,19.951,16.054,21.18,17.752z M15.901,19.192c-0.65,1.087-1.234,3.719,0.479,4.08
|
||||||
|
c1.305-0.855,2.313-2.007,2.64-3.84C18.334,18.765,16.911,18.93,15.901,19.192z M100.38,20.872c-0.043-0.69-1.745-0.956-2.159-0.48
|
||||||
|
C99.227,20.265,99.561,20.812,100.38,20.872z M119.581,26.152c8.754,2.136,16.763,4.572,25.2,6.72
|
||||||
|
c3.148,0.801,6.432,1.065,9.84,1.68c2.46,0.443,5.003,1.364,6.96,1.68c4.544,0.733,8.42,1.37,12.72,1.92
|
||||||
|
c2.854,0.364,6.668,1.791,9.12-0.72c-0.155-1.847-2.051-2.091-3.36-2.64c-9.778-4.101-21.683-7.286-32.64-8.88
|
||||||
|
c1.938,0.772,3.869,1.362,6,1.92c2.259,0.591,4.64,0.699,6.96,1.2c0.835,0.18,1.508,0.71,2.4,0.96
|
||||||
|
c0.902,0.253,1.931,0.237,2.88,0.48c3.551,0.907,6.789,2.409,9.84,3.6c1.567,0.612,3.974,0.669,4.319,2.64
|
||||||
|
c-1.635,2.107-4.569,0.608-6.959,0.24c-3.174-0.488-5.125-0.856-7.44-1.44c-4.142-1.043-8.385-1.535-12.72-2.4
|
||||||
|
c-5.418-1.082-10.871-2.907-16.32-4.08c-5.503-1.185-10.977-2.667-16.32-4.08c-2.738-0.725-5.652-0.984-8.398-1.68
|
||||||
|
c-3.6-0.912-7.153-2.42-10.561-2.16C106.85,22.779,113.795,24.741,119.581,26.152z M135.42,23.032c-1.168,0.12-2.3-1.049-3.12-0.48
|
||||||
|
C132.919,23.471,134.123,23.59,135.42,23.032z M137.581,23.512c-0.429,0.001-1.396-0.646-1.681,0
|
||||||
|
C136.659,23.182,137.35,24.106,137.581,23.512z M138.781,23.752C137.244,23.475,138.253,24.485,138.781,23.752L138.781,23.752z
|
||||||
|
M140.701,24.232c-0.418,0.018-0.747-0.054-0.96-0.24C139.355,24.661,140.777,24.656,140.701,24.232z M146.939,25.672
|
||||||
|
c-0.42,0.066-1.196-0.675-1.438,0C145.921,25.606,146.698,26.348,146.939,25.672z M50.46,140.872
|
||||||
|
c0.413,0.013,0.615-0.186,0.72-0.479C50.767,140.379,50.565,140.576,50.46,140.872z M83.34,145.192
|
||||||
|
c-0.599,0.171-0.986-1.058-1.439-0.24C82.156,145.124,83.005,145.89,83.34,145.192z M43.741,158.151
|
||||||
|
c-0.613,1.31-1.417,2.559-1.92,3.841c-1.446,3.681-2.29,8.405-3.36,12.239c-0.197,0.707-0.124,1.976-0.96,2.4
|
||||||
|
c0.58-5.143,1.586-9.746,3.12-13.921c1.579-4.298,3.745-8.028,4.8-12c-1.188,2.661-2.916,5.207-4.08,8.16
|
||||||
|
c-0.661,1.677-0.845,3.556-1.439,5.28c-0.392,1.133-1.079,2.006-1.44,3.119c-0.397,1.227-0.381,2.485-0.72,3.84
|
||||||
|
c-0.347,1.387-1.107,2.686-1.44,4.08c-0.313,1.313,0.229,3.015-1.2,3.841c0.68-4.237,1.386-8.304,2.4-12.24
|
||||||
|
c0.995-3.864,2.999-7.331,3.6-10.8c-2.037,3.979-3.683,9.367-4.8,13.68c-0.562,2.166-1.047,4.053-1.68,5.76
|
||||||
|
c-0.333,0.898-0.376,2.038-1.2,2.642c0.995-3.695,1.656-7.688,2.64-11.521c0.853-3.32,2.179-6.689,3.36-9.601
|
||||||
|
c0.623-1.534,1.54-2.929,1.68-4.56c-4.529,7.229-6.201,17.317-8.88,26.398c0.265-7.02,3.061-14.855,4.561-20.641
|
||||||
|
c-3.25,7.15-5.026,15.774-6.721,24.48c-0.278-3.48,0.833-6.847,0.721-9.361c-0.837,2.524-0.938,5.783-1.92,8.16
|
||||||
|
c0.249-8.07,2.284-14.354,4.079-20.879c-1.099,0.604-1.108,2.125-1.439,3.359c-0.33,1.229-0.892,2.562-1.2,3.84
|
||||||
|
c-0.954,3.941-1.397,8.85-2.399,12.24c-0.175,0.59,0.17,1.729-0.721,1.92c0.333-4.309,1.486-9.072,1.681-12.721
|
||||||
|
c-1.289,3.992-1.331,9.229-2.641,13.201c-0.339-2.184,0.607-4.351,0-6c0,0.56,0,1.118,0,1.68c-0.873,1.207-0.36,3.8-1.2,5.039
|
||||||
|
c-0.27-0.689-0.048-1.871-0.479-2.399c0.245,0.824-0.396,1.979-0.72,1.44c-0.22-1.982,1.028-3.769,0.239-5.521
|
||||||
|
c-0.602,3.717-0.384,8.256-1.92,11.039c-0.146-0.574,0.302-1.741-0.239-1.92c-0.188,0.625-0.413,2.451-0.961,1.92
|
||||||
|
c0.091-1.16,0.98-3.078,0-3.84c0.321,1.521-0.976,3.624-1.68,1.92c-0.085,1.595-0.108,3.252-0.479,4.562
|
||||||
|
c-0.516-0.525-0.113-1.969-0.721-2.4c-0.175,0.545,0.433,1.873-0.479,1.68c-1.05-2.633,0.628-7.181-0.48-9.84
|
||||||
|
c-1.418,6.688-0.179,14.127,0,21.6c2.076,1.42,3.499,0.545,5.761,0.721c1.369,0.106,3.233,0.787,5.04,1.439
|
||||||
|
c1.765,0.639,3.736,0.938,5.039,1.44c2.034,0.786,3.52,2.353,5.28,2.159c1.335-0.146,2.019-1.488,3.12-1.92
|
||||||
|
c2.678,1.703,7.881,2.992,10.32,0.239c1.113-1.255,1.084-3.026,0.479-5.278c0.331-1.271,1.749-1.451,2.4-2.4
|
||||||
|
c-2.405-9.068-1.622-20.285,0.479-28.561c0.488-1.918,0.75-4.174,1.44-6c0.134-0.353,0.766-0.771,0.96-1.199
|
||||||
|
c0.886-1.948,1.236-2.836,2.16-4.32c1.059-1.702,2.208-3.387,3.12-4.8c-2.922,1.388-4.375,5.03-5.521,7.681
|
||||||
|
c-0.801,1.852-1.863,3.79-2.399,5.279c-0.763,2.117-0.429,4.688-2.16,6.24c0.534-2.24,0.688-4.629,1.439-6.721
|
||||||
|
c0.263-0.728,0.917-1.197,1.2-1.921c0.23-0.586,0.055-1.355,0.24-1.92c0.108-0.33,0.553-0.344,0.72-0.72
|
||||||
|
c0.202-0.454,0.059-1.048,0.24-1.439c1.094-2.361,2.931-4.074,3.36-6.24c-0.611,0.59-1.02,1.382-1.92,1.681
|
||||||
|
c-0.104,1.799-1.573,2.836-2.4,4.319c-1.735,3.113-3.22,6.397-4.561,9.36c-0.674,1.49-1.021,3.412-2.399,4.56
|
||||||
|
c1.534-4.945,3.68-9.279,5.76-13.68c0.425-0.216,0.701-0.578,1.2-0.72c0.619-2.661,2.062-4.499,3.36-6.479
|
||||||
|
c-3.013,0.856-3.713,4.425-5.04,7.199c-0.45,0.94-1.206,1.77-1.681,2.641c-0.541,0.994-0.717,2.013-1.2,3.119
|
||||||
|
c-0.875,2.007-2.194,3.858-2.88,5.761c-0.434,1.204-0.337,3.796-1.92,3.84c1.629-3.503,2.437-7.214,3.84-10.8
|
||||||
|
c1.331-3.398,3.999-6.447,4.08-10.08c-3.672,5.197-6.271,12.139-8.399,18.48c-0.297,0.883-0.301,2.074-1.2,2.64
|
||||||
|
c0.978-7.423,4.625-13.454,6.479-19.2c-1.604,2.814-3.279,5.785-4.56,9.12c-1.296,3.375-1.818,7.194-3.36,10.56
|
||||||
|
c0.077-3.477,1.145-7.088,2.4-10.56c0.404-1.118,0.97-2.237,1.44-3.36c0.494-1.182,0.69-2.555,1.199-3.6
|
||||||
|
c0.804-1.65,2.572-2.846,2.641-4.801c-0.997,0.047-1.504,1.209-1.92,1.92c-3.448,5.897-4.921,13.352-7.44,19.681
|
||||||
|
c0.259-2.94,0.81-5.591,1.92-7.681c0.018-3.902,2.939-6.178,2.641-9.601c-1.471,2.734-2.318,5.974-3.36,9.121
|
||||||
|
c-1.07,3.234-1.825,6.58-3.12,9.601c0.832-8.607,3.912-14.969,6.48-21.84c0.947-1.293,2.015-2.466,2.159-4.562
|
||||||
|
c0.559-0.647,1.829-1.842,1.44-2.64C47.226,150.467,45.48,154.437,43.741,158.151z M61.26,149.032
|
||||||
|
c0.413,0.013,0.615-0.186,0.72-0.479C61.568,148.539,61.365,148.735,61.26,149.032z M32.46,149.752
|
||||||
|
c0.601,0.041,0.751-0.369,0.96-0.72C32.82,148.991,32.669,149.399,32.46,149.752z M49.5,150.712
|
||||||
|
C48.405,152.59,50.104,150.498,49.5,150.712L49.5,150.712z M52.14,154.553c0.787-1.134,1.448-2.393,2.16-3.601
|
||||||
|
C52.851,151.267,52.217,153.665,52.14,154.553z M135.18,164.633c-0.4-0.397-0.189-1.409-0.24-2.159
|
||||||
|
c0.188-0.054,0.279-0.201,0.24-0.48c0.703,0.062,0.737-0.543,1.44-0.48c0.905-0.104,1.127,0.475,1.68,0.721
|
||||||
|
c0.244-1.283-0.329-1.75-0.96-2.159c-1.496,0.184-2.255,1.104-3.6,1.438C133.689,162.928,134.73,164.515,135.18,164.633z
|
||||||
|
M139.5,161.992c0.439,0.462,0.812-0.205,1.2-0.479c-0.343-0.058-0.405-0.396-0.722-0.479
|
||||||
|
C139.977,161.509,139.326,161.338,139.5,161.992z M136.141,163.672c1.604-0.376,1.142,0.785,0.959,1.681
|
||||||
|
c0.455-0.054,0.529,0.271,0.961,0.239c-0.129-1.552-0.433-2.928-1.92-3.118C136.141,162.872,136.141,163.271,136.141,163.672z
|
||||||
|
M140.46,163.672c0.478,0.004,0.306,0.655,0.96,0.479c0.075-0.323,0.342-0.459,0.24-0.959
|
||||||
|
C141.232,163.324,140.664,163.315,140.46,163.672z M30.06,165.353c0.139-0.146,0.562-1.139,0-1.199
|
||||||
|
C30.373,164.748,29.356,165.147,30.06,165.353z M136.141,169.433c-0.297-1.623-1.065-2.773-1.92-3.841
|
||||||
|
C134.659,167.074,135.413,168.24,136.141,169.433z M29.821,166.553c0.064-0.327-0.149-0.938-0.24-0.479
|
||||||
|
C29.515,166.397,29.729,167.009,29.821,166.553z M27.661,168.474c0.73-0.082,0.964-1.9,0.479-2.4
|
||||||
|
C27.971,166.863,27.754,167.606,27.661,168.474z M141.9,167.512c0,0.24,0,0.48,0,0.722c0.324,0.326,1.676,0.457,1.681-0.24
|
||||||
|
C143.09,167.731,142.6,166.842,141.9,167.512z M19.02,168.712c0.07-0.146,0.598-0.924,0-0.96
|
||||||
|
C18.951,167.899,18.423,168.676,19.02,168.712z M31.26,172.553c-0.155-1.84,1.534-3.102,0.72-4.801
|
||||||
|
C31.852,168.927,30.532,171.974,31.26,172.553z M143.34,170.633c0.68,0.197,0.622-0.339,1.2-0.239
|
||||||
|
c0.076-0.558-0.039-0.922-0.24-1.201C143.473,169.164,143.184,169.676,143.34,170.633z M27.18,172.072
|
||||||
|
c0.136-0.677,0.835-2.029,0.24-2.642C27.301,170.125,26.843,171.719,27.18,172.072z M137.82,172.792
|
||||||
|
c-0.106-0.613-0.336-1.104-0.721-1.439C137.223,171.95,137.092,172.8,137.82,172.792z M25.5,173.271
|
||||||
|
c-0.171,4.112,0.753-0.395,0.24-0.719C25.703,172.834,25.782,173.233,25.5,173.271z M148.38,175.192
|
||||||
|
c1.005,0.115,1.526,0.713,1.44,1.92c1.74-0.732-0.082-3.19-1.2-3.36C148.344,174.035,148.35,174.601,148.38,175.192z M21.42,175.433
|
||||||
|
c0.038-0.166-0.084-1.396-0.24-0.721C20.969,175.626,21.309,175.914,21.42,175.433z M16.14,178.553c0.035-0.284,0.428-0.545,0-0.721
|
||||||
|
C16.106,178.115,15.713,178.376,16.14,178.553z M128.46,178.312c-0.315-0.244-0.885-0.235-1.2-0.479
|
||||||
|
C126.975,178.545,128.271,178.863,128.46,178.312z M22.861,179.032c0.064-0.327-0.149-0.938-0.24-0.479
|
||||||
|
C22.555,178.879,22.769,179.489,22.861,179.032z M130.861,179.992c0.285-0.714-1.012-1.031-1.2-0.479
|
||||||
|
C130.4,179.332,129.953,180.34,130.861,179.992z M22.621,181.433c0.038-0.166-0.084-1.396-0.24-0.721
|
||||||
|
C22.169,181.626,22.509,181.914,22.621,181.433z M138.3,182.872c-0.282-0.034-0.544-0.428-0.72,0
|
||||||
|
C137.863,182.906,138.125,183.299,138.3,182.872z M148.38,188.633c0.229-1.432-0.444-1.955-0.96-2.641
|
||||||
|
C141.995,184.959,145.773,191.787,148.38,188.633z M32.22,206.633c-0.457-0.104-0.603-0.518-1.2-0.479
|
||||||
|
C30.852,206.748,32.046,207.168,32.22,206.633z M32.94,208.072c0.286-0.714-1.011-1.031-1.199-0.48
|
||||||
|
C32.298,207.697,32.383,208.651,32.94,208.072z M35.821,214.553c0.402,1.761,1.146,5.343,2.64,5.039
|
||||||
|
c0.277-3.799-2.039-7.08-2.4-11.039c-0.57-0.086-1.38-1.157-1.92-0.479C35.256,209.742,35.267,212.132,35.821,214.553z
|
||||||
|
M33.18,209.512c-0.148,1.339,1.039,2.537,0.24,4.08c0.282,0.357,0.603,0.679,0.96,0.961c-1.081,0.765,1.131,2.709,0,3.601
|
||||||
|
c0.576,0.223,0.636,0.965,1.68,0.72c-0.979-3.261-1.55-6.931-2.64-10.08c-0.85-0.21-0.59-0.03-1.44-0.239
|
||||||
|
C31.747,209.506,33.322,208.649,33.18,209.512z M39.18,210.231c0.174-0.733-0.414-0.705-0.24-1.439c-0.399,0-0.8,0-1.199,0
|
||||||
|
C38.161,209.332,38.641,209.812,39.18,210.231z M38.22,211.433c0.68,0.041,1,0.439,1.681,0.479
|
||||||
|
C39.902,210.8,38.848,210.937,38.22,211.433z M38.701,213.112c1.781-0.084-0.676,0.596,0.239,0.96
|
||||||
|
c0.066-0.566,0.895,0.021,0.961,0.239c0.459,1.181-0.798,0.644-0.721,1.439c0.596-0.035,1.165-0.045,1.2,0.48
|
||||||
|
c0.206,0.766-0.378,0.742-0.479,1.199c0.728-0.089,0.498,0.781,0.72,1.2c-0.617,1.144-1.351,2.169-3.12,2.159
|
||||||
|
c-0.871-0.169-0.853-1.229-1.68-1.439c-0.346,0.135-0.586,0.375-0.721,0.721c1.11-0.18,0.98,0.271,0.721,0.96
|
||||||
|
c2.038,0.269,3.538,1.712,5.52,0.72c1.29-3.207-0.678-6.458-1.439-9.119C39.305,212.597,38.736,212.588,38.701,213.112z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 30 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
Vendored
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<h3>About</h3>
|
||||||
|
<p>
|
||||||
|
Flask-Security is an opinionated Flask extension which adds basic
|
||||||
|
security and authentication features to your Flask apps quickly
|
||||||
|
and easily. Flask-Social can also be used to add "social" or OAuth
|
||||||
|
login and connection management.
|
||||||
|
</p>
|
||||||
|
<h3>Useful Links</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
|
||||||
|
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
|
||||||
|
<li><a href="http://github.com/mattupstate/flask-security/issues">Issue Tracker</a></li>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<li><a href="http://pypi.python.org/pypi/Flask-Social">Flask-Social</a></li>
|
||||||
|
<li><a href="http://github.com/mattupstate/flask-social">Flask-Social @ github</a></li>
|
||||||
|
</ul>
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
<p class="logo"><a href="{{ pathto(master_doc) }}">
|
||||||
|
<img class="logo" src="{{ pathto('_static/logo-helmet.png', 1) }}" alt="Logo"/>
|
||||||
|
</a></p>
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
API
|
||||||
|
===
|
||||||
|
|
||||||
|
Core
|
||||||
|
----
|
||||||
|
.. autoclass:: flask_security.core.Security
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. data:: flask_security.core.current_user
|
||||||
|
|
||||||
|
A proxy for the current user.
|
||||||
|
|
||||||
|
|
||||||
|
Protecting Views
|
||||||
|
----------------
|
||||||
|
.. autofunction:: flask_security.decorators.login_required
|
||||||
|
|
||||||
|
.. autofunction:: flask_security.decorators.roles_required
|
||||||
|
|
||||||
|
.. autofunction:: flask_security.decorators.roles_accepted
|
||||||
|
|
||||||
|
.. autofunction:: flask_security.decorators.http_auth_required
|
||||||
|
|
||||||
|
.. autofunction:: flask_security.decorators.auth_token_required
|
||||||
|
|
||||||
|
|
||||||
|
User Object Helpers
|
||||||
|
-------------------
|
||||||
|
.. autoclass:: flask_security.core.UserMixin
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: flask_security.core.RoleMixin
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: flask_security.core.AnonymousUser
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Datastores
|
||||||
|
----------
|
||||||
|
.. autoclass:: flask_security.datastore.UserDatastore
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: flask_security.datastore.SQLAlchemyUserDatastore
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: flask_security.datastore.MongoEngineUserDatastore
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
.. autoclass:: flask_security.datastore.PeeweeUserDatastore
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
|
||||||
|
Signals
|
||||||
|
-------
|
||||||
|
See the `Flask documentation on signals`_ for information on how to use these
|
||||||
|
signals in your code.
|
||||||
|
|
||||||
|
See the documentation for the signals provided by the Flask-Login and
|
||||||
|
Flask-Principal extensions. In addition to those signals, Flask-Security
|
||||||
|
sends the following signals.
|
||||||
|
|
||||||
|
.. data:: user_registered
|
||||||
|
|
||||||
|
Sent when a user registers on the site. It is passed a dict with
|
||||||
|
the `user` and `confirm_token`, the user being logged in and the
|
||||||
|
(if so configured) the confirmation token issued.
|
||||||
|
|
||||||
|
.. data:: user_confirmed
|
||||||
|
|
||||||
|
Sent when a user is confirmed. It is passed `user`, which is the
|
||||||
|
user being confirmed.
|
||||||
|
|
||||||
|
.. data:: confirm_instructions_sent
|
||||||
|
|
||||||
|
Sent when a user requests confirmation instructions. It is passed
|
||||||
|
the `user`.
|
||||||
|
|
||||||
|
.. data:: login_instructions_sent
|
||||||
|
|
||||||
|
Sent when passwordless login is used and user logs in. It is passed
|
||||||
|
a dict with the `user` and `login_token`, the user being logged in
|
||||||
|
and the (if so configured) the login token issued.
|
||||||
|
|
||||||
|
.. data:: password_reset
|
||||||
|
|
||||||
|
Sent when a user completes a password reset. It is passed the
|
||||||
|
`user`.
|
||||||
|
|
||||||
|
.. data:: password_changed
|
||||||
|
|
||||||
|
Sent when a user completes a password change. It is passed the
|
||||||
|
`user`.
|
||||||
|
|
||||||
|
.. data:: reset_password_instructions_sent
|
||||||
|
|
||||||
|
Sent when a user requests a password reset. It is passed a dict
|
||||||
|
with the `user` and `token`, the user being logged in and
|
||||||
|
the (if so configured) the reset token issued.
|
||||||
|
|
||||||
|
All signals are also passed a `app` keyword argument, which is the
|
||||||
|
current application.
|
||||||
|
|
||||||
|
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
|
||||||
+13
-7
@@ -16,8 +16,8 @@ import sys, os
|
|||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
sys.path.append(os.path.abspath('_themes'))
|
sys.path.append(os.path.abspath('_themes'))
|
||||||
sys.path.append(os.path.abspath('..'))
|
|
||||||
|
|
||||||
# -- General configuration -----------------------------------------------------
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
@@ -49,9 +49,9 @@ copyright = u'2012, Matt Wright'
|
|||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '1.1'
|
version = '1.6.7'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = '1.1.0'
|
release = version
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
@@ -92,14 +92,16 @@ pygments_style = 'sphinx'
|
|||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||||
html_theme = 'flask_small'
|
html_theme = 'flask'
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
# documentation.
|
# documentation.
|
||||||
html_theme_options = {
|
html_theme_options = {
|
||||||
'github_fork': 'mattupstate/flask-security',
|
#'github_fork': 'mattupstate/flask-security',
|
||||||
'index_logo': False
|
#'index_logo': False
|
||||||
|
'touch_icon': 'touch-icon.png',
|
||||||
|
'index_logo': 'logo-full.png'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
@@ -135,7 +137,11 @@ html_static_path = ['_static']
|
|||||||
#html_use_smartypants = True
|
#html_use_smartypants = True
|
||||||
|
|
||||||
# Custom sidebar templates, maps document names to template names.
|
# Custom sidebar templates, maps document names to template names.
|
||||||
#html_sidebars = {}
|
html_sidebars = {
|
||||||
|
'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
|
||||||
|
'**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
|
||||||
|
'sourcelink.html', 'searchbox.html']
|
||||||
|
}
|
||||||
|
|
||||||
# Additional templates that should be rendered to pages, maps page names to
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
# template names.
|
# template names.
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
The following configuration values are used by Flask-Security:
|
||||||
|
|
||||||
|
Core
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
|
||||||
|
|
||||||
|
======================================== =======================================
|
||||||
|
``SECURITY_BLUEPRINT_NAME`` Specifies the name for the
|
||||||
|
Flask-Security blueprint. Defaults to
|
||||||
|
``security``.
|
||||||
|
``SECURITY_URL_PREFIX`` Specifies the URL prefix for the
|
||||||
|
Flask-Security blueprint. Defaults to
|
||||||
|
``None``.
|
||||||
|
``SECURITY_FLASH_MESSAGES`` Specifies wether or not to flash
|
||||||
|
messages during security procedures.
|
||||||
|
Defaults to ``True``.
|
||||||
|
``SECURITY_PASSWORD_HASH`` Specifies the password hash algorith to
|
||||||
|
use when encrypting and decrypting
|
||||||
|
passwords. Recommended values for
|
||||||
|
production systems are ``bcrypt``,
|
||||||
|
``sha512_crypt``, or ``pbkdf2_sha512``.
|
||||||
|
Defaults to ``plaintext``.
|
||||||
|
``SECURITY_PASSWORD_SALT`` Specifies the HMAC salt. This is only
|
||||||
|
used if the password hash type is set
|
||||||
|
to something other than plain text.
|
||||||
|
Defaults to ``None``.
|
||||||
|
``SECURITY_EMAIL_SENDER`` Specifies the email address to send
|
||||||
|
emails as. Defaults to
|
||||||
|
``no-reply@localhost``.
|
||||||
|
``SECURITY_TOKEN_AUTHENTICATION_KEY`` Specifies the query sting parameter to
|
||||||
|
read when using token authentication.
|
||||||
|
Defaults to ``auth_token``.
|
||||||
|
``SECURITY_TOKEN_AUTHENTICATION_HEADER`` Specifies the HTTP header to read when
|
||||||
|
using token authentication. Defaults to
|
||||||
|
``Authentication-Token``.
|
||||||
|
``SECURITY_DEFAULT_HTTP_AUTH_REALM`` Specifies the default authentication
|
||||||
|
realm when using basic HTTP auth.
|
||||||
|
Defaults to ``Login Required``
|
||||||
|
======================================== =======================================
|
||||||
|
|
||||||
|
|
||||||
|
URLs and Views
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
|
||||||
|
|
||||||
|
=============================== ================================================
|
||||||
|
``SECURITY_LOGIN_URL`` Specifies the login URL. Defaults to ``/login``.
|
||||||
|
``SECURITY_LOGOUT_URL`` Specifies the logout URL. Defaults to
|
||||||
|
``/logout``.
|
||||||
|
``SECURITY_REGISTER_URL`` Specifies the register URL. Defaults to
|
||||||
|
``/register``.
|
||||||
|
``SECURITY_RESET_URL`` Specifies the password reset URL. Defaults to
|
||||||
|
``/reset``.
|
||||||
|
``SECURITY_CONFIRM_URL`` Specifies the email confirmation URL. Defaults
|
||||||
|
to ``/confirm``.
|
||||||
|
``SECURITY_POST_LOGIN_VIEW`` Specifies the default view to redirect to after
|
||||||
|
a user logs in. This value can be set to a URL
|
||||||
|
or an endpoint name. Defaults to ``/``.
|
||||||
|
``SECURITY_POST_LOGOUT_VIEW`` Specifies the default view to redirect to after
|
||||||
|
a user logs out. This value can be set to a URL
|
||||||
|
or an endpoint name. Defaults to ``/``.
|
||||||
|
``SECURITY_CONFIRM_ERROR_VIEW`` Specifies the view to redirect to if a
|
||||||
|
confirmation error occurs. This value can be set
|
||||||
|
to a URL or an endpoint name. If this value is
|
||||||
|
``None`` the user is presented the default view
|
||||||
|
to resend a confirmation link. Defaults to
|
||||||
|
``None``.
|
||||||
|
``SECURITY_POST_REGISTER_VIEW`` Specifies the view to redirect to after a user
|
||||||
|
successfully registers. 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_POST_CONFIRM_VIEW`` Specifies the view to redirect to after a user
|
||||||
|
successfully confirms their email. 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_POST_RESET_VIEW`` Specifies the view to redirect to after a user
|
||||||
|
successfully resets their password. This value
|
||||||
|
can be set to a URL or an endpoint name. If this
|
||||||
|
value is ``None`` the user is redirected to the
|
||||||
|
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults to
|
||||||
|
``None``.
|
||||||
|
``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user
|
||||||
|
attempts to access a URL/endpoint that they do
|
||||||
|
not have permission to access. If this value is
|
||||||
|
``None`` the user is presented with a default
|
||||||
|
HTTP 403 response. Defaults to ``None``.
|
||||||
|
=============================== ================================================
|
||||||
|
|
||||||
|
|
||||||
|
Template Paths
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
|
||||||
|
|
||||||
|
======================================== =======================================
|
||||||
|
``SECURITY_FORGOT_PASSWORD_TEMPLATE`` Specifies the path to the template for
|
||||||
|
the forgot password page. Defaults to
|
||||||
|
``security/forgot_password.html``.
|
||||||
|
``SECURITY_LOGIN_USER_TEMPLATE`` Specifies the path to the template for
|
||||||
|
the user login page. Defaults to
|
||||||
|
``security/login_user.html``.
|
||||||
|
``SECURITY_REGISTER_USER_TEMPLATE`` Specifies the path to the template for
|
||||||
|
the user registration page. Defaults to
|
||||||
|
``security/register_user.html``.
|
||||||
|
``SECURITY_RESET_PASSWORD_TEMPLATE`` Specifies the path to the template for
|
||||||
|
the reset password page. Defaults to
|
||||||
|
``security/reset_password.html``.
|
||||||
|
``SECURITY_SEND_CONFIRMATION_TEMPLATE`` Specifies the path to the template for
|
||||||
|
the resend confirmation instructions
|
||||||
|
page. Defaults to
|
||||||
|
``security/send_confirmation.html``.
|
||||||
|
``SECURITY_SEND_LOGIN_TEMPLATE`` Specifies the path to the template for
|
||||||
|
the send login instructions page for
|
||||||
|
passwordless logins. Defaults to
|
||||||
|
``security/send_login.html``.
|
||||||
|
======================================== =======================================
|
||||||
|
|
||||||
|
|
||||||
|
Feature Flags
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
|
||||||
|
|
||||||
|
========================= ======================================================
|
||||||
|
``SECURITY_CONFIRMABLE`` Specifies if users are required to confirm their email
|
||||||
|
address when registering a new account. If this value
|
||||||
|
is `True` Flask-Security creates an endpoint to handle
|
||||||
|
confirmations and requests to resend confirmation
|
||||||
|
instructions. The URL for this endpoint is specified
|
||||||
|
by the ``SECURITY_CONFIRM_URL`` configuration option.
|
||||||
|
Defaults to ``False``.
|
||||||
|
``SECURITY_REGISTERABLE`` Specifies if Flask-Security should create a user
|
||||||
|
registration endpoint. The URL for this endpoint is
|
||||||
|
specified by the ``SECURITY_REGISTER_URL``
|
||||||
|
configuration option. Defaults to ``False``.
|
||||||
|
``SECURITY_RECOVERABLE`` Specifies if Flask-Security should create a password
|
||||||
|
reset/recover endpoint. The URL for this endpoint is
|
||||||
|
specified by the ``SECURITY_RESET_URL`` configuration
|
||||||
|
option. Defaults to ``False``.
|
||||||
|
``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user
|
||||||
|
login statistics. If set to ``True`` ensure your
|
||||||
|
models have the required fields/attribues. Defaults to
|
||||||
|
``False``
|
||||||
|
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the
|
||||||
|
passwordless login feature. If set to ``True`` users
|
||||||
|
are not required to enter a password to login but are
|
||||||
|
sent an email with a login link. This feature is
|
||||||
|
experimental and should be used with caution. Defaults
|
||||||
|
to ``False``.
|
||||||
|
``SECURITY_CHANGEABLE`` Specifies if Flask-Security should enable the
|
||||||
|
change password endpoint. The URL for this endpoint is
|
||||||
|
specified by the ``SECURITY_CHANGE_URL`` configuration
|
||||||
|
option. Defaults to ``False``.
|
||||||
|
========================= ======================================================
|
||||||
|
|
||||||
|
Email
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
|
||||||
|
|
||||||
|
=========================================== ====================================
|
||||||
|
``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the
|
||||||
|
confirmation email. Defaults to
|
||||||
|
``Welcome``
|
||||||
|
``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the
|
||||||
|
passwordless feature. Defaults to
|
||||||
|
``Login instructions``
|
||||||
|
``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password
|
||||||
|
notice. Defaults to
|
||||||
|
``Your password has been reset``
|
||||||
|
``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the password
|
||||||
|
reset. Defaults to
|
||||||
|
``Password reset instructions``
|
||||||
|
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
|
||||||
|
confirmation message. Defaults to
|
||||||
|
``Please confirm your email``
|
||||||
|
=========================================== ====================================
|
||||||
|
|
||||||
|
Miscellaneous
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
|
||||||
|
|
||||||
|
======================================= ========================================
|
||||||
|
``SECURITY_SEND_REGISTER_EMAIL`` Specifies whether registration email is
|
||||||
|
sent. Defaults to ``True``.
|
||||||
|
``SECURITY_CONFIRM_EMAIL_WITHIN`` Specifies the amount of time a user has
|
||||||
|
before their confirmation link expires.
|
||||||
|
Always pluralized the time unit for this
|
||||||
|
value. Defaults to ``5 days``.
|
||||||
|
``SECURITY_RESET_PASSWORD_WITHIN`` Specifies the amount of time a user has
|
||||||
|
before their password reset link
|
||||||
|
expires. Always pluralized the time unit
|
||||||
|
for this value. Defaults to ``5 days``.
|
||||||
|
``SECURITY_LOGIN_WITHIN`` Specifies the amount of time a user has
|
||||||
|
before a login link expires. This is
|
||||||
|
only used when the passwordless login
|
||||||
|
feature is enabled. Always pluralized
|
||||||
|
the time unit for this value. Defaults
|
||||||
|
to ``1 days``.
|
||||||
|
``SECURITY_LOGIN_WITHOUT_CONFIRMATION`` Specifies if a user may login before
|
||||||
|
confirming their email when the value
|
||||||
|
of ``SECURITY_CONFIRMABLE`` is set to
|
||||||
|
``True``. Defaults to ``False``.
|
||||||
|
``SECURITY_CONFIRM_SALT`` Specifies the salt value when generating
|
||||||
|
confirmation links/tokens. Defaults to
|
||||||
|
``confirm-salt``.
|
||||||
|
``SECURITY_RESET_SALT`` Specifies the salt value when generating
|
||||||
|
password reset links/tokens. Defaults to
|
||||||
|
``reset-salt``.
|
||||||
|
``SECURITY_LOGIN_SALT`` Specifies the salt value when generating
|
||||||
|
login links/tokens. Defaults to
|
||||||
|
``login-salt``.
|
||||||
|
``SECURITY_REMEMBER_SALT`` Specifies the salt value when generating
|
||||||
|
remember tokens. Remember tokens are
|
||||||
|
used instead of user ID's as it is more
|
||||||
|
secure. Defaults to ``remember-salt``.
|
||||||
|
``SECURITY_DEFAULT_REMEMBER_ME`` Specifies the default "remember me"
|
||||||
|
value used when logging in a user.
|
||||||
|
Defaults to ``False``.
|
||||||
|
======================================= ========================================
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
Contents
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
features
|
||||||
|
configuration
|
||||||
|
quickstart
|
||||||
|
models
|
||||||
|
customizing
|
||||||
|
api
|
||||||
|
changelog
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
Customizing Views
|
||||||
|
=================
|
||||||
|
|
||||||
|
Flask-Security bootstraps your application with various views for handling its
|
||||||
|
configured features to get you up and running as quick as possible. However,
|
||||||
|
you'll probably want to change the way these views look to be more in line with
|
||||||
|
your application's visual design.
|
||||||
|
|
||||||
|
|
||||||
|
Views
|
||||||
|
-----
|
||||||
|
|
||||||
|
Flask-Security is packaged with a default template for each view it presents to
|
||||||
|
a user. Templates are located within a subfolder named ``security``. The
|
||||||
|
following is a list of view templates:
|
||||||
|
|
||||||
|
* `security/forgot_password.html`
|
||||||
|
* `security/login_user.html`
|
||||||
|
* `security/register_user.html`
|
||||||
|
* `security/reset_password.html`
|
||||||
|
* `security/change_password.html`
|
||||||
|
* `security/send_confirmation.html`
|
||||||
|
* `security/send_login.html`
|
||||||
|
|
||||||
|
Overriding these templates is simple:
|
||||||
|
|
||||||
|
1. Create a folder named ``security`` within your application's templates folder
|
||||||
|
2. Create a template with the same name for the template you wish to override
|
||||||
|
|
||||||
|
You can also specify custom template file paths in the :doc:`configuration <configuration>`.
|
||||||
|
|
||||||
|
Each template is passed a template context object that includes the following,
|
||||||
|
including the objects/values that are passed to the template by the main
|
||||||
|
Flask application context processor:
|
||||||
|
|
||||||
|
* ``<template_name>_form``: A form object for the view
|
||||||
|
* ``security``: The Flask-Security extension object
|
||||||
|
|
||||||
|
To add more values to the template context you can specify a context processor
|
||||||
|
for all views or a specific view. For example::
|
||||||
|
|
||||||
|
security = Security(app, user_datastore)
|
||||||
|
|
||||||
|
# This processor is added to all templates
|
||||||
|
@security.context_processor
|
||||||
|
def security_context_processor():
|
||||||
|
return dict(hello="world")
|
||||||
|
|
||||||
|
# This processor is added to only the register view
|
||||||
|
@security.register_context_processor
|
||||||
|
def security_register_processor():
|
||||||
|
return dict(something="else")
|
||||||
|
|
||||||
|
The following is a list of all the available context processor decorators:
|
||||||
|
|
||||||
|
* ``context_processor``: All views
|
||||||
|
* ``forgot_password_context_processor``: Forgot password view
|
||||||
|
* ``login_context_processor``: Login view
|
||||||
|
* ``register_context_processor``: Register view
|
||||||
|
* ``reset_password_context_processor``: Reset password view
|
||||||
|
* ``change_password_context_processor``: Reset password view
|
||||||
|
* ``send_confirmation_context_processor``: Send confirmation view
|
||||||
|
* ``send_login_context_processor``: Send login view
|
||||||
|
|
||||||
|
|
||||||
|
Forms
|
||||||
|
-----
|
||||||
|
|
||||||
|
All forms can be overridden. For each form used, you can specify a
|
||||||
|
replacement class. This allows you to add extra fields to the
|
||||||
|
register form or override validators::
|
||||||
|
|
||||||
|
from flask_security.forms import RegisterForm
|
||||||
|
|
||||||
|
class ExtendedRegisterForm(RegisterForm):
|
||||||
|
first_name = TextField('First Name', [Required()])
|
||||||
|
last_name = TextField('Last Name', [Required()])
|
||||||
|
|
||||||
|
security = Security(app, user_datastore,
|
||||||
|
register_form=ExtendedRegisterForm)
|
||||||
|
|
||||||
|
For the ``register_form`` and ``confirm_register_form``, each field is
|
||||||
|
passed to the user model (as kwargs) when a user is created. In the
|
||||||
|
above case, the ``first_name`` and ``last_name`` fields are passed
|
||||||
|
directly to the model, so the model should look like::
|
||||||
|
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
email = db.Column(db.String(255), unique=True)
|
||||||
|
password = db.Column(db.String(255))
|
||||||
|
first_name = db.Column(db.String(255))
|
||||||
|
last_name = db.Column(db.String(255))
|
||||||
|
|
||||||
|
The following is a list of all the available form overrides:
|
||||||
|
|
||||||
|
* ``login_form``: Login form
|
||||||
|
* ``confirm_register_form``: Confirmable register form
|
||||||
|
* ``register_form``: Register form
|
||||||
|
* ``forgot_password_form``: Forgot password form
|
||||||
|
* ``reset_password_form``: Reset password form
|
||||||
|
* ``change_password_form``: Reset password form
|
||||||
|
* ``send_confirmation_form``: Send confirmation form
|
||||||
|
* ``passwordless_login_form``: Passwordless login form
|
||||||
|
|
||||||
|
|
||||||
|
Emails
|
||||||
|
------
|
||||||
|
|
||||||
|
Flask-Security is also packaged with a default tempalte for each email that it
|
||||||
|
may send. Templates are located within the subfolder named ``security/mail``.
|
||||||
|
The following is a list of email templates:
|
||||||
|
|
||||||
|
* `security/mail/confirmation_instructions.html`
|
||||||
|
* `security/mail/confirmation_instructions.txt`
|
||||||
|
* `security/mail/login_instructions.html`
|
||||||
|
* `security/mail/login_instructions.txt`
|
||||||
|
* `security/mail/reset_instructions.html`
|
||||||
|
* `security/mail/reset_instructions.txt`
|
||||||
|
* `security/mail/reset_notice.html`
|
||||||
|
* `security/mail/change_notice.txt`
|
||||||
|
* `security/mail/change_notice.html`
|
||||||
|
* `security/mail/reset_notice.txt`
|
||||||
|
* `security/mail/welcome.html`
|
||||||
|
* `security/mail/welcome.txt`
|
||||||
|
|
||||||
|
Overriding these templates is simple:
|
||||||
|
|
||||||
|
1. Create a folder named ``security`` within your application's templates folder
|
||||||
|
2. Create a folder named ``email`` within the ``security`` folder
|
||||||
|
3. Create a template with the same name for the template you wish to override
|
||||||
|
|
||||||
|
Each template is passed a template context object that includes values for any
|
||||||
|
links that are required in the email. If you require more values in the
|
||||||
|
templates you can specify an email context processor with the
|
||||||
|
``email_context_processor`` decorator. For example::
|
||||||
|
|
||||||
|
security = Security(app, user_datastore)
|
||||||
|
|
||||||
|
# This processor is added to all emails
|
||||||
|
@security.email_context_processor
|
||||||
|
def security_mail_processor():
|
||||||
|
return dict(hello="world")
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
Features
|
||||||
|
========
|
||||||
|
|
||||||
|
Flask-Security allows you to quickly add common security mechanisms to your
|
||||||
|
Flask application. They include:
|
||||||
|
|
||||||
|
|
||||||
|
Session Based Authentication
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Session based authentication is fulfilled entirely by the `Flask-Login`_
|
||||||
|
extension. Flask-Security handles the configuration of Flask-Login automatically
|
||||||
|
based on a few of its own configuration values and uses Flask-Login's
|
||||||
|
`alternative token`_ feature for remembering users when their session has
|
||||||
|
expired.
|
||||||
|
|
||||||
|
|
||||||
|
Role/Identity Based Access
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Flask-Security implements very basic role management out of the box. This means
|
||||||
|
that you can associate a high level role or multiple roles to any user. For
|
||||||
|
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
|
||||||
|
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
|
||||||
|
control you can refer to the Flask-Princpal `documentation on this topic`_.
|
||||||
|
|
||||||
|
|
||||||
|
Password Encryption
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Password encryption is enabled with `passlib`_. Passwords are stored in plain
|
||||||
|
text by default but you can easily configure the encryption algorithm. You
|
||||||
|
should **always use an encryption algorithm** in your production environment.
|
||||||
|
You may also specify to use HMAC with a configured salt value in addition to the
|
||||||
|
algorithm chosen. Bear in mind passlib does not assume which algorithm you will choose and may require additional libraries to be installed.
|
||||||
|
|
||||||
|
|
||||||
|
Basic HTTP Authentication
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Basic HTTP authentication is achievable using a simple view method decorator.
|
||||||
|
This feature expects the incoming authentication information to identify a user
|
||||||
|
in the system. This means that the username must be equal to their email address.
|
||||||
|
|
||||||
|
|
||||||
|
Token Authentication
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Token based authentication is enabled by retrieving the user auth token by
|
||||||
|
performing an HTTP POST with the authentication details as JSON data against the
|
||||||
|
authentication endpoint. A successful call to this endpoint will return the
|
||||||
|
user's ID and their authentication token. This token can be used in subsequent
|
||||||
|
requests to protected resources. The auth token is supplied in the request
|
||||||
|
through an HTTP header or query string parameter. By default the HTTP header
|
||||||
|
name is `X-Auth-Token` and the default query string parameter name is
|
||||||
|
`auth_token`. Authentication tokens are generated using the user's password.
|
||||||
|
Thus if the user changes his or her password their existing authentication token
|
||||||
|
will become invalid. A new token will need to be retrieved using the user's new
|
||||||
|
password.
|
||||||
|
|
||||||
|
|
||||||
|
Email Confirmation
|
||||||
|
------------------
|
||||||
|
|
||||||
|
If desired you can require that new users confirm their email address.
|
||||||
|
Flask-Security will send an email message to any new users with an confirmation
|
||||||
|
link. Upon navigating to the confirmation link, the user will be automatically
|
||||||
|
logged in. There is also view for resending a confirmation link to a given email
|
||||||
|
if the user happens to try to use an expired token or has lost the previous
|
||||||
|
email. Confirmation links can be configured to expire after a specified amount
|
||||||
|
of time.
|
||||||
|
|
||||||
|
|
||||||
|
Password Reset/Recovery
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Password reset and recovery is available for when a user forgets his or her
|
||||||
|
password. Flask-Security sends an email to the user with a link to a view which
|
||||||
|
they can reset their password. Once the password is reset they are automatically
|
||||||
|
logged in and can use the new password from then on. Password reset links can
|
||||||
|
be configured to expire after a specified amount of time.
|
||||||
|
|
||||||
|
|
||||||
|
User Registration
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
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.
|
||||||
|
This view can be overrided if your registration process requires more fields.
|
||||||
|
|
||||||
|
|
||||||
|
Login Tracking
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Flask-Security can, if configured, keep track of basic login events and
|
||||||
|
statistics. They include:
|
||||||
|
|
||||||
|
* Last login date
|
||||||
|
* Current login date
|
||||||
|
* Last login IP address
|
||||||
|
* Current login IP address
|
||||||
|
* Total login count
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. _Flask-Login: http://packages.python.org/Flask-Login/
|
||||||
|
.. _alternative token: http://packages.python.org/Flask-Login/#alternative-tokens
|
||||||
|
.. _Flask-Principal: http://packages.python.org/Flask-Principal/
|
||||||
|
.. _documentation on this topic: http://packages.python.org/Flask-Principal/#granular-resource-protection
|
||||||
|
.. _passlib: http://packages.python.org/passlib/
|
||||||
+31
-305
@@ -1,311 +1,37 @@
|
|||||||
.. Flask-Security documentation master file, created by
|
|
||||||
sphinx-quickstart on Mon Mar 12 15:35:21 2012.
|
|
||||||
You can adapt this file completely to your liking, but it should at least
|
|
||||||
contain the root `toctree` directive.
|
|
||||||
|
|
||||||
Flask-Security
|
Flask-Security
|
||||||
==============
|
==============
|
||||||
|
|
||||||
.. module:: flask_security
|
Flask-Security allows you to quickly add common security mechanisms to your
|
||||||
|
Flask application. They include:
|
||||||
|
|
||||||
Simple security for Flask applications combining
|
1. Session based authentication
|
||||||
`Flask-Login <http://packages.python.org/Flask-Login/>`_,
|
2. Role management
|
||||||
`Flask-Principal <http://packages.python.org/Flask-Principal/>`_,
|
3. Password encryption
|
||||||
`Flask-WTF <http://packages.python.org/Flask-WTF/>`_,
|
4. Basic HTTP authentication
|
||||||
`passlib <http://packages.python.org/passlib/>`_, and your choice of datastore.
|
5. Token based authentication
|
||||||
Currently `SQLAlchemy <http://www.sqlalchemy.org>`_ via
|
6. Token based account activation (optional)
|
||||||
`Flask-SQLAlchemy <http://packages.python.org/Flask-SQLAlchemy/>`_ and
|
7. Token based password recovery / resetting (optional)
|
||||||
`MongoEngine <http://www.mongoengine.org/>`_ via
|
8. User registration (optional)
|
||||||
`Flask-MongoEngine <https://github.com/sbook/flask-mongoengine/>`_ are supported
|
9. Login tracking (optional)
|
||||||
out of the box. You will need to install the necessary Flask extensions that
|
|
||||||
you'll be using on your own. Additionally, you may need to install an encryption
|
Many of these features are made possible by integrating various Flask extensions
|
||||||
library such as `py-bcrypt <http://www.mindrot.org/projects/py-bcrypt/>`_ (if
|
and libraries. They include:
|
||||||
you plan to use bcrypt) for your desired encryption method.
|
|
||||||
|
1. `Flask-Login <http://packages.python.org/Flask-Login/>`_
|
||||||
|
2. `Flask-Mail <http://packages.python.org/Flask-Mail/>`_
|
||||||
|
3. `Flask-Principal <http://packages.python.org/Flask-Principal/>`_
|
||||||
|
4. `Flask-Script <http://packages.python.org/Flask-Script/>`_
|
||||||
|
5. `Flask-WTF <http://packages.python.org/Flask-WTF/>`_
|
||||||
|
6. `itsdangerous <http://packages.python.org/itsdangerous/>`_
|
||||||
|
7. `passlib <http://packages.python.org/passlib/>`_
|
||||||
|
|
||||||
|
Additionally, it assumes you'll be using a common library for your database
|
||||||
|
connections and model definitions. Flask-Security supports the following Flask
|
||||||
|
extensions out of the box for data persistence:
|
||||||
|
|
||||||
|
1. `Flask-SQLAlchemy <http://pypi.python.org/pypi/flask-sqlalchemy/>`_
|
||||||
|
2. `Flask-MongoEngine <http://pypi.python.org/pypi/flask-mongoengine/>`_
|
||||||
|
3. `Flask-Peewee <http://pypi.python.org/pypi/flask-peewee/>`_
|
||||||
|
|
||||||
|
|
||||||
Contents
|
.. include:: contents.rst.inc
|
||||||
=========
|
|
||||||
* :ref:`overview`
|
|
||||||
* :ref:`installation`
|
|
||||||
* :ref:`getting-started`
|
|
||||||
* :ref:`api`
|
|
||||||
* :doc:`Changelog </changelog>`
|
|
||||||
|
|
||||||
|
|
||||||
.. _overview:
|
|
||||||
|
|
||||||
Overview
|
|
||||||
========
|
|
||||||
|
|
||||||
Flask-Security does a few things that Flask-Login and Flask-Principal don't
|
|
||||||
provide out of the box. They are:
|
|
||||||
|
|
||||||
1. Setting up login and logout endpoints
|
|
||||||
2. Authenticating users based on username or email
|
|
||||||
3. Limiting access based on user 'roles'
|
|
||||||
4. User and role creation
|
|
||||||
5. Password encryption
|
|
||||||
|
|
||||||
That being said, you can still hook into things such as the Flask-Login and
|
|
||||||
Flask-Principal signals if need be.
|
|
||||||
|
|
||||||
|
|
||||||
.. _installation:
|
|
||||||
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
First, install Flask-Security::
|
|
||||||
|
|
||||||
$ mkvirtualenv app-name
|
|
||||||
$ pip install https://github.com/mattupstate/flask-security/tarball/master
|
|
||||||
|
|
||||||
Then install your datastore requirement.
|
|
||||||
|
|
||||||
**SQLAlchemy**::
|
|
||||||
|
|
||||||
$ pip install Flask-SQLAlchemy
|
|
||||||
|
|
||||||
**MongoEngine**::
|
|
||||||
|
|
||||||
$ pip install https://github.com/sbook/flask-mongoengine/tarball/master
|
|
||||||
|
|
||||||
|
|
||||||
.. _getting-started:
|
|
||||||
|
|
||||||
Getting Started
|
|
||||||
===============
|
|
||||||
|
|
||||||
The following code samples will illustrate how to get started using SQLAlchemy.
|
|
||||||
First thing you'll want to do is setup your application and datastore::
|
|
||||||
|
|
||||||
from flask import Flask, render_template
|
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
|
||||||
from flask.ext.security import User, Security, LoginForm,
|
|
||||||
login_required, roles_accepted, user_datastore
|
|
||||||
from flask.ext.security.datastore.sqlalchemy import SQLAlchemyUserDataStore
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.config['SECRET_KEY'] = 'secret'
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
|
||||||
Security(app, SQLAlchemyUserDatastore(db))
|
|
||||||
|
|
||||||
You'll probably want to at least one user to the database to test this out, so
|
|
||||||
you can add something such as the following to quickly add an initial user::
|
|
||||||
|
|
||||||
@app.before_first_request
|
|
||||||
def before_first_request():
|
|
||||||
user_datastore.create_role(name='admin')
|
|
||||||
user_datastore.create_user(username='matt', email='matt@something.com',
|
|
||||||
password='password', roles['admin'])
|
|
||||||
|
|
||||||
Next you'll want to setup your login screen. Setup your view::
|
|
||||||
|
|
||||||
@app.route("/login")
|
|
||||||
def login():
|
|
||||||
return render_template('login.html', form=LoginForm())
|
|
||||||
|
|
||||||
And corresponding template::
|
|
||||||
|
|
||||||
<form action="{{ url_for('auth.authenticate') }}" method="POST">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
{{ form.username.label }} {{ form.username }}<br/>
|
|
||||||
{{ form.password.label }} {{ form.password }}<br/>
|
|
||||||
{{ form.remember.label }} {{ form.remember }}<br/>
|
|
||||||
{{ form.submit }}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
By default, Flask-Security will redirect a user to `/profile` after logging in.
|
|
||||||
You can set this page up yourself or set the `SECURITY_POST_LOGIN` config
|
|
||||||
value to change this behavior. Regardless, setup a protected view as such::
|
|
||||||
|
|
||||||
@app.route('/profile')
|
|
||||||
@login_required
|
|
||||||
def profile():
|
|
||||||
return render_template('profile.html')
|
|
||||||
|
|
||||||
Now you have an application with basic authentication. If you run the local
|
|
||||||
development server you can visit `http://localhost:5000/login <http://localhost:5000/login>`_
|
|
||||||
to login.
|
|
||||||
|
|
||||||
The last thing you'll want to do is add a logout link to your templates. This
|
|
||||||
can be achieved with::
|
|
||||||
|
|
||||||
<a href="{{ url_for('auth.logout') }}">Logout</a>
|
|
||||||
|
|
||||||
Now, for instance, say you want to protect an admin area to users that are
|
|
||||||
administrators. You can use the `roles_accepted` decorator to prevent access.
|
|
||||||
The corresponding view would look like such::
|
|
||||||
|
|
||||||
@app.route('/admin')
|
|
||||||
@roles_accepted('admin')
|
|
||||||
def admin():
|
|
||||||
return render_template('admin/index.html')
|
|
||||||
|
|
||||||
And lastly, maybe you only want to show something in a template if a user has a
|
|
||||||
specific role::
|
|
||||||
|
|
||||||
{% if current_user.has_role('admin') %}
|
|
||||||
<a href="{{ url_for('admin.index') }}">Admin Panel</a>
|
|
||||||
{$ endif %}
|
|
||||||
|
|
||||||
|
|
||||||
.. _configuration:
|
|
||||||
|
|
||||||
Configuration Values
|
|
||||||
====================
|
|
||||||
|
|
||||||
* :attr:`SECURITY_URL_PREFIX`: Specifies the URL prefix for the Security
|
|
||||||
blueprint
|
|
||||||
* :attr:`SECURITY_AUTH_PROVIDER`: Specifies the class to use as the
|
|
||||||
authentication provider. Such as `flask.ext.security.AuthenticationProvider`
|
|
||||||
* :attr:`SECURITY_PASSWORD_HASH`: Specifies the encryption method to use. e.g.:
|
|
||||||
plaintext, bcrypt, etc
|
|
||||||
* :attr:`SECURITY_USER_DATASTORE`: Specifies the property name to use for the
|
|
||||||
user datastore on the application instance
|
|
||||||
* :attr:`SECURITY_LOGIN_FORM`: Specifies the form class to use when processing
|
|
||||||
an authentication request
|
|
||||||
* :attr:`SECURITY_AUTH_URL`: Specifies the URL to to handle authentication
|
|
||||||
* :attr:`SECURITY_LOGOUT_URL`: Specifies the URL to process a logout request
|
|
||||||
* :attr:`SECURITY_LOGIN_VIEW`: Specifies the URL to redirect to when
|
|
||||||
authentication is required
|
|
||||||
* :attr:`SECURITY_POST_LOGIN`: Specifies the URL to redirect to after a user is
|
|
||||||
authenticated
|
|
||||||
* :attr:`SECURITY_POST_LOGOUT`: Specifies the URL to redirect to after a user
|
|
||||||
logs out
|
|
||||||
* :attr:`SECURITY_FLASH_MESSAGES`: Specifies wether or not to flash messages
|
|
||||||
during authentication request
|
|
||||||
|
|
||||||
|
|
||||||
.. _api:
|
|
||||||
|
|
||||||
API
|
|
||||||
===
|
|
||||||
|
|
||||||
.. autoclass:: flask_security.Security
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. data:: flask_security.current_user
|
|
||||||
|
|
||||||
A proxy for the current user.
|
|
||||||
|
|
||||||
|
|
||||||
Protecting Views
|
|
||||||
----------------
|
|
||||||
.. autofunction:: flask_security.login_required
|
|
||||||
|
|
||||||
.. autofunction:: flask_security.roles_required
|
|
||||||
|
|
||||||
.. autofunction:: flask_security.roles_accepted
|
|
||||||
|
|
||||||
|
|
||||||
User Object Helpers
|
|
||||||
-------------------
|
|
||||||
.. autoclass:: flask_security.UserMixin
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: flask_security.RoleMixin
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: flask_security.AnonymousUser
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
Datastores
|
|
||||||
----------
|
|
||||||
.. autoclass:: flask_security.datastore.UserDatastore
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: flask_security.datastore.sqlalchemy.SQLAlchemyUserDatastore
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: flask_security.datastore.mongoengine.MongoEngineUserDatastore
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
Models
|
|
||||||
------
|
|
||||||
.. autoclass:: flask_security.User
|
|
||||||
|
|
||||||
.. attribute:: id
|
|
||||||
|
|
||||||
User ID
|
|
||||||
|
|
||||||
.. attribute:: username
|
|
||||||
|
|
||||||
Username
|
|
||||||
|
|
||||||
.. attribute:: email
|
|
||||||
|
|
||||||
Email address
|
|
||||||
|
|
||||||
.. attribute:: password
|
|
||||||
|
|
||||||
Password
|
|
||||||
|
|
||||||
.. attribute:: active
|
|
||||||
|
|
||||||
Active state
|
|
||||||
|
|
||||||
.. attribute:: roles
|
|
||||||
|
|
||||||
User roles
|
|
||||||
|
|
||||||
.. attribute:: created_at
|
|
||||||
|
|
||||||
Created date
|
|
||||||
|
|
||||||
.. attribute:: modified_at
|
|
||||||
|
|
||||||
Modified date
|
|
||||||
|
|
||||||
|
|
||||||
.. autoclass:: flask_security.Role
|
|
||||||
|
|
||||||
.. attribute:: id
|
|
||||||
|
|
||||||
Role ID
|
|
||||||
|
|
||||||
.. attribute:: name
|
|
||||||
|
|
||||||
Role name
|
|
||||||
|
|
||||||
.. attribute:: description
|
|
||||||
|
|
||||||
Role description
|
|
||||||
|
|
||||||
|
|
||||||
Exceptions
|
|
||||||
----------
|
|
||||||
.. autoexception:: flask_security.BadCredentialsError
|
|
||||||
|
|
||||||
.. autoexception:: flask_security.AuthenticationError
|
|
||||||
|
|
||||||
.. autoexception:: flask_security.UserNotFoundError
|
|
||||||
|
|
||||||
.. autoexception:: flask_security.RoleNotFoundError
|
|
||||||
|
|
||||||
.. autoexception:: flask_security.UserIdNotFoundError
|
|
||||||
|
|
||||||
.. autoexception:: flask_security.UserDatastoreError
|
|
||||||
|
|
||||||
.. autoexception:: flask_security.UserCreationError
|
|
||||||
|
|
||||||
.. autoexception:: flask_security.RoleCreationError
|
|
||||||
|
|
||||||
|
|
||||||
Signals
|
|
||||||
-------
|
|
||||||
See the documentation for the signals provided by the Flask-Login and
|
|
||||||
Flask-Principal extensions. Flask-Security does not provide any additional
|
|
||||||
signals.
|
|
||||||
|
|
||||||
|
|
||||||
Changelog
|
|
||||||
=========
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
changelog
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
Models
|
||||||
|
======
|
||||||
|
|
||||||
|
Flask-Security assumes you'll be using libraries such as SQLAlchemy,
|
||||||
|
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
|
||||||
|
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
|
||||||
|
minimum your `User` and `Role` model should include the following fields:
|
||||||
|
|
||||||
|
**User**
|
||||||
|
|
||||||
|
* ``id``
|
||||||
|
* ``email``
|
||||||
|
* ``password``
|
||||||
|
* ``active``
|
||||||
|
|
||||||
|
**Role**
|
||||||
|
|
||||||
|
* ``id``
|
||||||
|
* ``name``
|
||||||
|
* ``description``
|
||||||
|
|
||||||
|
|
||||||
|
Additional Functionality
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Depending on the application's configuration, additional fields may need to be
|
||||||
|
added to your `User` model.
|
||||||
|
|
||||||
|
Confirmable
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
If you enable account confirmation by setting your application's
|
||||||
|
`SECURITY_CONFIRMABLE` configuration value to `True` your `User` model will
|
||||||
|
require the following additional field:
|
||||||
|
|
||||||
|
* ``confirmed_at``
|
||||||
|
|
||||||
|
Trackable
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
If you enable user tracking by setting your application's `SECURITY_TRACKABLE`
|
||||||
|
configuration value to `True` your `User` model will require the following
|
||||||
|
additional fields:
|
||||||
|
|
||||||
|
* ``last_login_at``
|
||||||
|
* ``current_login_at``
|
||||||
|
* ``last_login_ip``
|
||||||
|
* ``current_login_ip``
|
||||||
|
* ``login_count``
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
Quick Start
|
||||||
|
===========
|
||||||
|
|
||||||
|
- `Basic SQLAlchemy Application <#basic-sqlalchemy-application>`_
|
||||||
|
- `Basic MongoEngine Application <#basic-mongoengine-application>`_
|
||||||
|
- `Basic Peewee Application <#basic-peewee-application>`_
|
||||||
|
- `Mail Configuration <#mail-configuration>`_
|
||||||
|
|
||||||
|
Basic SQLAlchemy Application
|
||||||
|
=============================
|
||||||
|
|
||||||
|
SQLAlchemy Install requirements
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ mkvirtualenv <your-app-name>
|
||||||
|
$ pip install flask-security flask-sqlalchemy
|
||||||
|
|
||||||
|
|
||||||
|
SQLAlchemy Application
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The following code sample illustrates how to get started as quickly as
|
||||||
|
possible using SQLAlchemy:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from flask import Flask, render_template
|
||||||
|
from flask.ext.sqlalchemy import SQLAlchemy
|
||||||
|
from flask.ext.security import Security, SQLAlchemyUserDatastore, \
|
||||||
|
UserMixin, RoleMixin, login_required
|
||||||
|
|
||||||
|
# Create app
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['DEBUG'] = True
|
||||||
|
app.config['SECRET_KEY'] = 'super-secret'
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
|
||||||
|
|
||||||
|
# Create database connection object
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
# Define models
|
||||||
|
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))
|
||||||
|
active = db.Column(db.Boolean())
|
||||||
|
confirmed_at = db.Column(db.DateTime())
|
||||||
|
roles = db.relationship('Role', secondary=roles_users,
|
||||||
|
backref=db.backref('users', lazy='dynamic'))
|
||||||
|
|
||||||
|
# Setup Flask-Security
|
||||||
|
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
|
||||||
|
security = Security(app, user_datastore)
|
||||||
|
|
||||||
|
# Create a user to test with
|
||||||
|
@app.before_first_request
|
||||||
|
def create_user():
|
||||||
|
db.create_all()
|
||||||
|
user_datastore.create_user(email='matt@nobien.net', password='password')
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Views
|
||||||
|
@app.route('/')
|
||||||
|
@login_required
|
||||||
|
def home():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
Basic MongoEngine Application
|
||||||
|
==============================
|
||||||
|
|
||||||
|
MongoEngine Install requirements
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ mkvirtualenv <your-app-name>
|
||||||
|
$ pip install flask-security flask-mongoengine
|
||||||
|
|
||||||
|
MongoEngine Application
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The following code sample illustrates how to get started as quickly as
|
||||||
|
possible using MongoEngine:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from flask import Flask, render_template
|
||||||
|
from flask.ext.mongoengine import MongoEngine
|
||||||
|
from flask.ext.security import Security, MongoEngineUserDatastore, \
|
||||||
|
UserMixin, RoleMixin, login_required
|
||||||
|
|
||||||
|
# Create app
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['DEBUG'] = True
|
||||||
|
app.config['SECRET_KEY'] = 'super-secret'
|
||||||
|
|
||||||
|
# MongoDB Config
|
||||||
|
app.config['MONGODB_DB'] = 'mydatabase'
|
||||||
|
app.config['MONGODB_HOST'] = 'localhost'
|
||||||
|
app.config['MONGODB_PORT'] = 27017
|
||||||
|
|
||||||
|
# Create database connection object
|
||||||
|
db = MongoEngine(app)
|
||||||
|
|
||||||
|
class Role(db.Document, RoleMixin):
|
||||||
|
name = db.StringField(max_length=80, unique=True)
|
||||||
|
description = db.StringField(max_length=255)
|
||||||
|
|
||||||
|
class User(db.Document, UserMixin):
|
||||||
|
email = db.StringField(max_length=255)
|
||||||
|
password = db.StringField(max_length=255)
|
||||||
|
active = db.BooleanField(default=True)
|
||||||
|
confirmed_at = db.DateTimeField()
|
||||||
|
roles = db.ListField(db.ReferenceField(Role), default=[])
|
||||||
|
|
||||||
|
# Setup Flask-Security
|
||||||
|
user_datastore = MongoEngineUserDatastore(db, User, Role)
|
||||||
|
security = Security(app, user_datastore)
|
||||||
|
|
||||||
|
# Create a user to test with
|
||||||
|
@app.before_first_request
|
||||||
|
def create_user():
|
||||||
|
user_datastore.create_user(email='matt@nobien.net', password='password')
|
||||||
|
|
||||||
|
# Views
|
||||||
|
@app.route('/')
|
||||||
|
@login_required
|
||||||
|
def home():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
Basic Peewee Application
|
||||||
|
========================
|
||||||
|
|
||||||
|
Peewee Install requirements
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ mkvirtualenv <your-app-name>
|
||||||
|
$ pip install flask-security flask-peewee
|
||||||
|
|
||||||
|
Peewee Application
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The following code sample illustrates how to get started as quickly as
|
||||||
|
possible using Peewee:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from flask import Flask, render_template
|
||||||
|
from flask_peewee.db import Database
|
||||||
|
from peewee import *
|
||||||
|
from flask.ext.security import Security, PeeweeUserDatastore, \
|
||||||
|
UserMixin, RoleMixin, login_required
|
||||||
|
|
||||||
|
# Create app
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['DEBUG'] = True
|
||||||
|
app.config['SECRET_KEY'] = 'super-secret'
|
||||||
|
app.config['DATABASE'] = {
|
||||||
|
'name': 'example.db',
|
||||||
|
'engine': 'peewee.SqliteDatabase',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create database connection object
|
||||||
|
db = Database(app)
|
||||||
|
|
||||||
|
class Role(db.Model, RoleMixin):
|
||||||
|
name = TextField(unique=True)
|
||||||
|
description = TextField(null=True)
|
||||||
|
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
email = TextField()
|
||||||
|
password = TextField()
|
||||||
|
active = BooleanField(default=True)
|
||||||
|
confirmed_at = DateTimeField(null=True)
|
||||||
|
|
||||||
|
class UserRoles(db.Model):
|
||||||
|
# Because peewee does not come with built-in many-to-many
|
||||||
|
# relationships, we need this intermediary class to link
|
||||||
|
# user to roles.
|
||||||
|
user = ForeignKeyField(User, related_name='roles')
|
||||||
|
role = ForeignKeyField(Role, related_name='users')
|
||||||
|
name = property(lambda self: self.role.name)
|
||||||
|
description = property(lambda self: self.role.description)
|
||||||
|
|
||||||
|
# Setup Flask-Security
|
||||||
|
user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles)
|
||||||
|
security = Security(app, user_datastore)
|
||||||
|
|
||||||
|
# Create a user to test with
|
||||||
|
@app.before_first_request
|
||||||
|
def create_user():
|
||||||
|
for Model in (Role, User, UserRoles):
|
||||||
|
Model.drop_table(fail_silently=True)
|
||||||
|
Model.create_table(fail_silently=True)
|
||||||
|
user_datastore.create_user(email='matt@nobien.net', password='password')
|
||||||
|
|
||||||
|
# Views
|
||||||
|
@app.route('/')
|
||||||
|
@login_required
|
||||||
|
def home():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
Mail Configuration
|
||||||
|
===================
|
||||||
|
|
||||||
|
Flask-Security integrates with Flask-Mail to handle all email
|
||||||
|
communications between user and site, so it's important to configure
|
||||||
|
Flask-Mail with your email server details so Flask-Security can talk
|
||||||
|
with Flask-Mail correctly.
|
||||||
|
|
||||||
|
The following code illustrates a basic setup, which could be added to
|
||||||
|
the basic application code in the previous section::
|
||||||
|
|
||||||
|
# At top of file
|
||||||
|
from flask_mail import Mail
|
||||||
|
|
||||||
|
# After 'Create app'
|
||||||
|
app.config['MAIL_SERVER'] = 'smtp.example.com'
|
||||||
|
app.config['MAIL_PORT'] = 465
|
||||||
|
app.config['MAIL_USE_SSL'] = True
|
||||||
|
app.config['MAIL_USERNAME'] = 'username'
|
||||||
|
app.config['MAIL_PASSWORD'] = 'password'
|
||||||
|
mail = Mail(app)
|
||||||
|
|
||||||
|
To learn more about the various Flask-Mail settings to configure it to
|
||||||
|
work with your particular email server configuration, please see the
|
||||||
|
`Flask-Mail documentation <http://packages.python.org/Flask-Mail/>`_.
|
||||||
-118
@@ -1,118 +0,0 @@
|
|||||||
# a little trick so you can run:
|
|
||||||
# $ python example/app.py
|
|
||||||
# from the root of the security project
|
|
||||||
import sys, os
|
|
||||||
sys.path.pop(0)
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
|
|
||||||
from flask import Flask, render_template
|
|
||||||
|
|
||||||
from flask.ext.mongoengine import MongoEngine
|
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
|
||||||
|
|
||||||
from flask.ext.security import (Security, LoginForm, user_datastore,
|
|
||||||
login_required, roles_required, roles_accepted)
|
|
||||||
|
|
||||||
from flask.ext.security.datastore.sqlalchemy import SQLAlchemyUserDatastore
|
|
||||||
from flask.ext.security.datastore.mongoengine import MongoEngineUserDatastore
|
|
||||||
|
|
||||||
def create_roles():
|
|
||||||
for role in ('admin', 'editor', 'author'):
|
|
||||||
user_datastore.create_role(name=role)
|
|
||||||
|
|
||||||
def create_users():
|
|
||||||
for u in (('matt','matt@lp.com','password',['admin'],True),
|
|
||||||
('joe','joe@lp.com','password',['editor'],True),
|
|
||||||
('jill','jill@lp.com','password',['author'],True),
|
|
||||||
('tiya','tiya@lp.com','password',[],False)):
|
|
||||||
user_datastore.create_user(username=u[0], email=u[1], password=u[2],
|
|
||||||
roles=u[3], active=u[4])
|
|
||||||
|
|
||||||
def populate_data():
|
|
||||||
create_roles()
|
|
||||||
create_users()
|
|
||||||
|
|
||||||
def create_app(auth_config):
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.debug = True
|
|
||||||
app.config['SECRET_KEY'] = 'secret'
|
|
||||||
|
|
||||||
if auth_config:
|
|
||||||
for key, value in auth_config.items():
|
|
||||||
app.config[key] = value
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
return render_template('index.html', content='Home Page')
|
|
||||||
|
|
||||||
@app.route('/login')
|
|
||||||
def login():
|
|
||||||
return render_template('login.html', content='Login Page', form=LoginForm())
|
|
||||||
|
|
||||||
@app.route('/custom_login')
|
|
||||||
def custom_login():
|
|
||||||
return render_template('login.html', content='Custom Login Page', form=LoginForm())
|
|
||||||
|
|
||||||
@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('/post_logout')
|
|
||||||
def post_logout():
|
|
||||||
return render_template('index.html', content='Post Logout')
|
|
||||||
|
|
||||||
@app.route('/admin')
|
|
||||||
@roles_required('admin')
|
|
||||||
def admin():
|
|
||||||
return render_template('index.html', content='Admin Page')
|
|
||||||
|
|
||||||
@app.route('/admin_or_editor')
|
|
||||||
@roles_accepted('admin', 'editor')
|
|
||||||
def admin_or_editor():
|
|
||||||
return render_template('index.html', content='Admin or Editor Page')
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
def create_sqlalchemy_app(auth_config=None):
|
|
||||||
app = create_app(auth_config)
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/flask_security_example.sqlite'
|
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
|
||||||
Security(app, SQLAlchemyUserDatastore(db))
|
|
||||||
|
|
||||||
@app.before_first_request
|
|
||||||
def before_first_request():
|
|
||||||
db.drop_all()
|
|
||||||
db.create_all()
|
|
||||||
populate_data()
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
def create_mongoengine_app(auth_config=None):
|
|
||||||
app = create_app(auth_config)
|
|
||||||
app.config['MONGODB_DB'] = 'flask_security_example'
|
|
||||||
app.config['MONGODB_HOST'] = 'localhost'
|
|
||||||
app.config['MONGODB_PORT'] = 27017
|
|
||||||
|
|
||||||
db = MongoEngine(app)
|
|
||||||
Security(app, MongoEngineUserDatastore(db))
|
|
||||||
|
|
||||||
@app.before_first_request
|
|
||||||
def before_first_request():
|
|
||||||
from flask.ext.security import User, Role
|
|
||||||
User.drop_collection()
|
|
||||||
Role.drop_collection()
|
|
||||||
populate_data()
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app = create_sqlalchemy_app()
|
|
||||||
#app = create_mongoengine_app()
|
|
||||||
app.run()
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# a little trick so you can run:
|
|
||||||
# $ python example/app.py
|
|
||||||
# from the root of the security project
|
|
||||||
import sys, os
|
|
||||||
sys.path.pop(0)
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
|
|
||||||
from example import app
|
|
||||||
from flask.ext.script import Manager
|
|
||||||
from flask.ext.security.script import (CreateUserCommand , AddRoleCommand,
|
|
||||||
RemoveRoleCommand, ActivateUserCommand, DeactivateUserCommand)
|
|
||||||
|
|
||||||
manager = Manager(app.create_sqlalchemy_app())
|
|
||||||
manager.add_command('create_user', CreateUserCommand())
|
|
||||||
manager.add_command('add_role', AddRoleCommand())
|
|
||||||
manager.add_command('remove_role', RemoveRoleCommand())
|
|
||||||
manager.add_command('deactivate_user', DeactivateUserCommand())
|
|
||||||
manager.add_command('activate_user', ActivateUserCommand())
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
manager.run()
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{% include "_messages.html" %}
|
|
||||||
{% include "_nav.html" %}
|
|
||||||
<form action="{{ url_for('auth.authenticate') }}" method="POST" name="login_form">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
{{ form.username.label }} {{ form.username }}<br/>
|
|
||||||
{{ form.password.label }} {{ form.password }}<br/>
|
|
||||||
{{ form.remember.label }} {{ form.remember }}<br/>
|
|
||||||
{{ form.submit }}
|
|
||||||
</form>
|
|
||||||
<p>{{ content }}</p>
|
|
||||||
+11
-458
@@ -3,468 +3,21 @@
|
|||||||
flask.ext.security
|
flask.ext.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.
|
||||||
|
|
||||||
: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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
__version__ = '1.6.7'
|
||||||
|
|
||||||
from datetime import datetime
|
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
|
||||||
from types import StringType
|
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
|
||||||
|
from .decorators import auth_token_required, http_auth_required, \
|
||||||
from flask import (current_app, Blueprint, flash, redirect, request,
|
login_required, roles_accepted, roles_required, auth_required
|
||||||
session, _request_ctx_stack, url_for, abort, g)
|
from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
|
||||||
|
ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
|
||||||
from flask.ext.login import (AnonymousUser as AnonymousUserBase,
|
from .signals import confirm_instructions_sent, password_reset, \
|
||||||
UserMixin as BaseUserMixin, LoginManager, login_required, login_user,
|
reset_password_instructions_sent, user_confirmed, user_registered
|
||||||
logout_user, current_user, user_logged_in, user_logged_out)
|
from .utils import login_user, logout_user, url_for_security
|
||||||
|
|
||||||
from flask.ext.principal import (Identity, Principal, RoleNeed, UserNeed,
|
|
||||||
Permission, AnonymousIdentity, identity_changed, identity_loaded)
|
|
||||||
|
|
||||||
from flask.ext.wtf import (Form, TextField, PasswordField, SubmitField,
|
|
||||||
HiddenField, Required, ValidationError, BooleanField, Email)
|
|
||||||
|
|
||||||
from functools import wraps
|
|
||||||
from passlib.context import CryptContext
|
|
||||||
from werkzeug.utils import import_string
|
|
||||||
from werkzeug.local import LocalProxy
|
|
||||||
|
|
||||||
class User(object):
|
|
||||||
"""User model"""
|
|
||||||
|
|
||||||
class Role(object):
|
|
||||||
"""Role model"""
|
|
||||||
|
|
||||||
URL_PREFIX_KEY = 'SECURITY_URL_PREFIX'
|
|
||||||
AUTH_PROVIDER_KEY = 'SECURITY_AUTH_PROVIDER'
|
|
||||||
PASSWORD_HASH_KEY = 'SECURITY_PASSWORD_HASH'
|
|
||||||
USER_DATASTORE_KEY = 'SECURITY_USER_DATASTORE'
|
|
||||||
LOGIN_FORM_KEY = 'SECURITY_LOGIN_FORM'
|
|
||||||
AUTH_URL_KEY = 'SECURITY_AUTH_URL'
|
|
||||||
LOGOUT_URL_KEY = 'SECURITY_LOGOUT_URL'
|
|
||||||
LOGIN_VIEW_KEY = 'SECURITY_LOGIN_VIEW'
|
|
||||||
POST_LOGIN_KEY = 'SECURITY_POST_LOGIN'
|
|
||||||
POST_LOGOUT_KEY = 'SECURITY_POST_LOGOUT'
|
|
||||||
FLASH_MESSAGES_KEY = 'SECURITY_FLASH_MESSAGES'
|
|
||||||
|
|
||||||
DEBUG_LOGIN = 'User %s logged in. Redirecting to: %s'
|
|
||||||
ERROR_LOGIN = 'Unsuccessful authentication attempt: %s. Redirecting to: %s'
|
|
||||||
DEBUG_LOGOUT = 'User logged out, redirecting to: %s'
|
|
||||||
FLASH_INACTIVE = 'Inactive user'
|
|
||||||
FLASH_PERMISSIONS = 'You do not have permission to view this resource.'
|
|
||||||
|
|
||||||
#: Default Flask-Security configuration
|
|
||||||
default_config = {
|
|
||||||
URL_PREFIX_KEY: None,
|
|
||||||
FLASH_MESSAGES_KEY: True,
|
|
||||||
PASSWORD_HASH_KEY: 'plaintext',
|
|
||||||
USER_DATASTORE_KEY: 'user_datastore',
|
|
||||||
AUTH_PROVIDER_KEY: 'flask.ext.security.AuthenticationProvider',
|
|
||||||
LOGIN_FORM_KEY: 'flask.ext.security.LoginForm',
|
|
||||||
AUTH_URL_KEY: '/auth',
|
|
||||||
LOGOUT_URL_KEY: '/logout',
|
|
||||||
LOGIN_VIEW_KEY: '/login',
|
|
||||||
POST_LOGIN_KEY: '/',
|
|
||||||
POST_LOGOUT_KEY: '/',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BadCredentialsError(Exception):
|
|
||||||
"""Raised when an authentication attempt fails due to an error with the
|
|
||||||
provided credentials.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class AuthenticationError(Exception):
|
|
||||||
"""Raised when an authentication attempt fails due to invalid configuration
|
|
||||||
or an unknown reason.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class UserNotFoundError(Exception):
|
|
||||||
"""Raised by a user datastore when there is an attempt to find a user by
|
|
||||||
their identifier, often username or email, and the user is not found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class RoleNotFoundError(Exception):
|
|
||||||
"""Raised by a user datastore when there is an attempt to find a role and
|
|
||||||
the role cannot be found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class UserIdNotFoundError(Exception):
|
|
||||||
"""Raised by a user datastore when there is an attempt to find a user by
|
|
||||||
ID and the user is not found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class UserDatastoreError(Exception):
|
|
||||||
"""Raised when a user datastore experiences an unexpected error
|
|
||||||
"""
|
|
||||||
|
|
||||||
class UserCreationError(Exception):
|
|
||||||
"""Raised when an error occurs when creating a user
|
|
||||||
"""
|
|
||||||
|
|
||||||
class RoleCreationError(Exception):
|
|
||||||
"""Raised when an error occurs when creating a role
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
#: App logger for convenience
|
|
||||||
logger = LocalProxy(lambda: current_app.logger)
|
|
||||||
|
|
||||||
#: Authentication provider
|
|
||||||
auth_provider = LocalProxy(lambda: current_app.auth_provider)
|
|
||||||
|
|
||||||
#: Login manager
|
|
||||||
login_manager = LocalProxy(lambda: current_app.login_manager)
|
|
||||||
|
|
||||||
#: Password encyption context
|
|
||||||
pwd_context = LocalProxy(lambda: current_app.pwd_context)
|
|
||||||
|
|
||||||
#: User datastore
|
|
||||||
user_datastore = LocalProxy(lambda: getattr(current_app,
|
|
||||||
current_app.config[USER_DATASTORE_KEY]))
|
|
||||||
|
|
||||||
def roles_required(*args):
|
|
||||||
"""View decorator which specifies that a user must have all the specified
|
|
||||||
roles. Example::
|
|
||||||
|
|
||||||
@app.route('/dashboard')
|
|
||||||
@roles_required('admin', 'editor')
|
|
||||||
def dashboard():
|
|
||||||
return 'Dashboard'
|
|
||||||
|
|
||||||
The current user must have both the `admin` role and `editor` role in order
|
|
||||||
to view the page.
|
|
||||||
|
|
||||||
:param args: The required roles.
|
|
||||||
"""
|
|
||||||
roles = args
|
|
||||||
perm = Permission(*[RoleNeed(role) for role in roles])
|
|
||||||
def wrapper(fn):
|
|
||||||
@wraps(fn)
|
|
||||||
def decorated_view(*args, **kwargs):
|
|
||||||
if not current_user.is_authenticated():
|
|
||||||
return redirect(current_app.config[LOGIN_VIEW_KEY])
|
|
||||||
|
|
||||||
if perm.can():
|
|
||||||
return fn(*args, **kwargs)
|
|
||||||
|
|
||||||
logger.debug('Identity does not provide all of the '
|
|
||||||
'following roles: %s' % [r for r in roles])
|
|
||||||
|
|
||||||
do_flash(FLASH_PERMISSIONS, 'error')
|
|
||||||
return redirect(request.referrer or '/')
|
|
||||||
return decorated_view
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def roles_accepted(*args):
|
|
||||||
"""View decorator which specifies that a user must have at least one of the
|
|
||||||
specified roles. Example::
|
|
||||||
|
|
||||||
@app.route('/create_post')
|
|
||||||
@roles_accepted('editor', 'author')
|
|
||||||
def create_post():
|
|
||||||
return 'Create Post'
|
|
||||||
|
|
||||||
The current user must have either the `editor` role or `author` role in
|
|
||||||
order to view the page.
|
|
||||||
|
|
||||||
:param args: The possible roles.
|
|
||||||
"""
|
|
||||||
roles = args
|
|
||||||
perms = [Permission(RoleNeed(role)) for role in roles]
|
|
||||||
def wrapper(fn):
|
|
||||||
@wraps(fn)
|
|
||||||
def decorated_view(*args, **kwargs):
|
|
||||||
if not current_user.is_authenticated():
|
|
||||||
return redirect(current_app.config[LOGIN_VIEW_KEY])
|
|
||||||
|
|
||||||
for perm in perms:
|
|
||||||
if perm.can():
|
|
||||||
return fn(*args, **kwargs)
|
|
||||||
|
|
||||||
logger.debug('Identity does not provide at least one of '
|
|
||||||
'the following roles: %s' % [r for r in roles])
|
|
||||||
|
|
||||||
do_flash(FLASH_PERMISSIONS, 'error')
|
|
||||||
return redirect(request.referrer or '/')
|
|
||||||
return decorated_view
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class RoleMixin(object):
|
|
||||||
"""Mixin for `Role` model definitions"""
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.name == other.name
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return self.name != other.name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '<Role name=%s, description=%s>' % (self.name, self.description)
|
|
||||||
|
|
||||||
|
|
||||||
class UserMixin(BaseUserMixin):
|
|
||||||
"""Mixin for `User` model definitions"""
|
|
||||||
|
|
||||||
def is_active(self):
|
|
||||||
"""Returns `True` if the user is active."""
|
|
||||||
return self.active
|
|
||||||
|
|
||||||
def has_role(self, role):
|
|
||||||
"""Returns `True` if the user identifies with the specified role.
|
|
||||||
|
|
||||||
:param role: A role name or `Role` instance"""
|
|
||||||
if not isinstance(role, Role):
|
|
||||||
role = Role(name=role)
|
|
||||||
return role in self.roles
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
ctx = (str(self.id), self.username, self.email)
|
|
||||||
return '<User id=%s, username=%s, email=%s>' % ctx
|
|
||||||
|
|
||||||
|
|
||||||
class AnonymousUser(AnonymousUserBase):
|
|
||||||
def __init__(self):
|
|
||||||
super(AnonymousUser, self).__init__()
|
|
||||||
self.roles = [] # TODO: Make this immutable?
|
|
||||||
|
|
||||||
def has_role(self, *args):
|
|
||||||
"""Returns `False`"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Security(object):
|
|
||||||
"""The :class:`Security` class initializes the Flask-Security extension.
|
|
||||||
|
|
||||||
:param app: The application.
|
|
||||||
:param datastore: An instance of a user datastore.
|
|
||||||
"""
|
|
||||||
def __init__(self, app=None, datastore=None):
|
|
||||||
self.init_app(app, datastore)
|
|
||||||
|
|
||||||
def init_app(self, app, datastore):
|
|
||||||
"""Initializes the Flask-Security extension for the specified
|
|
||||||
application and datastore implentation.
|
|
||||||
|
|
||||||
:param app: The application.
|
|
||||||
:param datastore: An instance of a user datastore.
|
|
||||||
"""
|
|
||||||
if app is None or datastore is None: return
|
|
||||||
|
|
||||||
blueprint = Blueprint('auth', __name__)
|
|
||||||
|
|
||||||
configured = {}
|
|
||||||
|
|
||||||
for key, value in default_config.items():
|
|
||||||
configured[key] = app.config.get(key, value)
|
|
||||||
|
|
||||||
app.config.update(configured)
|
|
||||||
config = app.config
|
|
||||||
|
|
||||||
# setup the login manager extension
|
|
||||||
login_manager = LoginManager()
|
|
||||||
login_manager.anonymous_user = AnonymousUser
|
|
||||||
login_manager.login_view = config[LOGIN_VIEW_KEY]
|
|
||||||
login_manager.setup_app(app)
|
|
||||||
app.login_manager = login_manager
|
|
||||||
|
|
||||||
Provider = get_class_from_config(AUTH_PROVIDER_KEY, config)
|
|
||||||
Form = get_class_from_config(LOGIN_FORM_KEY, config)
|
|
||||||
pw_hash = config[PASSWORD_HASH_KEY]
|
|
||||||
|
|
||||||
app.pwd_context = CryptContext(schemes=[pw_hash], default=pw_hash)
|
|
||||||
app.auth_provider = Provider(Form)
|
|
||||||
app.principal = Principal(app)
|
|
||||||
|
|
||||||
from flask.ext import security as s
|
|
||||||
s.User, s.Role = datastore.get_models()
|
|
||||||
|
|
||||||
setattr(app, config[USER_DATASTORE_KEY], datastore)
|
|
||||||
|
|
||||||
@identity_loaded.connect_via(app)
|
|
||||||
def on_identity_loaded(sender, identity):
|
|
||||||
if hasattr(current_user, 'id'):
|
|
||||||
identity.provides.add(UserNeed(current_user.id))
|
|
||||||
|
|
||||||
for role in current_user.roles:
|
|
||||||
identity.provides.add(RoleNeed(role.name))
|
|
||||||
|
|
||||||
identity.user = current_user
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
|
||||||
def load_user(user_id):
|
|
||||||
try:
|
|
||||||
return datastore.with_id(user_id)
|
|
||||||
except Exception, e:
|
|
||||||
logger.error('Error getting user: %s' % e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
auth_url = config[AUTH_URL_KEY]
|
|
||||||
@blueprint.route(auth_url, methods=['POST'], endpoint='authenticate')
|
|
||||||
def authenticate():
|
|
||||||
try:
|
|
||||||
form = Form()
|
|
||||||
user = auth_provider.authenticate(form)
|
|
||||||
|
|
||||||
if login_user(user, remember=form.remember.data):
|
|
||||||
redirect_url = get_post_login_redirect()
|
|
||||||
identity_changed.send(app, identity=Identity(user.id))
|
|
||||||
logger.debug(DEBUG_LOGIN % (user, redirect_url))
|
|
||||||
return redirect(redirect_url)
|
|
||||||
|
|
||||||
raise BadCredentialsError(FLASH_INACTIVE)
|
|
||||||
|
|
||||||
except BadCredentialsError, e:
|
|
||||||
message = '%s' % e
|
|
||||||
do_flash(message, 'error')
|
|
||||||
redirect_url = request.referrer or login_manager.login_view
|
|
||||||
logger.error(ERROR_LOGIN % (message, redirect_url))
|
|
||||||
return redirect(redirect_url)
|
|
||||||
|
|
||||||
@blueprint.route(config[LOGOUT_URL_KEY], endpoint='logout')
|
|
||||||
@login_required
|
|
||||||
def logout():
|
|
||||||
for value in ('identity.name', 'identity.auth_type'):
|
|
||||||
session.pop(value, None)
|
|
||||||
|
|
||||||
identity_changed.send(app, identity=AnonymousIdentity())
|
|
||||||
logout_user()
|
|
||||||
|
|
||||||
redirect_url = find_redirect(POST_LOGOUT_KEY)
|
|
||||||
logger.debug(DEBUG_LOGOUT % redirect_url)
|
|
||||||
return redirect(redirect_url)
|
|
||||||
|
|
||||||
app.register_blueprint(blueprint, url_prefix=config[URL_PREFIX_KEY])
|
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(Form):
|
|
||||||
"""The default login form"""
|
|
||||||
|
|
||||||
username = TextField("Username or Email",
|
|
||||||
validators=[Required(message="Username not provided")])
|
|
||||||
password = PasswordField("Password",
|
|
||||||
validators=[Required(message="Password not provided")])
|
|
||||||
remember = BooleanField("Remember Me")
|
|
||||||
next = HiddenField()
|
|
||||||
submit = SubmitField("Login")
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(LoginForm, self).__init__(*args, **kwargs)
|
|
||||||
self.next.data = request.args.get('next', None)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationProvider(object):
|
|
||||||
"""The default authentication provider implementation.
|
|
||||||
|
|
||||||
:param login_form_class: The login form class to use when authenticating a
|
|
||||||
user
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, login_form_class=None):
|
|
||||||
self.login_form_class = login_form_class or LoginForm
|
|
||||||
|
|
||||||
def login_form(self, formdata=None):
|
|
||||||
"""Returns an instance of the login form with the provided form.
|
|
||||||
|
|
||||||
:param formdata: The incoming form data"""
|
|
||||||
return self.login_form_class(formdata)
|
|
||||||
|
|
||||||
def authenticate(self, form):
|
|
||||||
"""Processes an authentication request and returns a user instance if
|
|
||||||
authentication is successful.
|
|
||||||
|
|
||||||
:param form: An instance of a populated login form
|
|
||||||
"""
|
|
||||||
if not form.validate():
|
|
||||||
if form.username.errors:
|
|
||||||
raise BadCredentialsError(form.username.errors[0])
|
|
||||||
if form.password.errors:
|
|
||||||
raise BadCredentialsError(form.password.errors[0])
|
|
||||||
|
|
||||||
return self.do_authenticate(form.username.data, form.password.data)
|
|
||||||
|
|
||||||
def do_authenticate(self, user_identifier, password):
|
|
||||||
"""Returns the authenticated user if authentication is successfull. If
|
|
||||||
authentication fails an appropriate error is raised
|
|
||||||
|
|
||||||
:param user_identifier: The user's identifier, either an email address
|
|
||||||
or username
|
|
||||||
:param password: The user's unencrypted password
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
user = user_datastore.find_user(user_identifier)
|
|
||||||
except AttributeError, e:
|
|
||||||
self.auth_error("Could not find user service: %s" % e)
|
|
||||||
except UserNotFoundError, e:
|
|
||||||
raise BadCredentialsError("Specified user does not exist")
|
|
||||||
except AttributeError, e:
|
|
||||||
self.auth_error('Invalid user service: %s' % e)
|
|
||||||
except Exception, e:
|
|
||||||
self.auth_error('Unexpected authentication error: %s' % e)
|
|
||||||
|
|
||||||
# compare passwords
|
|
||||||
if pwd_context.verify(password, user.password):
|
|
||||||
return user
|
|
||||||
|
|
||||||
# bad match
|
|
||||||
raise BadCredentialsError("Password does not match")
|
|
||||||
|
|
||||||
def auth_error(self, msg):
|
|
||||||
"""Sends an error log message and raises an authentication error.
|
|
||||||
|
|
||||||
:param msg: An authentication error message"""
|
|
||||||
logger.error(msg)
|
|
||||||
raise AuthenticationError(msg)
|
|
||||||
|
|
||||||
def do_flash(message, category):
|
|
||||||
if current_app.config[FLASH_MESSAGES_KEY]:
|
|
||||||
flash(message, category)
|
|
||||||
|
|
||||||
|
|
||||||
def get_class_by_name(clazz):
|
|
||||||
"""Get a reference to a class by its string representation."""
|
|
||||||
parts = clazz.split('.')
|
|
||||||
module = ".".join(parts[:-1])
|
|
||||||
m = __import__( module )
|
|
||||||
for comp in parts[1:]:
|
|
||||||
m = getattr(m, comp)
|
|
||||||
return m
|
|
||||||
|
|
||||||
def get_class_from_config(key, config):
|
|
||||||
"""Get a reference to a class by its configuration key name."""
|
|
||||||
try:
|
|
||||||
return get_class_by_name(config[key])
|
|
||||||
except Exception, e:
|
|
||||||
raise AttributeError(
|
|
||||||
"Could not get class '%s' for Auth setting '%s' >> %s" %
|
|
||||||
(config[key], key, e))
|
|
||||||
|
|
||||||
def get_url(endpoint_or_url):
|
|
||||||
"""Returns a URL if a valid endpoint is found. Otherwise, returns the
|
|
||||||
provided value."""
|
|
||||||
try:
|
|
||||||
return url_for(endpoint_or_url)
|
|
||||||
except:
|
|
||||||
return endpoint_or_url
|
|
||||||
|
|
||||||
def get_post_login_redirect():
|
|
||||||
"""Returns the URL to redirect to after a user logs in successfully"""
|
|
||||||
return (get_url(request.args.get('next')) or
|
|
||||||
get_url(request.form.get('next')) or
|
|
||||||
find_redirect(POST_LOGIN_KEY))
|
|
||||||
|
|
||||||
def find_redirect(key):
|
|
||||||
"""Returns the URL to redirect to after a user logs in successfully"""
|
|
||||||
result = (get_url(session.pop(key.lower(), None)) or
|
|
||||||
get_url(current_app.config[key.upper()] or None) or '/')
|
|
||||||
|
|
||||||
try:
|
|
||||||
del session[key.lower()]
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return result
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.changeable
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security recoverable module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:author: Eskil Heyn Olsen
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import current_app as app, request
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .signals import password_changed
|
||||||
|
from .utils import send_mail, encrypt_password, config_value
|
||||||
|
|
||||||
|
|
||||||
|
# Convenient references
|
||||||
|
_security = LocalProxy(lambda: app.extensions['security'])
|
||||||
|
|
||||||
|
_datastore = LocalProxy(lambda: _security.datastore)
|
||||||
|
|
||||||
|
|
||||||
|
def send_password_changed_notice(user):
|
||||||
|
"""Sends the password changed notice email for the specified user.
|
||||||
|
|
||||||
|
:param user: The user to send the notice to
|
||||||
|
"""
|
||||||
|
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE'), user.email,
|
||||||
|
'change_notice', user=user)
|
||||||
|
|
||||||
|
|
||||||
|
def change_user_password(user, password):
|
||||||
|
"""Change the specified user's password
|
||||||
|
|
||||||
|
:param user: The user to change_password
|
||||||
|
:param password: The unencrypted new password
|
||||||
|
"""
|
||||||
|
user.password = encrypt_password(password)
|
||||||
|
_datastore.put(user)
|
||||||
|
send_password_changed_notice(user)
|
||||||
|
password_changed.send(user, app=app._get_current_object())
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.confirmable
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security confirmable module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import current_app as app, request
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .utils import send_mail, md5, url_for_security, get_token_status,\
|
||||||
|
config_value
|
||||||
|
from .signals import user_confirmed, confirm_instructions_sent
|
||||||
|
|
||||||
|
|
||||||
|
# Convenient references
|
||||||
|
_security = LocalProxy(lambda: app.extensions['security'])
|
||||||
|
|
||||||
|
_datastore = LocalProxy(lambda: _security.datastore)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_confirmation_link(user):
|
||||||
|
token = generate_confirmation_token(user)
|
||||||
|
return url_for_security('confirm_email', token=token, _external=True), token
|
||||||
|
|
||||||
|
|
||||||
|
def send_confirmation_instructions(user):
|
||||||
|
"""Sends the confirmation instructions email for the specified user.
|
||||||
|
|
||||||
|
:param user: The user to send the instructions to
|
||||||
|
:param token: The confirmation token
|
||||||
|
"""
|
||||||
|
|
||||||
|
confirmation_link, token = generate_confirmation_link(user)
|
||||||
|
|
||||||
|
send_mail(config_value('EMAIL_SUBJECT_CONFIRM'), user.email,
|
||||||
|
'confirmation_instructions', user=user,
|
||||||
|
confirmation_link=confirmation_link)
|
||||||
|
|
||||||
|
confirm_instructions_sent.send(app._get_current_object(), user=user)
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def generate_confirmation_token(user):
|
||||||
|
"""Generates a unique confirmation token for the specified user.
|
||||||
|
|
||||||
|
:param user: The user to work with
|
||||||
|
"""
|
||||||
|
data = [str(user.id), md5(user.email)]
|
||||||
|
return _security.confirm_serializer.dumps(data)
|
||||||
|
|
||||||
|
|
||||||
|
def requires_confirmation(user):
|
||||||
|
"""Returns `True` if the user requires confirmation."""
|
||||||
|
return _security.confirmable and user.confirmed_at == None
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_email_token_status(token):
|
||||||
|
"""Returns the expired status, invalid status, and user of a confirmation
|
||||||
|
token. For example::
|
||||||
|
|
||||||
|
expired, invalid, user = confirm_email_token_status('...')
|
||||||
|
|
||||||
|
:param token: The confirmation token
|
||||||
|
"""
|
||||||
|
return get_token_status(token, 'confirm', 'CONFIRM_EMAIL')
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_user(user):
|
||||||
|
"""Confirms the specified user
|
||||||
|
|
||||||
|
:param user: The user to confirm
|
||||||
|
"""
|
||||||
|
user.confirmed_at = datetime.utcnow()
|
||||||
|
_datastore.put(user)
|
||||||
|
user_confirmed.send(app._get_current_object(), user=user)
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.core
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security core module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
from flask.ext.login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
|
||||||
|
LoginManager, current_user
|
||||||
|
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \
|
||||||
|
identity_loaded
|
||||||
|
from itsdangerous import URLSafeTimedSerializer
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from werkzeug.datastructures import ImmutableList
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .utils import config_value as cv, get_config, md5, url_for_security
|
||||||
|
from .views import create_blueprint
|
||||||
|
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
|
||||||
|
ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \
|
||||||
|
SendConfirmationForm, PasswordlessLoginForm
|
||||||
|
|
||||||
|
# Convenient references
|
||||||
|
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||||
|
|
||||||
|
|
||||||
|
#: Default Flask-Security configuration
|
||||||
|
_default_config = {
|
||||||
|
'BLUEPRINT_NAME': 'security',
|
||||||
|
'URL_PREFIX': None,
|
||||||
|
'SUBDOMAIN': None,
|
||||||
|
'FLASH_MESSAGES': True,
|
||||||
|
'PASSWORD_HASH': 'plaintext',
|
||||||
|
'PASSWORD_SALT': None,
|
||||||
|
'LOGIN_URL': '/login',
|
||||||
|
'LOGOUT_URL': '/logout',
|
||||||
|
'REGISTER_URL': '/register',
|
||||||
|
'RESET_URL': '/reset',
|
||||||
|
'CHANGE_URL': '/change',
|
||||||
|
'CONFIRM_URL': '/confirm',
|
||||||
|
'POST_LOGIN_VIEW': '/',
|
||||||
|
'POST_LOGOUT_VIEW': '/',
|
||||||
|
'CONFIRM_ERROR_VIEW': None,
|
||||||
|
'POST_REGISTER_VIEW': None,
|
||||||
|
'POST_CONFIRM_VIEW': None,
|
||||||
|
'POST_RESET_VIEW': None,
|
||||||
|
'POST_CHANGE_VIEW': None,
|
||||||
|
'UNAUTHORIZED_VIEW': None,
|
||||||
|
'FORGOT_PASSWORD_TEMPLATE': 'security/forgot_password.html',
|
||||||
|
'LOGIN_USER_TEMPLATE': 'security/login_user.html',
|
||||||
|
'REGISTER_USER_TEMPLATE': 'security/register_user.html',
|
||||||
|
'RESET_PASSWORD_TEMPLATE': 'security/reset_password.html',
|
||||||
|
'SEND_CONFIRMATION_TEMPLATE': 'security/send_confirmation.html',
|
||||||
|
'SEND_LOGIN_TEMPLATE': 'security/send_login.html',
|
||||||
|
'CONFIRMABLE': False,
|
||||||
|
'REGISTERABLE': False,
|
||||||
|
'RECOVERABLE': False,
|
||||||
|
'TRACKABLE': False,
|
||||||
|
'PASSWORDLESS': False,
|
||||||
|
'CHANGEABLE': False,
|
||||||
|
'SEND_REGISTER_EMAIL': True,
|
||||||
|
'LOGIN_WITHIN': '1 days',
|
||||||
|
'CONFIRM_EMAIL_WITHIN': '5 days',
|
||||||
|
'RESET_PASSWORD_WITHIN': '5 days',
|
||||||
|
'LOGIN_WITHOUT_CONFIRMATION': False,
|
||||||
|
'EMAIL_SENDER': 'no-reply@localhost',
|
||||||
|
'TOKEN_AUTHENTICATION_KEY': 'auth_token',
|
||||||
|
'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token',
|
||||||
|
'CONFIRM_SALT': 'confirm-salt',
|
||||||
|
'RESET_SALT': 'reset-salt',
|
||||||
|
'LOGIN_SALT': 'login-salt',
|
||||||
|
'CHANGE_SALT': 'change-salt',
|
||||||
|
'REMEMBER_SALT': 'remember-salt',
|
||||||
|
'DEFAULT_REMEMBER_ME': False,
|
||||||
|
'DEFAULT_HTTP_AUTH_REALM': 'Login Required',
|
||||||
|
'EMAIL_SUBJECT_REGISTER': 'Welcome',
|
||||||
|
'EMAIL_SUBJECT_CONFIRM': 'Please confirm your email',
|
||||||
|
'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions',
|
||||||
|
'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset',
|
||||||
|
'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed',
|
||||||
|
'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions'
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Default Flask-Security messages
|
||||||
|
_default_messages = {
|
||||||
|
'UNAUTHORIZED': ('You do not have permission to view this resource.', 'error'),
|
||||||
|
'CONFIRM_REGISTRATION': ('Thank you. Confirmation instructions have been sent to %(email)s.', 'success'),
|
||||||
|
'EMAIL_CONFIRMED': ('Thank you. Your email has been confirmed.', 'success'),
|
||||||
|
'ALREADY_CONFIRMED': ('Your email has already been confirmed.', 'info'),
|
||||||
|
'INVALID_CONFIRMATION_TOKEN': ('Invalid confirmation token.', 'error'),
|
||||||
|
'EMAIL_ALREADY_ASSOCIATED': ('%(email)s is already associated with an account.', 'error'),
|
||||||
|
'PASSWORD_MISMATCH': ('Password does not match', 'error'),
|
||||||
|
'RETYPE_PASSWORD_MISMATCH': ('Passwords do not match', 'error'),
|
||||||
|
'INVALID_REDIRECT': ('Redirections outside the domain are forbidden', 'error'),
|
||||||
|
'PASSWORD_RESET_REQUEST': ('Instructions to reset your password have been sent to %(email)s.', 'info'),
|
||||||
|
'PASSWORD_RESET_EXPIRED': ('You did not reset your password within %(within)s. New instructions have been sent to %(email)s.', 'error'),
|
||||||
|
'INVALID_RESET_PASSWORD_TOKEN': ('Invalid reset password token.', 'error'),
|
||||||
|
'CONFIRMATION_REQUIRED': ('Email requires confirmation.', 'error'),
|
||||||
|
'CONFIRMATION_REQUEST': ('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_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_CHANGE': ('You successfully changed your password.', 'success'),
|
||||||
|
'LOGIN': ('Please log in to access this page.', 'info'),
|
||||||
|
'REFRESH': ('Please reauthenticate to access this page.', 'info'),
|
||||||
|
}
|
||||||
|
|
||||||
|
_allowed_password_hash_schemes = [
|
||||||
|
'bcrypt',
|
||||||
|
'des_crypt',
|
||||||
|
'pbkdf2_sha256',
|
||||||
|
'pbkdf2_sha512',
|
||||||
|
'sha256_crypt',
|
||||||
|
'sha512_crypt',
|
||||||
|
# And always last one...
|
||||||
|
'plaintext'
|
||||||
|
]
|
||||||
|
|
||||||
|
_default_forms = {
|
||||||
|
'login_form': LoginForm,
|
||||||
|
'confirm_register_form': ConfirmRegisterForm,
|
||||||
|
'register_form': RegisterForm,
|
||||||
|
'forgot_password_form': ForgotPasswordForm,
|
||||||
|
'reset_password_form': ResetPasswordForm,
|
||||||
|
'change_password_form': ChangePasswordForm,
|
||||||
|
'send_confirmation_form': SendConfirmationForm,
|
||||||
|
'passwordless_login_form': PasswordlessLoginForm,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _user_loader(user_id):
|
||||||
|
return _security.datastore.find_user(id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _token_loader(token):
|
||||||
|
try:
|
||||||
|
data = _security.remember_token_serializer.loads(token)
|
||||||
|
user = _security.datastore.find_user(id=data[0])
|
||||||
|
if user and md5(user.password) == data[1]:
|
||||||
|
return user
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return AnonymousUser()
|
||||||
|
|
||||||
|
|
||||||
|
def _identity_loader():
|
||||||
|
if not isinstance(current_user._get_current_object(), AnonymousUser):
|
||||||
|
identity = Identity(current_user.id)
|
||||||
|
return identity
|
||||||
|
|
||||||
|
|
||||||
|
def _on_identity_loaded(sender, identity):
|
||||||
|
if hasattr(current_user, 'id'):
|
||||||
|
identity.provides.add(UserNeed(current_user.id))
|
||||||
|
|
||||||
|
for role in current_user.roles:
|
||||||
|
identity.provides.add(RoleNeed(role.name))
|
||||||
|
|
||||||
|
identity.user = current_user
|
||||||
|
|
||||||
|
|
||||||
|
def _get_login_manager(app):
|
||||||
|
lm = LoginManager()
|
||||||
|
lm.anonymous_user = AnonymousUser
|
||||||
|
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
|
||||||
|
lm.user_loader(_user_loader)
|
||||||
|
lm.token_loader(_token_loader)
|
||||||
|
|
||||||
|
if cv('FLASH_MESSAGES', app=app):
|
||||||
|
lm.login_message, lm.login_message_category = cv('MSG_LOGIN', app=app)
|
||||||
|
lm.needs_refresh_message, lm.needs_refresh_message_category = cv('MSG_REFRESH', app=app)
|
||||||
|
else:
|
||||||
|
lm.login_message = None
|
||||||
|
lm.needs_refresh_message = None
|
||||||
|
|
||||||
|
lm.init_app(app)
|
||||||
|
return lm
|
||||||
|
|
||||||
|
|
||||||
|
def _get_principal(app):
|
||||||
|
p = Principal(app, use_sessions=False)
|
||||||
|
p.identity_loader(_identity_loader)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def _get_pwd_context(app):
|
||||||
|
pw_hash = cv('PASSWORD_HASH', app=app)
|
||||||
|
if pw_hash not in _allowed_password_hash_schemes:
|
||||||
|
allowed = ', '.join(_allowed_password_hash_schemes[:-1]) + ' and ' + _allowed_password_hash_schemes[-1]
|
||||||
|
raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed))
|
||||||
|
return CryptContext(schemes=_allowed_password_hash_schemes, default=pw_hash)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_serializer(app, name):
|
||||||
|
secret_key = app.config.get('SECRET_KEY')
|
||||||
|
salt = app.config.get('SECURITY_%s_SALT' % name.upper())
|
||||||
|
return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_state(app, datastore, **kwargs):
|
||||||
|
for key, value in get_config(app).items():
|
||||||
|
kwargs[key.lower()] = value
|
||||||
|
|
||||||
|
kwargs.update(dict(
|
||||||
|
app=app,
|
||||||
|
datastore=datastore,
|
||||||
|
login_manager=_get_login_manager(app),
|
||||||
|
principal=_get_principal(app),
|
||||||
|
pwd_context=_get_pwd_context(app),
|
||||||
|
remember_token_serializer=_get_serializer(app, 'remember'),
|
||||||
|
login_serializer=_get_serializer(app, 'login'),
|
||||||
|
reset_serializer=_get_serializer(app, 'reset'),
|
||||||
|
confirm_serializer=_get_serializer(app, 'confirm'),
|
||||||
|
_context_processors={},
|
||||||
|
_send_mail_task=None
|
||||||
|
))
|
||||||
|
|
||||||
|
for key, value in _default_forms.items():
|
||||||
|
if key not in kwargs or not kwargs[key]:
|
||||||
|
kwargs[key] = value
|
||||||
|
|
||||||
|
return _SecurityState(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _context_processor():
|
||||||
|
return dict(url_for_security=url_for_security, security=_security)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleMixin(object):
|
||||||
|
"""Mixin for `Role` model definitions"""
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (self.name == other or \
|
||||||
|
self.name == getattr(other, 'name', None))
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
|
||||||
|
class UserMixin(BaseUserMixin):
|
||||||
|
"""Mixin for `User` model definitions"""
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
"""Returns `True` if the user is active."""
|
||||||
|
return self.active
|
||||||
|
|
||||||
|
def get_auth_token(self):
|
||||||
|
"""Returns the user's authentication token."""
|
||||||
|
data = [str(self.id), md5(self.password)]
|
||||||
|
return _security.remember_token_serializer.dumps(data)
|
||||||
|
|
||||||
|
def has_role(self, role):
|
||||||
|
"""Returns `True` if the user identifies with the specified role.
|
||||||
|
|
||||||
|
:param role: A role name or `Role` instance"""
|
||||||
|
if isinstance(role, basestring):
|
||||||
|
return role in (role.name for role in self.roles)
|
||||||
|
else:
|
||||||
|
return role in self.roles
|
||||||
|
|
||||||
|
|
||||||
|
class AnonymousUser(AnonymousUserMixin):
|
||||||
|
"""AnonymousUser definition"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.roles = ImmutableList()
|
||||||
|
|
||||||
|
def has_role(self, *args):
|
||||||
|
"""Returns `False`"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class _SecurityState(object):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
setattr(self, key.lower(), value)
|
||||||
|
|
||||||
|
def _add_ctx_processor(self, endpoint, fn):
|
||||||
|
group = self._context_processors.setdefault(endpoint, [])
|
||||||
|
fn not in group and group.append(fn)
|
||||||
|
|
||||||
|
def _run_ctx_processor(self, endpoint):
|
||||||
|
rv, fns = {}, []
|
||||||
|
for g in [None, endpoint]:
|
||||||
|
for fn in self._context_processors.setdefault(g, []):
|
||||||
|
rv.update(fn())
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def context_processor(self, fn):
|
||||||
|
self._add_ctx_processor(None, fn)
|
||||||
|
|
||||||
|
def forgot_password_context_processor(self, fn):
|
||||||
|
self._add_ctx_processor('forgot_password', fn)
|
||||||
|
|
||||||
|
def login_context_processor(self, fn):
|
||||||
|
self._add_ctx_processor('login', fn)
|
||||||
|
|
||||||
|
def register_context_processor(self, fn):
|
||||||
|
self._add_ctx_processor('register', fn)
|
||||||
|
|
||||||
|
def reset_password_context_processor(self, fn):
|
||||||
|
self._add_ctx_processor('reset_password', fn)
|
||||||
|
|
||||||
|
def change_password_context_processor(self, fn):
|
||||||
|
self._add_ctx_processor('change_password', fn)
|
||||||
|
|
||||||
|
def send_confirmation_context_processor(self, fn):
|
||||||
|
self._add_ctx_processor('send_confirmation', fn)
|
||||||
|
|
||||||
|
def send_login_context_processor(self, fn):
|
||||||
|
self._add_ctx_processor('send_login', fn)
|
||||||
|
|
||||||
|
def mail_context_processor(self, fn):
|
||||||
|
self._add_ctx_processor('mail', fn)
|
||||||
|
|
||||||
|
def send_mail_task(self, fn):
|
||||||
|
self._send_mail_task = fn
|
||||||
|
|
||||||
|
|
||||||
|
class Security(object):
|
||||||
|
"""The :class:`Security` class initializes the Flask-Security extension.
|
||||||
|
|
||||||
|
:param app: The application.
|
||||||
|
:param datastore: An instance of a user datastore.
|
||||||
|
"""
|
||||||
|
def __init__(self, app=None, datastore=None, **kwargs):
|
||||||
|
self.app = app
|
||||||
|
self.datastore = datastore
|
||||||
|
|
||||||
|
if app is not None and datastore is not None:
|
||||||
|
self._state = self.init_app(app, datastore, **kwargs)
|
||||||
|
|
||||||
|
def init_app(self, app, datastore=None, register_blueprint=True,
|
||||||
|
login_form=None, confirm_register_form=None,
|
||||||
|
register_form=None, forgot_password_form=None,
|
||||||
|
reset_password_form=None, change_password_form=None,
|
||||||
|
send_confirmation_form=None, passwordless_login_form=None):
|
||||||
|
"""Initializes the Flask-Security extension for the specified
|
||||||
|
application and datastore implentation.
|
||||||
|
|
||||||
|
:param app: The application.
|
||||||
|
:param datastore: An instance of a user datastore.
|
||||||
|
:param register_blueprint: to register the Security blueprint or not.
|
||||||
|
"""
|
||||||
|
datastore = datastore or self.datastore
|
||||||
|
|
||||||
|
for key, value in _default_config.items():
|
||||||
|
app.config.setdefault('SECURITY_' + key, value)
|
||||||
|
|
||||||
|
for key, value in _default_messages.items():
|
||||||
|
app.config.setdefault('SECURITY_MSG_' + key, value)
|
||||||
|
|
||||||
|
identity_loaded.connect_via(app)(_on_identity_loaded)
|
||||||
|
|
||||||
|
state = _get_state(app, datastore,
|
||||||
|
login_form=login_form,
|
||||||
|
confirm_register_form=confirm_register_form,
|
||||||
|
register_form=register_form,
|
||||||
|
forgot_password_form=forgot_password_form,
|
||||||
|
reset_password_form=reset_password_form,
|
||||||
|
change_password_form=change_password_form,
|
||||||
|
send_confirmation_form=send_confirmation_form,
|
||||||
|
passwordless_login_form=passwordless_login_form)
|
||||||
|
|
||||||
|
if register_blueprint:
|
||||||
|
app.register_blueprint(create_blueprint(state, __name__))
|
||||||
|
app.context_processor(_context_processor)
|
||||||
|
|
||||||
|
app.extensions['security'] = state
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._state, name, None)
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.datastore
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module contains an user datastore classes.
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Datastore(object):
|
||||||
|
def __init__(self, db):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def put(self, model):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def delete(self, model):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class SQLAlchemyDatastore(Datastore):
|
||||||
|
def commit(self):
|
||||||
|
self.db.session.commit()
|
||||||
|
|
||||||
|
def put(self, model):
|
||||||
|
self.db.session.add(model)
|
||||||
|
return model
|
||||||
|
|
||||||
|
def delete(self, model):
|
||||||
|
self.db.session.delete(model)
|
||||||
|
|
||||||
|
|
||||||
|
class MongoEngineDatastore(Datastore):
|
||||||
|
def put(self, model):
|
||||||
|
model.save()
|
||||||
|
return model
|
||||||
|
|
||||||
|
def delete(self, model):
|
||||||
|
model.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class PeeweeDatastore(Datastore):
|
||||||
|
def put(self, model):
|
||||||
|
model.save()
|
||||||
|
return model
|
||||||
|
|
||||||
|
def delete(self, model):
|
||||||
|
model.delete_instance()
|
||||||
|
|
||||||
|
|
||||||
|
class UserDatastore(object):
|
||||||
|
"""Abstracted user datastore.
|
||||||
|
|
||||||
|
:param user_model: A user model class definition
|
||||||
|
:param role_model: A role model class definition
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, user_model, role_model):
|
||||||
|
self.user_model = user_model
|
||||||
|
self.role_model = role_model
|
||||||
|
|
||||||
|
def _prepare_role_modify_args(self, user, role):
|
||||||
|
if isinstance(user, basestring):
|
||||||
|
user = self.find_user(email=user)
|
||||||
|
if isinstance(role, basestring):
|
||||||
|
role = self.find_role(role)
|
||||||
|
return user, role
|
||||||
|
|
||||||
|
def _prepare_create_user_args(self, **kwargs):
|
||||||
|
kwargs.setdefault('active', True)
|
||||||
|
roles = kwargs.get('roles', [])
|
||||||
|
for i, role in enumerate(roles):
|
||||||
|
rn = role.name if isinstance(role, self.role_model) else role
|
||||||
|
# see if the role exists
|
||||||
|
roles[i] = self.find_role(rn)
|
||||||
|
kwargs['roles'] = roles
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def find_user(self, *args, **kwargs):
|
||||||
|
"""Returns a user matching the provided parameters."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def find_role(self, *args, **kwargs):
|
||||||
|
"""Returns a role matching the provided name."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def add_role_to_user(self, user, role):
|
||||||
|
"""Adds a role tp a user
|
||||||
|
|
||||||
|
:param user: The user to manipulate
|
||||||
|
:param role: The role to add to the user
|
||||||
|
"""
|
||||||
|
user, role = self._prepare_role_modify_args(user, role)
|
||||||
|
if role not in user.roles:
|
||||||
|
user.roles.append(role)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_role_from_user(self, user, role):
|
||||||
|
"""Removes a role from a user
|
||||||
|
|
||||||
|
:param user: The user to manipulate
|
||||||
|
:param role: The role to remove from the user
|
||||||
|
"""
|
||||||
|
rv = False
|
||||||
|
user, role = self._prepare_role_modify_args(user, role)
|
||||||
|
if role in user.roles:
|
||||||
|
rv = True
|
||||||
|
user.roles.remove(role)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def toggle_active(self, user):
|
||||||
|
"""Toggles a user's active status. Always returns True."""
|
||||||
|
user.active = not user.active
|
||||||
|
return True
|
||||||
|
|
||||||
|
def deactivate_user(self, user):
|
||||||
|
"""Deactivates a specified user. Returns `True` if a change was made.
|
||||||
|
|
||||||
|
:param user: The user to deactivate
|
||||||
|
"""
|
||||||
|
if user.active:
|
||||||
|
user.active = False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def activate_user(self, user):
|
||||||
|
"""Activates a specified user. Returns `True` if a change was made.
|
||||||
|
|
||||||
|
:param user: The user to activate
|
||||||
|
"""
|
||||||
|
if not user.active:
|
||||||
|
user.active = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_role(self, **kwargs):
|
||||||
|
"""Creates and returns a new role from the given parameters."""
|
||||||
|
|
||||||
|
role = self.role_model(**kwargs)
|
||||||
|
return self.put(role)
|
||||||
|
|
||||||
|
def find_or_create_role(self, name, **kwargs):
|
||||||
|
"""Returns a role matching the given name or creates it with any
|
||||||
|
additionally provided parameters
|
||||||
|
"""
|
||||||
|
kwargs["name"] = name
|
||||||
|
return self.find_role(name) or self.create_role(**kwargs)
|
||||||
|
|
||||||
|
def create_user(self, **kwargs):
|
||||||
|
"""Creates and returns a new user from the given parameters."""
|
||||||
|
|
||||||
|
user = self.user_model(**self._prepare_create_user_args(**kwargs))
|
||||||
|
return self.put(user)
|
||||||
|
|
||||||
|
def delete_user(self, user):
|
||||||
|
"""Delete the specified user
|
||||||
|
|
||||||
|
:param user: The user to delete
|
||||||
|
"""
|
||||||
|
self.delete(user)
|
||||||
|
|
||||||
|
|
||||||
|
class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
|
||||||
|
"""A SQLAlchemy datastore implementation for Flask-Security that assumes the
|
||||||
|
use of the Flask-SQLAlchemy extension.
|
||||||
|
"""
|
||||||
|
def __init__(self, db, user_model, role_model):
|
||||||
|
SQLAlchemyDatastore.__init__(self, db)
|
||||||
|
UserDatastore.__init__(self, user_model, role_model)
|
||||||
|
|
||||||
|
def find_user(self, **kwargs):
|
||||||
|
return self.user_model.query.filter_by(**kwargs).first()
|
||||||
|
|
||||||
|
def find_role(self, role):
|
||||||
|
return self.role_model.query.filter_by(name=role).first()
|
||||||
|
|
||||||
|
|
||||||
|
class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
|
||||||
|
"""A MongoEngine datastore implementation for Flask-Security that assumes
|
||||||
|
the use of the Flask-MongoEngine extension.
|
||||||
|
"""
|
||||||
|
def __init__(self, db, user_model, role_model):
|
||||||
|
MongoEngineDatastore.__init__(self, db)
|
||||||
|
UserDatastore.__init__(self, user_model, role_model)
|
||||||
|
|
||||||
|
def find_user(self, **kwargs):
|
||||||
|
try:
|
||||||
|
from mongoengine.queryset import Q, QCombination
|
||||||
|
except ImportError:
|
||||||
|
from mongoengine.queryset.visitor import Q, QCombination
|
||||||
|
|
||||||
|
queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items())
|
||||||
|
query = QCombination(QCombination.AND, queries)
|
||||||
|
return self.user_model.objects(query).first()
|
||||||
|
|
||||||
|
def find_role(self, role):
|
||||||
|
return self.role_model.objects(name=role).first()
|
||||||
|
|
||||||
|
|
||||||
|
class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
||||||
|
"""A PeeweeD datastore implementation for Flask-Security that assumes
|
||||||
|
the use of the Flask-Peewee extension.
|
||||||
|
|
||||||
|
:param user_model: A user model class definition
|
||||||
|
:param role_model: A role model class definition
|
||||||
|
:param role_link: A model implementing the many-to-many user-role relation
|
||||||
|
"""
|
||||||
|
def __init__(self, db, user_model, role_model, role_link):
|
||||||
|
PeeweeDatastore.__init__(self, db)
|
||||||
|
UserDatastore.__init__(self, user_model, role_model)
|
||||||
|
self.UserRole = role_link
|
||||||
|
|
||||||
|
def find_user(self, **kwargs):
|
||||||
|
try:
|
||||||
|
return self.user_model.filter(**kwargs).get()
|
||||||
|
except self.user_model.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_role(self, role):
|
||||||
|
return self.role_model.filter(name=role).get()
|
||||||
|
|
||||||
|
def create_user(self, **kwargs):
|
||||||
|
"""Creates and returns a new user from the given parameters."""
|
||||||
|
roles = kwargs.pop('roles', [])
|
||||||
|
user = self.user_model(**self._prepare_create_user_args(**kwargs))
|
||||||
|
user = self.put(user)
|
||||||
|
for role in roles:
|
||||||
|
self.add_role_to_user(user, role)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def add_role_to_user(self, user, role):
|
||||||
|
"""Adds a role tp a user
|
||||||
|
|
||||||
|
:param user: The user to manipulate
|
||||||
|
:param role: The role to add to the user
|
||||||
|
"""
|
||||||
|
user, role = self._prepare_role_modify_args(user, role)
|
||||||
|
result = self.UserRole.select() \
|
||||||
|
.where(self.UserRole.user == user.id, self.UserRole.role == role.id)
|
||||||
|
if result.count():
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.UserRole.create(user=user.id, role=role.id)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def remove_role_from_user(self, user, role):
|
||||||
|
"""Removes a role from a user
|
||||||
|
|
||||||
|
:param user: The user to manipulate
|
||||||
|
:param role: The role to remove from the user
|
||||||
|
"""
|
||||||
|
user, role = self._prepare_role_modify_args(user, role)
|
||||||
|
result = self.UserRole.select() \
|
||||||
|
.where(self.UserRole.user == user, self.UserRole.role == role)
|
||||||
|
if result.count():
|
||||||
|
self.UserRole.delete().where(
|
||||||
|
self.UserRole.user == user, self.UserRole.role == role)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.ext.security.datastore
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains an abstracted user datastore.
|
|
||||||
|
|
||||||
:copyright: (c) 2012 by Matt Wright.
|
|
||||||
:license: MIT, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from flask.ext import security
|
|
||||||
from flask.ext.security import UserCreationError, RoleCreationError, pwd_context
|
|
||||||
|
|
||||||
class UserDatastore(object):
|
|
||||||
"""Abstracted user datastore. Always extend this class and implement the
|
|
||||||
:attr:`get_models`, :attr:`_save_model`, :attr:`_do_with_id`,
|
|
||||||
:attr:`_do_find_user`, and :attr:`_do_find_role` methods.
|
|
||||||
|
|
||||||
:param db: An instance of a configured databse manager from a Flask
|
|
||||||
extension such as Flask-SQLAlchemy or Flask-MongoEngine"""
|
|
||||||
|
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
|
|
||||||
def get_models(self):
|
|
||||||
"""Returns configured `User` and `Role` models for the datastore
|
|
||||||
implementation"""
|
|
||||||
raise NotImplementedError(
|
|
||||||
"User datastore does not implement get_models method")
|
|
||||||
|
|
||||||
def _save_model(self, model, **kwargs):
|
|
||||||
raise NotImplementedError(
|
|
||||||
"User datastore does not implement _save_model method")
|
|
||||||
|
|
||||||
def _do_with_id(self, id):
|
|
||||||
raise NotImplementedError(
|
|
||||||
"User datastore does not implement _do_with_id method")
|
|
||||||
|
|
||||||
def _do_find_user(self):
|
|
||||||
raise NotImplementedError(
|
|
||||||
"User datastore does not implement _do_find_user method")
|
|
||||||
|
|
||||||
def _do_find_role(self):
|
|
||||||
raise NotImplementedError(
|
|
||||||
"User datastore does not implement _do_find_role method")
|
|
||||||
|
|
||||||
def _do_add_role(self, user, role):
|
|
||||||
user, role = self._prepare_role_modify_args(user, role)
|
|
||||||
if role not in user.roles:
|
|
||||||
user.roles.append(role)
|
|
||||||
return user
|
|
||||||
|
|
||||||
def _do_remove_role(self, user, role):
|
|
||||||
user, role = self._prepare_role_modify_args(user, role)
|
|
||||||
if role in user.roles:
|
|
||||||
user.roles.remove(role)
|
|
||||||
return user
|
|
||||||
|
|
||||||
def _do_toggle_active(self, user, active=None):
|
|
||||||
user = self.find_user(user)
|
|
||||||
if active is None:
|
|
||||||
user.active = not user.active
|
|
||||||
elif active != user.active:
|
|
||||||
user.active = active
|
|
||||||
return user
|
|
||||||
|
|
||||||
def _do_deactive_user(self, user):
|
|
||||||
return self._do_toggle_active(user, False)
|
|
||||||
|
|
||||||
def _do_active_user(self, user):
|
|
||||||
return self._do_toggle_active(user, True)
|
|
||||||
|
|
||||||
def _prepare_role_modify_args(self, user, role):
|
|
||||||
if isinstance(user, security.User):
|
|
||||||
user = user.username or user.email
|
|
||||||
|
|
||||||
if isinstance(role, security.Role):
|
|
||||||
role = role.name
|
|
||||||
|
|
||||||
return self.find_user(user), self.find_role(role)
|
|
||||||
|
|
||||||
def _prepare_create_role_args(self, kwargs):
|
|
||||||
for key in ('name', 'description'):
|
|
||||||
kwargs[key] = kwargs.get(key, None)
|
|
||||||
|
|
||||||
if kwargs['name'] is None:
|
|
||||||
raise RoleCreationError("Missing name argument")
|
|
||||||
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def _prepare_create_user_args(self, kwargs):
|
|
||||||
username = kwargs.get('username', None)
|
|
||||||
email = kwargs.get('email', None)
|
|
||||||
password = kwargs.get('password', None)
|
|
||||||
|
|
||||||
if username is None and email is None:
|
|
||||||
raise UserCreationError('Missing username and/or email arguments')
|
|
||||||
|
|
||||||
if password is None:
|
|
||||||
raise UserCreationError('Missing password argument')
|
|
||||||
|
|
||||||
roles = kwargs.get('roles', [])
|
|
||||||
|
|
||||||
for i, role in enumerate(roles):
|
|
||||||
rn = role.name if isinstance(role, security.Role) else role
|
|
||||||
# see if the role exists
|
|
||||||
roles[i] = self.find_role(rn)
|
|
||||||
|
|
||||||
kwargs['roles'] = roles
|
|
||||||
|
|
||||||
now = datetime.utcnow()
|
|
||||||
kwargs['created_at'], kwargs['modified_at'] = now, now
|
|
||||||
|
|
||||||
pw = kwargs['password']
|
|
||||||
if not pwd_context.identify(pw):
|
|
||||||
kwargs['password'] = pwd_context.encrypt(pw)
|
|
||||||
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def with_id(self, id):
|
|
||||||
"""Returns a user with the specified ID.
|
|
||||||
|
|
||||||
:param id: User ID"""
|
|
||||||
user = self._do_with_id(id)
|
|
||||||
if user: return user
|
|
||||||
raise security.UserIdNotFoundError()
|
|
||||||
|
|
||||||
def find_user(self, user):
|
|
||||||
"""Returns a user based on the specified identifier.
|
|
||||||
|
|
||||||
:param user: User identifier, usually a username or email address
|
|
||||||
"""
|
|
||||||
user = self._do_find_user(user)
|
|
||||||
if user: return user
|
|
||||||
raise security.UserNotFoundError()
|
|
||||||
|
|
||||||
def find_role(self, role):
|
|
||||||
"""Returns a role based on its name.
|
|
||||||
|
|
||||||
:param role: Role name
|
|
||||||
"""
|
|
||||||
role = self._do_find_role(role)
|
|
||||||
if role: return role
|
|
||||||
raise security.RoleNotFoundError()
|
|
||||||
|
|
||||||
def create_role(self, **kwargs):
|
|
||||||
"""Creates and returns a new role.
|
|
||||||
|
|
||||||
:param name: Role name
|
|
||||||
:param description: Role description
|
|
||||||
"""
|
|
||||||
role = security.Role(**self._prepare_create_role_args(kwargs))
|
|
||||||
return self._save_model(role)
|
|
||||||
|
|
||||||
def create_user(self, **kwargs):
|
|
||||||
"""Creates and returns a new user.
|
|
||||||
|
|
||||||
:param username: Username
|
|
||||||
:param email: Email address
|
|
||||||
:param password: Unencrypted password
|
|
||||||
:param active: The optional active state
|
|
||||||
"""
|
|
||||||
user = security.User(**self._prepare_create_user_args(kwargs))
|
|
||||||
return self._save_model(user)
|
|
||||||
|
|
||||||
def add_role_to_user(self, user, role):
|
|
||||||
"""Adds a role to a user if the user does not have it already. Returns
|
|
||||||
the modified user.
|
|
||||||
|
|
||||||
:param user: A User instance or a user identifier
|
|
||||||
:param role: A Role instance or a role name
|
|
||||||
"""
|
|
||||||
return self._save_model(self._do_add_role(user, role))
|
|
||||||
|
|
||||||
def remove_role_from_user(self, user, role, commit=True):
|
|
||||||
"""Removes a role from a user if the user has the role. Returns the
|
|
||||||
modified user.
|
|
||||||
|
|
||||||
:param user: A User instance or a user identifier
|
|
||||||
:param role: A Role instance or a role name
|
|
||||||
"""
|
|
||||||
return self._save_model(self._do_remove_role(user, role))
|
|
||||||
|
|
||||||
def deactivate_user(self, user):
|
|
||||||
"""Deactivates a user and returns the modified user.
|
|
||||||
|
|
||||||
:param user: A User instance or a user identifier
|
|
||||||
"""
|
|
||||||
return self._save_model(self._do_deactive_user(user))
|
|
||||||
|
|
||||||
def activate_user(self, user, commit=True):
|
|
||||||
"""Activates a user and returns the modified user.
|
|
||||||
|
|
||||||
:param user: A User instance or a user identifier
|
|
||||||
"""
|
|
||||||
return self._save_model(self._do_active_user(user))
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.ext.security.datastore.mongoengine
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains a Flask-Security MongoEngine datastore implementation
|
|
||||||
|
|
||||||
:copyright: (c) 2012 by Matt Wright.
|
|
||||||
:license: MIT, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask.ext import security
|
|
||||||
from flask.ext.security import UserMixin, RoleMixin
|
|
||||||
from flask.ext.security.datastore import UserDatastore
|
|
||||||
|
|
||||||
class MongoEngineUserDatastore(UserDatastore):
|
|
||||||
"""A MongoEngine datastore implementation for Flask-Security.
|
|
||||||
Example usage::
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask.ext.mongoengine import MongoEngine
|
|
||||||
from flask.ext.security import Security
|
|
||||||
from flask.ext.security.datastore.mongoengine import MongoEngineUserDatastore
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.config['SECRET_KEY'] = 'secret'
|
|
||||||
app.config['MONGODB_DB'] = 'flask_security_example'
|
|
||||||
app.config['MONGODB_HOST'] = 'localhost'
|
|
||||||
app.config['MONGODB_PORT'] = 27017
|
|
||||||
|
|
||||||
db = MongoEngine(app)
|
|
||||||
Security(app, MongoEngineUserDatastore(db))
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_models(self):
|
|
||||||
db = self.db
|
|
||||||
|
|
||||||
class Role(db.Document, RoleMixin):
|
|
||||||
"""MongoEngine Role model"""
|
|
||||||
|
|
||||||
name = db.StringField(required=True, unique=True, max_length=80)
|
|
||||||
description = db.StringField(max_length=255)
|
|
||||||
|
|
||||||
class User(db.Document, UserMixin):
|
|
||||||
"""MongoEngine User model"""
|
|
||||||
|
|
||||||
username = db.StringField(unique=True, max_length=255)
|
|
||||||
email = db.StringField(unique=True, max_length=255)
|
|
||||||
password = db.StringField(required=True, max_length=120)
|
|
||||||
active = db.BooleanField(default=True)
|
|
||||||
roles= db.ListField(db.ReferenceField(Role), default=[])
|
|
||||||
created_at = db.DateTimeField()
|
|
||||||
modified_at = db.DateTimeField()
|
|
||||||
|
|
||||||
return User, Role
|
|
||||||
|
|
||||||
def _save_model(self, model):
|
|
||||||
model.save()
|
|
||||||
return model
|
|
||||||
|
|
||||||
def _do_with_id(self, id):
|
|
||||||
try: return security.User.objects.get(id=id)
|
|
||||||
except: return None
|
|
||||||
|
|
||||||
def _do_find_user(self, user):
|
|
||||||
return security.User.objects(username=user).first() or \
|
|
||||||
security.User.objects(email=user).first()
|
|
||||||
|
|
||||||
def _do_find_role(self, role):
|
|
||||||
return security.Role.objects(name=role).first()
|
|
||||||
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.ext.security.datastore.sqlalchemy
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains a Flask-Security SQLAlchemy datastore implementation
|
|
||||||
|
|
||||||
:copyright: (c) 2012 by Matt Wright.
|
|
||||||
:license: MIT, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask.ext import security
|
|
||||||
from flask.ext.security import UserMixin, RoleMixin
|
|
||||||
from flask.ext.security.datastore import UserDatastore
|
|
||||||
|
|
||||||
class SQLAlchemyUserDatastore(UserDatastore):
|
|
||||||
"""A SQLAlchemy datastore implementation for Flask-Security.
|
|
||||||
Example usage::
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask.ext.security import Security
|
|
||||||
from flask.ext.security.datastore.sqlalchemy import SQLAlchemyUserDatastore
|
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.config['SECRET_KEY'] = 'secret'
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/flask_security_example.sqlite'
|
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
|
||||||
Security(app, SQLAlchemyUserDatastore(db))
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_models(self):
|
|
||||||
db = self.db
|
|
||||||
|
|
||||||
roles_users = db.Table('roles_users',
|
|
||||||
db.Column('user_id', db.Integer(), db.ForeignKey('role.id')),
|
|
||||||
db.Column('role_id', db.Integer(), db.ForeignKey('user.id')))
|
|
||||||
|
|
||||||
class Role(db.Model, RoleMixin):
|
|
||||||
"""SQLAlchemy Role model"""
|
|
||||||
|
|
||||||
id = db.Column(db.Integer(), primary_key=True)
|
|
||||||
name = db.Column(db.String(80), unique=True)
|
|
||||||
description = db.Column(db.String(255))
|
|
||||||
|
|
||||||
def __init__(self, name=None, description=None):
|
|
||||||
self.name = name
|
|
||||||
self.description = description
|
|
||||||
|
|
||||||
class User(db.Model, UserMixin):
|
|
||||||
"""SQLAlchemy User model"""
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
username = db.Column(db.String(255), unique=True)
|
|
||||||
email = db.Column(db.String(255), unique=True)
|
|
||||||
password = db.Column(db.String(120))
|
|
||||||
active = db.Column(db.Boolean())
|
|
||||||
created_at = db.Column(db.DateTime())
|
|
||||||
modified_at = db.Column(db.DateTime())
|
|
||||||
|
|
||||||
roles= db.relationship('Role', secondary=roles_users,
|
|
||||||
backref=db.backref('users', lazy='dynamic'))
|
|
||||||
|
|
||||||
def __init__(self, username=None, email=None, password=None,
|
|
||||||
active=True, roles=None,
|
|
||||||
created_at=None, modified_at=None):
|
|
||||||
self.username = username
|
|
||||||
self.email = email
|
|
||||||
self.password = password
|
|
||||||
self.active = active
|
|
||||||
self.roles = roles or []
|
|
||||||
self.created_at = created_at
|
|
||||||
self.modified_at = modified_at
|
|
||||||
|
|
||||||
return User, Role
|
|
||||||
|
|
||||||
def _save_model(self, model):
|
|
||||||
self.db.session.add(model)
|
|
||||||
self.db.session.commit()
|
|
||||||
return model
|
|
||||||
|
|
||||||
def _do_with_id(self, id):
|
|
||||||
return security.User.query.get(id)
|
|
||||||
|
|
||||||
def _do_find_user(self, user):
|
|
||||||
return security.User.query.filter_by(username=user).first() or \
|
|
||||||
security.User.query.filter_by(email=user).first()
|
|
||||||
|
|
||||||
def _do_find_role(self, role):
|
|
||||||
return security.Role.query.filter_by(name=role).first()
|
|
||||||
|
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.decorators
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security decorators module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import current_app, Response, request, redirect, _request_ctx_stack
|
||||||
|
from flask.ext.login import current_user, login_required
|
||||||
|
from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
|
# Convenient references
|
||||||
|
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||||
|
|
||||||
|
|
||||||
|
_default_unauthorized_html = """
|
||||||
|
<h1>Unauthorized</h1>
|
||||||
|
<p>The server could not verify that you are authorized to access the URL
|
||||||
|
requested. You either supplied the wrong credentials (e.g. a bad password),
|
||||||
|
or your browser doesn't understand how to supply the credentials required.</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
BasicAuth = namedtuple('BasicAuth', 'username, password')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_unauthorized_response(text=None, headers=None):
|
||||||
|
text = text or _default_unauthorized_html
|
||||||
|
headers = headers or {}
|
||||||
|
return Response(text, 401, headers)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_unauthorized_view():
|
||||||
|
cv = utils.get_url(utils.config_value('UNAUTHORIZED_VIEW'))
|
||||||
|
utils.do_flash(*utils.get_message('UNAUTHORIZED'))
|
||||||
|
return redirect(cv or request.referrer or '/')
|
||||||
|
|
||||||
|
|
||||||
|
def _check_token():
|
||||||
|
header_key = _security.token_authentication_header
|
||||||
|
args_key = _security.token_authentication_key
|
||||||
|
header_token = request.headers.get(header_key, None)
|
||||||
|
token = request.args.get(args_key, header_token)
|
||||||
|
if request.json:
|
||||||
|
token = request.json.get(args_key, token)
|
||||||
|
serializer = _security.remember_token_serializer
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = serializer.loads(token)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user = _security.datastore.find_user(id=data[0])
|
||||||
|
|
||||||
|
if utils.md5(user.password) == data[1]:
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
_request_ctx_stack.top.user = user
|
||||||
|
identity_changed.send(app, identity=Identity(user.id))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _check_http_auth():
|
||||||
|
auth = request.authorization or BasicAuth(username=None, password=None)
|
||||||
|
user = _security.datastore.find_user(email=auth.username)
|
||||||
|
|
||||||
|
if user and utils.verify_and_update_password(auth.password, user):
|
||||||
|
_security.datastore.commit()
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
_request_ctx_stack.top.user = user
|
||||||
|
identity_changed.send(app, identity=Identity(user.id))
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def http_auth_required(realm):
|
||||||
|
"""Decorator that protects endpoints using Basic HTTP authentication.
|
||||||
|
The username should be set to the user's email address.
|
||||||
|
|
||||||
|
:param realm: optional realm name"""
|
||||||
|
|
||||||
|
def decorator(fn):
|
||||||
|
@wraps(fn)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if _check_http_auth():
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
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
|
||||||
|
|
||||||
|
if callable(realm):
|
||||||
|
return decorator(realm)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def auth_token_required(fn):
|
||||||
|
"""Decorator that protects endpoints using token authentication. The token
|
||||||
|
should be added to the request by the client by using a query string
|
||||||
|
variable with a name equal to the configuration value of
|
||||||
|
`SECURITY_TOKEN_AUTHENTICATION_KEY` or in a request header named that of
|
||||||
|
the configuration value of `SECURITY_TOKEN_AUTHENTICATION_HEADER`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(fn)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if _check_token():
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
return _get_unauthorized_response()
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def auth_required(*auth_methods):
|
||||||
|
"""
|
||||||
|
Decorator that protects enpoints through multiple mechanisms
|
||||||
|
Example::
|
||||||
|
|
||||||
|
@app.route('/dashboard')
|
||||||
|
@auth_required('token', 'session')
|
||||||
|
def dashboard():
|
||||||
|
return 'Dashboard'
|
||||||
|
|
||||||
|
:param auth_methods: Specified mechanisms.
|
||||||
|
"""
|
||||||
|
login_mechanisms = {
|
||||||
|
'token': lambda: _check_token(),
|
||||||
|
'basic': lambda: _check_http_auth(),
|
||||||
|
'session': lambda: current_user.is_authenticated()
|
||||||
|
}
|
||||||
|
|
||||||
|
def wrapper(fn):
|
||||||
|
@wraps(fn)
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
mechanisms = [login_mechanisms.get(method) for method in auth_methods]
|
||||||
|
for mechanism in mechanisms:
|
||||||
|
if mechanism and mechanism():
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
return _get_unauthorized_response()
|
||||||
|
return decorated_view
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def roles_required(*roles):
|
||||||
|
"""Decorator which specifies that a user must have all the specified roles.
|
||||||
|
Example::
|
||||||
|
|
||||||
|
@app.route('/dashboard')
|
||||||
|
@roles_required('admin', 'editor')
|
||||||
|
def dashboard():
|
||||||
|
return 'Dashboard'
|
||||||
|
|
||||||
|
The current user must have both the `admin` role and `editor` role in order
|
||||||
|
to view the page.
|
||||||
|
|
||||||
|
:param args: The required roles.
|
||||||
|
"""
|
||||||
|
def wrapper(fn):
|
||||||
|
@wraps(fn)
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
perms = [Permission(RoleNeed(role)) for role in roles]
|
||||||
|
for perm in perms:
|
||||||
|
if not perm.can():
|
||||||
|
return _get_unauthorized_view()
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
return decorated_view
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def roles_accepted(*roles):
|
||||||
|
"""Decorator which specifies that a user must have at least one of the
|
||||||
|
specified roles. Example::
|
||||||
|
|
||||||
|
@app.route('/create_post')
|
||||||
|
@roles_accepted('editor', 'author')
|
||||||
|
def create_post():
|
||||||
|
return 'Create Post'
|
||||||
|
|
||||||
|
The current user must have either the `editor` role or `author` role in
|
||||||
|
order to view the page.
|
||||||
|
|
||||||
|
:param args: The possible roles.
|
||||||
|
"""
|
||||||
|
def wrapper(fn):
|
||||||
|
@wraps(fn)
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
perm = Permission(*[RoleNeed(role) for role in roles])
|
||||||
|
if perm.can():
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
return _get_unauthorized_view()
|
||||||
|
return decorated_view
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def anonymous_user_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if current_user.is_authenticated():
|
||||||
|
return redirect(utils.get_url(_security.post_login_view))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.forms
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security forms module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
import flask_wtf as wtf
|
||||||
|
|
||||||
|
from flask import request, current_app
|
||||||
|
from flask_wtf import Form as BaseForm, TextField, PasswordField, \
|
||||||
|
SubmitField, HiddenField, BooleanField, ValidationError, Field
|
||||||
|
from flask_login import current_user
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .confirmable import requires_confirmation
|
||||||
|
from .utils import verify_and_update_password, get_message
|
||||||
|
|
||||||
|
# Convenient reference
|
||||||
|
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
|
||||||
|
|
||||||
|
_default_field_labels = {
|
||||||
|
'email': 'Email Address',
|
||||||
|
'password': 'Password',
|
||||||
|
'remember_me': 'Remember Me',
|
||||||
|
'login': 'Login',
|
||||||
|
'retype_password': 'Retype Password',
|
||||||
|
'register': 'Register',
|
||||||
|
'send_confirmation': 'Resend Confirmation Instructions',
|
||||||
|
'recover_password': 'Recover Password',
|
||||||
|
'reset_password': 'Reset Password',
|
||||||
|
'retype_password': 'Retype Password',
|
||||||
|
'new_password': 'New Password',
|
||||||
|
'change_password': 'Change Password',
|
||||||
|
'send_login_link': 'Send Login Link'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ValidatorMixin(object):
|
||||||
|
def __call__(self, form, field):
|
||||||
|
if self.message and self.message.isupper():
|
||||||
|
self.message = get_message(self.message)[0]
|
||||||
|
return super(ValidatorMixin, self).__call__(form, field)
|
||||||
|
|
||||||
|
|
||||||
|
class EqualTo(ValidatorMixin, wtf.EqualTo):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Required(ValidatorMixin, wtf.Required):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Email(ValidatorMixin, wtf.Email):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Length(ValidatorMixin, wtf.Length):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
email_required = Required(message='EMAIL_NOT_PROVIDED')
|
||||||
|
email_validator = Email(message='INVALID_EMAIL_ADDRESS')
|
||||||
|
password_required = Required(message='PASSWORD_NOT_PROVIDED')
|
||||||
|
password_length = Length(min=6, max=128, message='PASSWORD_INVALID_LENGTH')
|
||||||
|
|
||||||
|
|
||||||
|
def get_form_field_label(key):
|
||||||
|
return _default_field_labels.get(key, '')
|
||||||
|
|
||||||
|
|
||||||
|
def unique_user_email(form, field):
|
||||||
|
if _datastore.find_user(email=field.data) is not None:
|
||||||
|
msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0]
|
||||||
|
raise ValidationError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def valid_user_email(form, field):
|
||||||
|
form.user = _datastore.find_user(email=field.data)
|
||||||
|
if form.user is None:
|
||||||
|
raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0])
|
||||||
|
|
||||||
|
|
||||||
|
class Form(BaseForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if current_app.testing:
|
||||||
|
self.TIME_LIMIT = None
|
||||||
|
super(Form, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailFormMixin():
|
||||||
|
email = TextField(get_form_field_label('email'),
|
||||||
|
validators=[email_required,
|
||||||
|
email_validator])
|
||||||
|
|
||||||
|
|
||||||
|
class UserEmailFormMixin():
|
||||||
|
user = None
|
||||||
|
email = TextField(get_form_field_label('email'),
|
||||||
|
validators=[email_required,
|
||||||
|
email_validator,
|
||||||
|
valid_user_email])
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueEmailFormMixin():
|
||||||
|
email = TextField(get_form_field_label('email'),
|
||||||
|
validators=[email_required,
|
||||||
|
email_validator,
|
||||||
|
unique_user_email])
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordFormMixin():
|
||||||
|
password = PasswordField(get_form_field_label('password'),
|
||||||
|
validators=[password_required])
|
||||||
|
|
||||||
|
|
||||||
|
class NewPasswordFormMixin():
|
||||||
|
password = PasswordField(get_form_field_label('password'),
|
||||||
|
validators=[password_required, password_length])
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordConfirmFormMixin():
|
||||||
|
password_confirm = PasswordField(
|
||||||
|
get_form_field_label('retype_password'),
|
||||||
|
validators=[EqualTo('password', message='RETYPE_PASSWORD_MISMATCH')])
|
||||||
|
|
||||||
|
|
||||||
|
class NextFormMixin():
|
||||||
|
next = HiddenField()
|
||||||
|
|
||||||
|
def validate_next(self, field):
|
||||||
|
url_next = urlparse.urlsplit(field.data)
|
||||||
|
url_base = urlparse.urlsplit(request.host_url)
|
||||||
|
if url_next.netloc and url_next.netloc != url_base.netloc:
|
||||||
|
field.data = ''
|
||||||
|
raise ValidationError(get_message('INVALID_REDIRECT')[0])
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterFormMixin():
|
||||||
|
submit = SubmitField(get_form_field_label('register'))
|
||||||
|
|
||||||
|
def to_dict(form):
|
||||||
|
def is_field_and_user_attr(member):
|
||||||
|
return isinstance(member, Field) and \
|
||||||
|
hasattr(_datastore.user_model, member.name)
|
||||||
|
|
||||||
|
fields = inspect.getmembers(form, is_field_and_user_attr)
|
||||||
|
return dict((key, value.data) for key, value in fields)
|
||||||
|
|
||||||
|
|
||||||
|
class SendConfirmationForm(Form, UserEmailFormMixin):
|
||||||
|
"""The default forgot password form"""
|
||||||
|
|
||||||
|
submit = SubmitField(get_form_field_label('send_confirmation'))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SendConfirmationForm, self).__init__(*args, **kwargs)
|
||||||
|
if request.method == 'GET':
|
||||||
|
self.email.data = request.args.get('email', None)
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
if not super(SendConfirmationForm, self).validate():
|
||||||
|
return False
|
||||||
|
if self.user.confirmed_at is not None:
|
||||||
|
self.email.errors.append(get_message('ALREADY_CONFIRMED')[0])
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ForgotPasswordForm(Form, UserEmailFormMixin):
|
||||||
|
"""The default forgot password form"""
|
||||||
|
|
||||||
|
submit = SubmitField(get_form_field_label('recover_password'))
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordlessLoginForm(Form, UserEmailFormMixin):
|
||||||
|
"""The passwordless login form"""
|
||||||
|
|
||||||
|
submit = SubmitField(get_form_field_label('send_login_link'))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(PasswordlessLoginForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
if not super(PasswordlessLoginForm, self).validate():
|
||||||
|
return False
|
||||||
|
if not self.user.is_active():
|
||||||
|
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class LoginForm(Form, NextFormMixin):
|
||||||
|
"""The default login form"""
|
||||||
|
|
||||||
|
email = TextField(get_form_field_label('email'))
|
||||||
|
password = PasswordField(get_form_field_label('password'))
|
||||||
|
remember = BooleanField(get_form_field_label('remember_me'))
|
||||||
|
submit = SubmitField(get_form_field_label('login'))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(LoginForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
if not super(LoginForm, self).validate():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.email.data.strip() == '':
|
||||||
|
self.email.errors.append(get_message('EMAIL_NOT_PROVIDED')[0])
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.password.data.strip() == '':
|
||||||
|
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.user = _datastore.find_user(email=self.email.data)
|
||||||
|
|
||||||
|
if self.user is None:
|
||||||
|
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
|
||||||
|
return False
|
||||||
|
if not verify_and_update_password(self.password.data, self.user):
|
||||||
|
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
|
||||||
|
return False
|
||||||
|
if requires_confirmation(self.user):
|
||||||
|
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
|
||||||
|
return False
|
||||||
|
if not self.user.is_active():
|
||||||
|
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmRegisterForm(Form, RegisterFormMixin,
|
||||||
|
UniqueEmailFormMixin, NewPasswordFormMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
|
||||||
|
"""The default reset password form"""
|
||||||
|
|
||||||
|
submit = SubmitField(get_form_field_label('reset_password'))
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordForm(Form, PasswordFormMixin):
|
||||||
|
"""The default change password form"""
|
||||||
|
|
||||||
|
new_password = PasswordField(get_form_field_label('new_password'),
|
||||||
|
validators=[password_required, password_length])
|
||||||
|
|
||||||
|
new_password_confirm = PasswordField(get_form_field_label('retype_password'),
|
||||||
|
validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
|
||||||
|
|
||||||
|
submit = SubmitField(get_form_field_label('change_password'))
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
if not super(ChangePasswordForm, self).validate():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.password.data.strip() == '':
|
||||||
|
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
|
||||||
|
return False
|
||||||
|
if not verify_and_update_password(self.password.data, current_user):
|
||||||
|
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
|
||||||
|
return False
|
||||||
|
return True
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.passwordless
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security passwordless module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import request, current_app as app
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .signals import login_instructions_sent
|
||||||
|
from .utils import send_mail, url_for_security, get_token_status, \
|
||||||
|
config_value
|
||||||
|
|
||||||
|
|
||||||
|
# Convenient references
|
||||||
|
_security = LocalProxy(lambda: app.extensions['security'])
|
||||||
|
|
||||||
|
_datastore = LocalProxy(lambda: _security.datastore)
|
||||||
|
|
||||||
|
|
||||||
|
def send_login_instructions(user):
|
||||||
|
"""Sends the login instructions email for the specified user.
|
||||||
|
|
||||||
|
:param user: The user to send the instructions to
|
||||||
|
:param token: The login token
|
||||||
|
"""
|
||||||
|
token = generate_login_token(user)
|
||||||
|
login_link = url_for_security('token_login', token=token, _external=True)
|
||||||
|
|
||||||
|
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
|
||||||
|
'login_instructions', user=user, login_link=login_link)
|
||||||
|
|
||||||
|
login_instructions_sent.send(app._get_current_object(),
|
||||||
|
user=user, login_token=token)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_login_token(user):
|
||||||
|
"""Generates a unique login token for the specified user.
|
||||||
|
|
||||||
|
:param user: The user the token belongs to
|
||||||
|
"""
|
||||||
|
return _security.login_serializer.dumps([str(user.id)])
|
||||||
|
|
||||||
|
|
||||||
|
def login_token_status(token):
|
||||||
|
"""Returns the expired status, invalid status, and user of a login token.
|
||||||
|
For example::
|
||||||
|
|
||||||
|
expired, invalid, user = login_token_status('...')
|
||||||
|
|
||||||
|
:param token: The login token
|
||||||
|
"""
|
||||||
|
return get_token_status(token, 'login', 'LOGIN')
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.recoverable
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security recoverable module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import current_app as app, request
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .signals import password_reset, reset_password_instructions_sent
|
||||||
|
from .utils import send_mail, md5, encrypt_password, url_for_security, \
|
||||||
|
get_token_status, config_value
|
||||||
|
|
||||||
|
|
||||||
|
# Convenient references
|
||||||
|
_security = LocalProxy(lambda: app.extensions['security'])
|
||||||
|
|
||||||
|
_datastore = LocalProxy(lambda: _security.datastore)
|
||||||
|
|
||||||
|
|
||||||
|
def send_reset_password_instructions(user):
|
||||||
|
"""Sends the reset password instructions email for the specified user.
|
||||||
|
|
||||||
|
:param user: The user to send the instructions to
|
||||||
|
"""
|
||||||
|
token = generate_reset_password_token(user)
|
||||||
|
reset_link = url_for_security('reset_password', token=token, _external=True)
|
||||||
|
|
||||||
|
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), user.email,
|
||||||
|
'reset_instructions',
|
||||||
|
user=user, reset_link=reset_link)
|
||||||
|
|
||||||
|
reset_password_instructions_sent.send(app._get_current_object(),
|
||||||
|
user=user, token=token)
|
||||||
|
|
||||||
|
|
||||||
|
def send_password_reset_notice(user):
|
||||||
|
"""Sends the password reset notice email for the specified user.
|
||||||
|
|
||||||
|
:param user: The user to send the notice to
|
||||||
|
"""
|
||||||
|
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
|
||||||
|
'reset_notice', user=user)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_reset_password_token(user):
|
||||||
|
"""Generates a unique reset password token for the specified user.
|
||||||
|
|
||||||
|
:param user: The user to work with
|
||||||
|
"""
|
||||||
|
data = [str(user.id), md5(user.password)]
|
||||||
|
return _security.reset_serializer.dumps(data)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_password_token_status(token):
|
||||||
|
"""Returns the expired status, invalid status, and user of a password reset
|
||||||
|
token. For example::
|
||||||
|
|
||||||
|
expired, invalid, user = reset_password_token_status('...')
|
||||||
|
|
||||||
|
:param token: The password reset token
|
||||||
|
"""
|
||||||
|
return get_token_status(token, 'reset', 'RESET_PASSWORD')
|
||||||
|
|
||||||
|
def update_password(user, password):
|
||||||
|
"""Update the specified user's password
|
||||||
|
|
||||||
|
:param user: The user to update_password
|
||||||
|
:param password: The unencrypted new password
|
||||||
|
"""
|
||||||
|
user.password = encrypt_password(password)
|
||||||
|
_datastore.put(user)
|
||||||
|
send_password_reset_notice(user)
|
||||||
|
password_reset.send(app._get_current_object(), user=user)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.registerable
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security registerable module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import current_app as app
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .confirmable import generate_confirmation_link
|
||||||
|
from .signals import user_registered
|
||||||
|
from .utils import do_flash, get_message, send_mail, encrypt_password, \
|
||||||
|
config_value
|
||||||
|
|
||||||
|
# Convenient references
|
||||||
|
_security = LocalProxy(lambda: app.extensions['security'])
|
||||||
|
|
||||||
|
_datastore = LocalProxy(lambda: _security.datastore)
|
||||||
|
|
||||||
|
|
||||||
|
def register_user(**kwargs):
|
||||||
|
confirmation_link, token = None, None
|
||||||
|
kwargs['password'] = encrypt_password(kwargs['password'])
|
||||||
|
user = _datastore.create_user(**kwargs)
|
||||||
|
_datastore.commit()
|
||||||
|
|
||||||
|
if _security.confirmable:
|
||||||
|
confirmation_link, token = generate_confirmation_link(user)
|
||||||
|
do_flash(*get_message('CONFIRM_REGISTRATION', email=user.email))
|
||||||
|
|
||||||
|
user_registered.send(app._get_current_object(),
|
||||||
|
user=user, confirm_token=token)
|
||||||
|
|
||||||
|
if config_value('SEND_REGISTER_EMAIL'):
|
||||||
|
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
|
||||||
|
user=user, confirmation_link=confirmation_link)
|
||||||
|
|
||||||
|
return user
|
||||||
+72
-31
@@ -3,50 +3,85 @@
|
|||||||
flask.ext.security.script
|
flask.ext.security.script
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
This module contains commands for use with the Flask-Script extension
|
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.
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
import simplejson as json
|
||||||
|
except ImportError:
|
||||||
|
import json
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
from flask.ext.script import Command, Option
|
from flask.ext.script import Command, Option
|
||||||
from flask.ext.security import (UserCreationError, UserNotFoundError,
|
from werkzeug.local import LocalProxy
|
||||||
RoleNotFoundError, user_datastore)
|
|
||||||
|
from .utils import encrypt_password
|
||||||
|
|
||||||
|
|
||||||
|
_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 wrapper(*args, **kwargs):
|
||||||
|
fn(*args, **kwargs)
|
||||||
|
_datastore.commit()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class CreateUserCommand(Command):
|
class CreateUserCommand(Command):
|
||||||
"""Create a user"""
|
"""Create a user"""
|
||||||
|
|
||||||
option_list = (
|
option_list = (
|
||||||
Option('-u', '--username', dest='username', default=None),
|
|
||||||
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=''),
|
||||||
Option('-r', '--roles', dest='roles', default=''),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@commit
|
||||||
def run(self, **kwargs):
|
def run(self, **kwargs):
|
||||||
# sanitize active input
|
# sanitize active input
|
||||||
ai = re.sub(r'\s', '', str(kwargs['active']))
|
ai = re.sub(r'\s', '', str(kwargs['active']))
|
||||||
kwargs['active'] = ai.lower() in ['', 'y','yes', '1', 'active']
|
kwargs['active'] = ai.lower() in ['', 'y', 'yes', '1', 'active']
|
||||||
|
|
||||||
# sanitize role input a bit
|
from flask_security.forms import ConfirmRegisterForm
|
||||||
ri = re.sub(r'\s', '', kwargs['roles'])
|
from werkzeug.datastructures import MultiDict
|
||||||
kwargs['roles'] = [] if ri == '' else ri.split(',')
|
|
||||||
|
form = ConfirmRegisterForm(MultiDict(kwargs), csrf_enabled=False)
|
||||||
user_datastore.create_user(**kwargs)
|
|
||||||
|
if form.validate():
|
||||||
print 'User created successfully.'
|
kwargs['password'] = encrypt_password(kwargs['password'])
|
||||||
kwargs['password'] = '****'
|
_datastore.create_user(**kwargs)
|
||||||
pprint(kwargs)
|
print 'User created successfully.'
|
||||||
|
kwargs['password'] = '****'
|
||||||
|
pprint(kwargs)
|
||||||
|
else:
|
||||||
|
print 'Error creating user'
|
||||||
|
pprint(form.errors)
|
||||||
|
|
||||||
|
|
||||||
class _RoleCommand(Command):
|
class CreateRoleCommand(Command):
|
||||||
|
"""Create a role"""
|
||||||
|
|
||||||
|
option_list = (
|
||||||
|
Option('-n', '--name', dest='name', default=None),
|
||||||
|
Option('-d', '--desc', dest='description', default=None),
|
||||||
|
)
|
||||||
|
|
||||||
|
@commit
|
||||||
|
def run(self, **kwargs):
|
||||||
|
_datastore.create_role(**kwargs)
|
||||||
|
print 'Role "%(name)s" created successfully.' % kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class _RoleCommand(Command):
|
||||||
option_list = (
|
option_list = (
|
||||||
Option('-u', '--user', dest='user_identifier'),
|
Option('-u', '--user', dest='user_identifier'),
|
||||||
Option('-r', '--role', dest='role_name'),
|
Option('-r', '--role', dest='role_name'),
|
||||||
@@ -55,35 +90,41 @@ class _RoleCommand(Command):
|
|||||||
|
|
||||||
class AddRoleCommand(_RoleCommand):
|
class AddRoleCommand(_RoleCommand):
|
||||||
"""Add a role to a user"""
|
"""Add a role to a user"""
|
||||||
|
|
||||||
|
@commit
|
||||||
def run(self, user_identifier, role_name):
|
def run(self, user_identifier, role_name):
|
||||||
user_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"""
|
"""Add a role to a user"""
|
||||||
|
|
||||||
|
@commit
|
||||||
def run(self, user_identifier, role_name):
|
def run(self, user_identifier, role_name):
|
||||||
user_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):
|
||||||
option_list = (
|
option_list = (
|
||||||
Option('-u', '--user', dest='user_identifier'),
|
Option('-u', '--user', dest='user_identifier'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeactivateUserCommand(_ToggleActiveCommand):
|
class DeactivateUserCommand(_ToggleActiveCommand):
|
||||||
"""Deactive a user"""
|
"""Deactive a user"""
|
||||||
|
|
||||||
|
@commit
|
||||||
def run(self, user_identifier):
|
def run(self, user_identifier):
|
||||||
user_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"""
|
"""Deactive a user"""
|
||||||
|
|
||||||
|
@commit
|
||||||
def run(self, user_identifier):
|
def run(self, user_identifier):
|
||||||
user_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
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.signals
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security signals module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import blinker
|
||||||
|
|
||||||
|
|
||||||
|
signals = blinker.Namespace()
|
||||||
|
|
||||||
|
user_registered = signals.signal("user-registered")
|
||||||
|
|
||||||
|
user_confirmed = signals.signal("user-confirmed")
|
||||||
|
|
||||||
|
confirm_instructions_sent = signals.signal("confirm-instructions-sent")
|
||||||
|
|
||||||
|
login_instructions_sent = signals.signal("login-instructions-sent")
|
||||||
|
|
||||||
|
password_reset = signals.signal("password-reset")
|
||||||
|
|
||||||
|
password_changed = signals.signal("password-changed")
|
||||||
|
|
||||||
|
reset_password_instructions_sent = signals.signal("password-reset-instructions-sent")
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{% macro render_field_with_errors(field) %}
|
||||||
|
<p>
|
||||||
|
{{ field.label }} {{ field(**kwargs)|safe }}
|
||||||
|
{% if field.errors %}
|
||||||
|
<ul>
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_field(field) %}
|
||||||
|
<p>{{ field(**kwargs)|safe }}</p>
|
||||||
|
{% endmacro %}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{% if security.registerable or security.recoverable or security.confirmabled %}
|
||||||
|
<h2>Menu</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{ url_for_security('login') }}">Login</a></li>
|
||||||
|
{% if security.registerable %}
|
||||||
|
<li><a href="{{ url_for_security('register') }}">Register</a><br/></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if security.recoverable %}
|
||||||
|
<li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if security.confirmable %}
|
||||||
|
<li><a href="{{ url_for_security('send_confirmation') }}">Confirm account</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{%- with messages = get_flashed_messages(with_categories=true) -%}
|
||||||
|
{% if messages %}
|
||||||
|
<ul class="flashes">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<li class="{{ category }}">{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{%- endwith %}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||||
|
{% include "security/_messages.html" %}
|
||||||
|
<h1>Change password</h1>
|
||||||
|
<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
|
||||||
|
{{ change_password_form.hidden_tag() }}
|
||||||
|
{{ render_field_with_errors(change_password_form.password) }}
|
||||||
|
{{ render_field_with_errors(change_password_form.new_password) }}
|
||||||
|
{{ render_field_with_errors(change_password_form.new_password_confirm) }}
|
||||||
|
{{ render_field(change_password_form.submit) }}
|
||||||
|
</form>
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<p>Your password has been changed.</p>
|
||||||
|
{% if security.recoverable %}
|
||||||
|
<p>If you did not change your password, <a href="{{ url_for_security('forgot_password', _external=True) }}">click here to reset it</a>.</p>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Your password has been changed
|
||||||
|
{% if security.recoverable %}
|
||||||
|
If you did not change your password, click the link below to reset it.
|
||||||
|
{{ url_for_security('forgot_password', _external=True) }}
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<p>Please confirm your email through the link below:</p>
|
||||||
|
|
||||||
|
<p><a href="{{ confirmation_link }}">Confirm my account</a></p>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Please confirm your email through the link below:
|
||||||
|
|
||||||
|
{{ confirmation_link }}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<p>Welcome {{ user.email }}!</p>
|
||||||
|
|
||||||
|
<p>You can log into your through the link below:</p>
|
||||||
|
|
||||||
|
<p><a href="{{ login_link }}">Login now</a></p>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Welcome {{ user.email }}!
|
||||||
|
|
||||||
|
You can log into your through the link below:
|
||||||
|
|
||||||
|
{{ login_link }}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p><a href="{{ reset_link }}">Click here to reset your password</a></p>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Click the link below to reset your password:
|
||||||
|
|
||||||
|
{{ reset_link }}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p>Your password has been reset</p>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Your password has been reset
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<p>Welcome {{ user.email }}!</p>
|
||||||
|
|
||||||
|
{% if security.confirmable %}
|
||||||
|
<p>You can confirm your email through the link below:</p>
|
||||||
|
|
||||||
|
<p><a href="{{ confirmation_link }}">Confirm my account</a></p>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Welcome {{ user.email }}!
|
||||||
|
|
||||||
|
{% if security.confirmable %}
|
||||||
|
You can confirm your email through the link below:
|
||||||
|
|
||||||
|
{{ confirmation_link }}
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||||
|
{% include "security/_messages.html" %}
|
||||||
|
<h1>Send password reset instructions</h1>
|
||||||
|
<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
|
||||||
|
{{ forgot_password_form.hidden_tag() }}
|
||||||
|
{{ render_field_with_errors(forgot_password_form.email) }}
|
||||||
|
{{ render_field(forgot_password_form.submit) }}
|
||||||
|
</form>
|
||||||
|
{% include "security/_menu.html" %}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||||
|
{% include "security/_messages.html" %}
|
||||||
|
<h1>Login</h1>
|
||||||
|
<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
|
||||||
|
{{ login_user_form.hidden_tag() }}
|
||||||
|
{{ render_field_with_errors(login_user_form.email) }}
|
||||||
|
{{ render_field_with_errors(login_user_form.password) }}
|
||||||
|
{{ render_field_with_errors(login_user_form.remember) }}
|
||||||
|
{{ render_field(login_user_form.next) }}
|
||||||
|
{{ render_field(login_user_form.submit) }}
|
||||||
|
</form>
|
||||||
|
{% include "security/_menu.html" %}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||||
|
{% include "security/_messages.html" %}
|
||||||
|
<h1>Register</h1>
|
||||||
|
<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
|
||||||
|
{{ register_user_form.hidden_tag() }}
|
||||||
|
{{ render_field_with_errors(register_user_form.email) }}
|
||||||
|
{{ render_field_with_errors(register_user_form.password) }}
|
||||||
|
{% if register_user_form.password_confirm %}
|
||||||
|
{{ render_field_with_errors(register_user_form.password_confirm) }}
|
||||||
|
{% endif %}
|
||||||
|
{{ render_field(register_user_form.submit) }}
|
||||||
|
</form>
|
||||||
|
{% include "security/_menu.html" %}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||||
|
{% include "security/_messages.html" %}
|
||||||
|
<h1>Reset password</h1>
|
||||||
|
<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form">
|
||||||
|
{{ reset_password_form.hidden_tag() }}
|
||||||
|
{{ render_field_with_errors(reset_password_form.password) }}
|
||||||
|
{{ render_field_with_errors(reset_password_form.password_confirm) }}
|
||||||
|
{{ render_field(reset_password_form.submit) }}
|
||||||
|
</form>
|
||||||
|
{% include "security/_menu.html" %}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||||
|
{% include "security/_messages.html" %}
|
||||||
|
<h1>Resend confirmation instructions</h1>
|
||||||
|
<form action="{{ url_for_security('send_confirmation') }}" method="POST" name="send_confirmation_form">
|
||||||
|
{{ send_confirmation_form.hidden_tag() }}
|
||||||
|
{{ render_field_with_errors(send_confirmation_form.email) }}
|
||||||
|
{{ render_field(send_confirmation_form.submit) }}
|
||||||
|
</form>
|
||||||
|
{% include "security/_menu.html" %}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||||
|
{% include "security/_messages.html" %}
|
||||||
|
<h1>Login</h1>
|
||||||
|
<form action="{{ url_for_security('login') }}" method="POST" name="send_login_form">
|
||||||
|
{{ send_login_form.hidden_tag() }}
|
||||||
|
{{ render_field_with_errors(send_login_form.email) }}
|
||||||
|
{{ render_field(send_login_form.submit) }}
|
||||||
|
</form>
|
||||||
|
{% include "security/_menu.html" %}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.utils
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security utils module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import blinker
|
||||||
|
import functools
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from flask import url_for, flash, current_app, request, session, render_template
|
||||||
|
from flask.ext.login import login_user as _login_user, \
|
||||||
|
logout_user as _logout_user
|
||||||
|
from flask.ext.mail import Message
|
||||||
|
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
|
||||||
|
from itsdangerous import BadSignature, SignatureExpired
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .signals import user_registered, user_confirmed, \
|
||||||
|
confirm_instructions_sent, login_instructions_sent, \
|
||||||
|
password_reset, password_changed, reset_password_instructions_sent
|
||||||
|
|
||||||
|
# Convenient references
|
||||||
|
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||||
|
|
||||||
|
_datastore = LocalProxy(lambda: _security.datastore)
|
||||||
|
|
||||||
|
_pwd_context = LocalProxy(lambda: _security.pwd_context)
|
||||||
|
|
||||||
|
|
||||||
|
def login_user(user, remember=None):
|
||||||
|
"""Performs the login and sends the appropriate signal."""
|
||||||
|
|
||||||
|
if remember is None:
|
||||||
|
remember = config_value('DEFAULT_REMEMBER_ME')
|
||||||
|
|
||||||
|
if not _login_user(user, remember):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if _security.trackable:
|
||||||
|
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
|
||||||
|
|
||||||
|
user.last_login_at = old_current_login or new_current_login
|
||||||
|
user.current_login_at = new_current_login
|
||||||
|
user.last_login_ip = old_current_ip or new_current_ip
|
||||||
|
user.current_login_ip = new_current_ip
|
||||||
|
user.login_count = user.login_count + 1 if user.login_count else 1
|
||||||
|
|
||||||
|
_datastore.put(user)
|
||||||
|
|
||||||
|
identity_changed.send(current_app._get_current_object(),
|
||||||
|
identity=Identity(user.id))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def logout_user():
|
||||||
|
for key in ('identity.name', 'identity.auth_type'):
|
||||||
|
session.pop(key, None)
|
||||||
|
identity_changed.send(current_app._get_current_object(),
|
||||||
|
identity=AnonymousIdentity())
|
||||||
|
_logout_user()
|
||||||
|
|
||||||
|
|
||||||
|
def get_hmac(password):
|
||||||
|
if _security.password_hash == 'plaintext':
|
||||||
|
return password
|
||||||
|
|
||||||
|
if _security.password_salt is None:
|
||||||
|
raise RuntimeError('The configuration value `SECURITY_PASSWORD_SALT` '
|
||||||
|
'must not be None when the value of `SECURITY_PASSWORD_HASH` is '
|
||||||
|
'set to "%s"' % _security.password_hash)
|
||||||
|
|
||||||
|
h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512)
|
||||||
|
return base64.b64encode(h.digest())
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(password, password_hash):
|
||||||
|
return _pwd_context.verify(get_hmac(password), password_hash)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_and_update_password(password, user):
|
||||||
|
verified, new_password = _pwd_context.verify_and_update(get_hmac(password), user.password)
|
||||||
|
if verified and new_password:
|
||||||
|
user.password = new_password
|
||||||
|
_datastore.put(user)
|
||||||
|
return verified
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_password(password):
|
||||||
|
return _pwd_context.encrypt(get_hmac(password))
|
||||||
|
|
||||||
|
|
||||||
|
def md5(data):
|
||||||
|
return hashlib.md5(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def do_flash(message, category=None):
|
||||||
|
"""Flash a message depending on if the `FLASH_MESSAGES` configuration
|
||||||
|
value is set.
|
||||||
|
|
||||||
|
:param message: The flash message
|
||||||
|
:param category: The flash message category
|
||||||
|
"""
|
||||||
|
if config_value('FLASH_MESSAGES'):
|
||||||
|
flash(message, category)
|
||||||
|
|
||||||
|
|
||||||
|
def get_url(endpoint_or_url):
|
||||||
|
"""Returns a URL if a valid endpoint is found. Otherwise, returns the
|
||||||
|
provided value.
|
||||||
|
|
||||||
|
:param endpoint_or_url: The endpoint name or URL to default to
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return url_for(endpoint_or_url)
|
||||||
|
except:
|
||||||
|
return endpoint_or_url
|
||||||
|
|
||||||
|
|
||||||
|
def get_security_endpoint_name(endpoint):
|
||||||
|
return '%s.%s' % (_security.blueprint_name, endpoint)
|
||||||
|
|
||||||
|
|
||||||
|
def url_for_security(endpoint, **values):
|
||||||
|
"""Return a URL for the security blueprint
|
||||||
|
|
||||||
|
:param endpoint: the endpoint of the URL (name of the function)
|
||||||
|
:param values: the variable arguments of the URL rule
|
||||||
|
:param _external: if set to `True`, an absolute URL is generated. Server
|
||||||
|
address can be changed via `SERVER_NAME` configuration variable which
|
||||||
|
defaults to `localhost`.
|
||||||
|
:param _anchor: if provided this is added as anchor to the URL.
|
||||||
|
:param _method: if provided this explicitly specifies an HTTP method.
|
||||||
|
"""
|
||||||
|
endpoint = get_security_endpoint_name(endpoint)
|
||||||
|
return url_for(endpoint, **values)
|
||||||
|
|
||||||
|
|
||||||
|
def get_post_action_redirect(config_key):
|
||||||
|
return (get_url(request.args.get('next')) or
|
||||||
|
get_url(request.form.get('next')) or
|
||||||
|
find_redirect(config_key))
|
||||||
|
|
||||||
|
|
||||||
|
def get_post_login_redirect():
|
||||||
|
return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW')
|
||||||
|
|
||||||
|
|
||||||
|
def get_post_register_redirect():
|
||||||
|
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW')
|
||||||
|
|
||||||
|
|
||||||
|
def find_redirect(key):
|
||||||
|
"""Returns the URL to redirect to after a user logs in successfully.
|
||||||
|
|
||||||
|
:param key: The session or application configuration key to search for
|
||||||
|
"""
|
||||||
|
rv = (get_url(session.pop(key.lower(), None)) or
|
||||||
|
get_url(current_app.config[key.upper()] or None) or '/')
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(app):
|
||||||
|
"""Conveniently get the security configuration for the specified
|
||||||
|
application without the annoying 'SECURITY_' prefix.
|
||||||
|
|
||||||
|
:param app: The application to inspect
|
||||||
|
"""
|
||||||
|
items = app.config.items()
|
||||||
|
prefix = 'SECURITY_'
|
||||||
|
|
||||||
|
def strip_prefix(tup):
|
||||||
|
return (tup[0].replace('SECURITY_', ''), tup[1])
|
||||||
|
|
||||||
|
return dict([strip_prefix(i) for i in items if i[0].startswith(prefix)])
|
||||||
|
|
||||||
|
|
||||||
|
def get_message(key, **kwargs):
|
||||||
|
rv = config_value('MSG_' + key)
|
||||||
|
return rv[0] % kwargs, rv[1]
|
||||||
|
|
||||||
|
|
||||||
|
def config_value(key, app=None, default=None):
|
||||||
|
"""Get a Flask-Security configuration value.
|
||||||
|
|
||||||
|
:param key: The configuration key without the prefix `SECURITY_`
|
||||||
|
:param app: An optional specific application to inspect. Defaults to Flask's
|
||||||
|
`current_app`
|
||||||
|
:param default: An optional default value if the value is not set
|
||||||
|
"""
|
||||||
|
app = app or current_app
|
||||||
|
return get_config(app).get(key.upper(), default)
|
||||||
|
|
||||||
|
|
||||||
|
def get_max_age(key, app=None):
|
||||||
|
now = datetime.utcnow()
|
||||||
|
expires = now + get_within_delta(key + '_WITHIN', app)
|
||||||
|
td = (expires - now)
|
||||||
|
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6
|
||||||
|
|
||||||
|
|
||||||
|
def get_within_delta(key, app=None):
|
||||||
|
"""Get a timedelta object from the application configuration following
|
||||||
|
the internal convention of::
|
||||||
|
|
||||||
|
<Amount of Units> <Type of Units>
|
||||||
|
|
||||||
|
Examples of valid config values::
|
||||||
|
|
||||||
|
5 days
|
||||||
|
10 minutes
|
||||||
|
|
||||||
|
:param key: The config value key without the 'SECURITY_' prefix
|
||||||
|
:param app: Optional application to inspect. Defaults to Flask's
|
||||||
|
`current_app`
|
||||||
|
"""
|
||||||
|
txt = config_value(key, app=app)
|
||||||
|
values = txt.split()
|
||||||
|
return timedelta(**{values[1]: int(values[0])})
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail(subject, recipient, template, **context):
|
||||||
|
"""Send an email via the Flask-Mail extension.
|
||||||
|
|
||||||
|
:param subject: Email subject
|
||||||
|
:param recipient: Email recipient
|
||||||
|
:param template: The name of the email template
|
||||||
|
:param context: The context to render the template with
|
||||||
|
"""
|
||||||
|
|
||||||
|
context.setdefault('security', _security)
|
||||||
|
context.update(_security._run_ctx_processor('mail'))
|
||||||
|
|
||||||
|
msg = Message(subject,
|
||||||
|
sender=_security.email_sender,
|
||||||
|
recipients=[recipient])
|
||||||
|
|
||||||
|
ctx = ('security/email', template)
|
||||||
|
msg.body = render_template('%s/%s.txt' % ctx, **context)
|
||||||
|
msg.html = render_template('%s/%s.html' % ctx, **context)
|
||||||
|
|
||||||
|
if _security._send_mail_task:
|
||||||
|
_security._send_mail_task(msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
mail = current_app.extensions.get('mail')
|
||||||
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_status(token, serializer, max_age=None):
|
||||||
|
serializer = getattr(_security, serializer + '_serializer')
|
||||||
|
max_age = get_max_age(max_age)
|
||||||
|
user, data = None, None
|
||||||
|
expired, invalid = False, False
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = serializer.loads(token, max_age=max_age)
|
||||||
|
except SignatureExpired:
|
||||||
|
d, data = serializer.loads_unsafe(token)
|
||||||
|
expired = True
|
||||||
|
except BadSignature:
|
||||||
|
invalid = True
|
||||||
|
|
||||||
|
if data:
|
||||||
|
user = _datastore.find_user(id=data[0])
|
||||||
|
|
||||||
|
expired = expired and (user is not None)
|
||||||
|
return expired, invalid, user
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def capture_passwordless_login_requests():
|
||||||
|
login_requests = []
|
||||||
|
|
||||||
|
def _on(app, **data):
|
||||||
|
login_requests.append(data)
|
||||||
|
|
||||||
|
login_instructions_sent.connect(_on)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield login_requests
|
||||||
|
finally:
|
||||||
|
login_instructions_sent.disconnect(_on)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def capture_registrations():
|
||||||
|
"""Testing utility for capturing registrations.
|
||||||
|
|
||||||
|
:param confirmation_sent_at: An optional datetime object to set the
|
||||||
|
user's `confirmation_sent_at` to
|
||||||
|
"""
|
||||||
|
registrations = []
|
||||||
|
|
||||||
|
def _on(app, **data):
|
||||||
|
registrations.append(data)
|
||||||
|
|
||||||
|
user_registered.connect(_on)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield registrations
|
||||||
|
finally:
|
||||||
|
user_registered.disconnect(_on)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def capture_reset_password_requests(reset_password_sent_at=None):
|
||||||
|
"""Testing utility for capturing password reset requests.
|
||||||
|
|
||||||
|
:param reset_password_sent_at: An optional datetime object to set the
|
||||||
|
user's `reset_password_sent_at` to
|
||||||
|
"""
|
||||||
|
reset_requests = []
|
||||||
|
|
||||||
|
def _on(app, **data):
|
||||||
|
reset_requests.append(data)
|
||||||
|
|
||||||
|
reset_password_instructions_sent.connect(_on)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield reset_requests
|
||||||
|
finally:
|
||||||
|
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])
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flask.ext.security.views
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Flask-Security views module
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Matt Wright.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import current_app, redirect, request, render_template, jsonify, \
|
||||||
|
after_this_request, Blueprint
|
||||||
|
from flask_login import current_user
|
||||||
|
from werkzeug.datastructures import MultiDict
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .confirmable import send_confirmation_instructions, \
|
||||||
|
confirm_user, confirm_email_token_status
|
||||||
|
from .decorators import login_required, anonymous_user_required
|
||||||
|
from .passwordless import send_login_instructions, \
|
||||||
|
login_token_status
|
||||||
|
from .recoverable import reset_password_token_status, \
|
||||||
|
send_reset_password_instructions, update_password
|
||||||
|
from .changeable import change_user_password
|
||||||
|
from .registerable import register_user
|
||||||
|
from .utils import config_value, do_flash, get_url, get_post_login_redirect, \
|
||||||
|
get_post_register_redirect, get_message, login_user, logout_user, \
|
||||||
|
url_for_security as url_for
|
||||||
|
|
||||||
|
# Convenient references
|
||||||
|
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||||
|
|
||||||
|
_datastore = LocalProxy(lambda: _security.datastore)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_json(form, include_auth_token=False):
|
||||||
|
has_errors = len(form.errors) > 0
|
||||||
|
|
||||||
|
if has_errors:
|
||||||
|
code = 400
|
||||||
|
response = dict(errors=form.errors)
|
||||||
|
else:
|
||||||
|
code = 200
|
||||||
|
response = dict(user=dict(id=str(form.user.id)))
|
||||||
|
if include_auth_token:
|
||||||
|
token = form.user.get_auth_token()
|
||||||
|
response['user']['authentication_token'] = token
|
||||||
|
|
||||||
|
return jsonify(dict(meta=dict(code=code), response=response))
|
||||||
|
|
||||||
|
|
||||||
|
def _commit(response=None):
|
||||||
|
_datastore.commit()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(endpoint):
|
||||||
|
return _security._run_ctx_processor(endpoint)
|
||||||
|
|
||||||
|
|
||||||
|
@anonymous_user_required
|
||||||
|
def login():
|
||||||
|
"""View function for login view"""
|
||||||
|
|
||||||
|
form_class = _security.login_form
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
form = form_class(MultiDict(request.json))
|
||||||
|
else:
|
||||||
|
form = form_class()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
login_user(form.user, remember=form.remember.data)
|
||||||
|
after_this_request(_commit)
|
||||||
|
|
||||||
|
if not request.json:
|
||||||
|
return redirect(get_post_login_redirect())
|
||||||
|
|
||||||
|
form.next.data = get_url(request.args.get('next')) \
|
||||||
|
or get_url(request.form.get('next')) or ''
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
return _render_json(form, True)
|
||||||
|
|
||||||
|
return render_template(config_value('LOGIN_USER_TEMPLATE'),
|
||||||
|
login_user_form=form,
|
||||||
|
**_ctx('login'))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
"""View function which handles a logout request."""
|
||||||
|
|
||||||
|
logout_user()
|
||||||
|
|
||||||
|
return redirect(request.args.get('next', None) or
|
||||||
|
get_url(_security.post_logout_view))
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
"""View function which handles a registration request."""
|
||||||
|
|
||||||
|
if _security.confirmable or request.json:
|
||||||
|
form_class = _security.confirm_register_form
|
||||||
|
else:
|
||||||
|
form_class = _security.register_form
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
form_data = MultiDict(request.json)
|
||||||
|
else:
|
||||||
|
form_data = request.form
|
||||||
|
|
||||||
|
form = form_class(form_data)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = register_user(**form.to_dict())
|
||||||
|
form.user = user
|
||||||
|
|
||||||
|
if not _security.confirmable or _security.login_without_confirmation:
|
||||||
|
after_this_request(_commit)
|
||||||
|
login_user(user)
|
||||||
|
|
||||||
|
if not request.json:
|
||||||
|
return redirect(get_post_register_redirect())
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
return _render_json(form)
|
||||||
|
|
||||||
|
return render_template(config_value('REGISTER_USER_TEMPLATE'),
|
||||||
|
register_user_form=form,
|
||||||
|
**_ctx('register'))
|
||||||
|
|
||||||
|
|
||||||
|
def send_login():
|
||||||
|
"""View function that sends login instructions for passwordless login"""
|
||||||
|
|
||||||
|
form_class = _security.passwordless_login_form
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
form = form_class(MultiDict(request.json))
|
||||||
|
else:
|
||||||
|
form = form_class()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
send_login_instructions(form.user)
|
||||||
|
if request.json is None:
|
||||||
|
do_flash(*get_message('LOGIN_EMAIL_SENT', email=form.user.email))
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
return _render_json(form)
|
||||||
|
|
||||||
|
return render_template(config_value('SEND_LOGIN_TEMPLATE'),
|
||||||
|
send_login_form=form,
|
||||||
|
**_ctx('send_login'))
|
||||||
|
|
||||||
|
|
||||||
|
@anonymous_user_required
|
||||||
|
def token_login(token):
|
||||||
|
"""View function that handles passwordless login via a token"""
|
||||||
|
|
||||||
|
expired, invalid, user = login_token_status(token)
|
||||||
|
|
||||||
|
if invalid:
|
||||||
|
do_flash(*get_message('INVALID_LOGIN_TOKEN'))
|
||||||
|
if expired:
|
||||||
|
send_login_instructions(user)
|
||||||
|
do_flash(*get_message('LOGIN_EXPIRED', email=user.email,
|
||||||
|
within=_security.login_within))
|
||||||
|
if invalid or expired:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
login_user(user)
|
||||||
|
after_this_request(_commit)
|
||||||
|
do_flash(*get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'))
|
||||||
|
|
||||||
|
return redirect(get_post_login_redirect())
|
||||||
|
|
||||||
|
|
||||||
|
def send_confirmation():
|
||||||
|
"""View function which sends confirmation instructions."""
|
||||||
|
|
||||||
|
form_class = _security.send_confirmation_form
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
form = form_class(MultiDict(request.json))
|
||||||
|
else:
|
||||||
|
form = form_class()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
send_confirmation_instructions(form.user)
|
||||||
|
if request.json is None:
|
||||||
|
do_flash(*get_message('CONFIRMATION_REQUEST', email=form.user.email))
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
return _render_json(form)
|
||||||
|
|
||||||
|
return render_template(config_value('SEND_CONFIRMATION_TEMPLATE'),
|
||||||
|
send_confirmation_form=form,
|
||||||
|
**_ctx('send_confirmation'))
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_email(token):
|
||||||
|
"""View function which handles a email confirmation request."""
|
||||||
|
|
||||||
|
expired, invalid, user = confirm_email_token_status(token)
|
||||||
|
|
||||||
|
if not user or invalid:
|
||||||
|
invalid = True
|
||||||
|
do_flash(*get_message('INVALID_CONFIRMATION_TOKEN'))
|
||||||
|
if expired:
|
||||||
|
send_confirmation_instructions(user)
|
||||||
|
do_flash(*get_message('CONFIRMATION_EXPIRED', email=user.email,
|
||||||
|
within=_security.confirm_email_within))
|
||||||
|
if invalid or expired:
|
||||||
|
return redirect(get_url(_security.confirm_error_view) or
|
||||||
|
url_for('send_confirmation'))
|
||||||
|
|
||||||
|
if user != current_user:
|
||||||
|
logout_user()
|
||||||
|
login_user(user)
|
||||||
|
|
||||||
|
confirm_user(user)
|
||||||
|
after_this_request(_commit)
|
||||||
|
do_flash(*get_message('EMAIL_CONFIRMED'))
|
||||||
|
|
||||||
|
return redirect(get_url(_security.post_confirm_view) or
|
||||||
|
get_url(_security.post_login_view))
|
||||||
|
|
||||||
|
|
||||||
|
def forgot_password():
|
||||||
|
"""View function that handles a forgotten password request."""
|
||||||
|
|
||||||
|
form_class = _security.forgot_password_form
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
form = form_class(MultiDict(request.json))
|
||||||
|
else:
|
||||||
|
form = form_class()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
send_reset_password_instructions(form.user)
|
||||||
|
if request.json is None:
|
||||||
|
do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email))
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
return _render_json(form)
|
||||||
|
|
||||||
|
return render_template(config_value('FORGOT_PASSWORD_TEMPLATE'),
|
||||||
|
forgot_password_form=form,
|
||||||
|
**_ctx('forgot_password'))
|
||||||
|
|
||||||
|
|
||||||
|
@anonymous_user_required
|
||||||
|
def reset_password(token):
|
||||||
|
"""View function that handles a reset password request."""
|
||||||
|
|
||||||
|
expired, invalid, user = reset_password_token_status(token)
|
||||||
|
|
||||||
|
if invalid:
|
||||||
|
do_flash(*get_message('INVALID_RESET_PASSWORD_TOKEN'))
|
||||||
|
if expired:
|
||||||
|
do_flash(*get_message('PASSWORD_RESET_EXPIRED', email=user.email,
|
||||||
|
within=_security.reset_password_within))
|
||||||
|
if invalid or expired:
|
||||||
|
return redirect(url_for('forgot_password'))
|
||||||
|
|
||||||
|
form = _security.reset_password_form()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
after_this_request(_commit)
|
||||||
|
update_password(user, form.password.data)
|
||||||
|
do_flash(*get_message('PASSWORD_RESET'))
|
||||||
|
login_user(user)
|
||||||
|
return redirect(get_url(_security.post_reset_view) or
|
||||||
|
get_url(_security.post_login_view))
|
||||||
|
|
||||||
|
return render_template(config_value('RESET_PASSWORD_TEMPLATE'),
|
||||||
|
reset_password_form=form,
|
||||||
|
reset_password_token=token,
|
||||||
|
**_ctx('reset_password'))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def change_password():
|
||||||
|
"""View function which handles a change password request."""
|
||||||
|
|
||||||
|
form_class = _security.change_password_form
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
form = form_class(MultiDict(request.json))
|
||||||
|
else:
|
||||||
|
form = form_class()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
after_this_request(_commit)
|
||||||
|
change_user_password(current_user, form.new_password.data)
|
||||||
|
if request.json is None:
|
||||||
|
do_flash(*get_message('PASSWORD_CHANGE'))
|
||||||
|
return redirect(get_url(_security.post_change_view) or
|
||||||
|
get_url(_security.post_login_view))
|
||||||
|
|
||||||
|
if request.json:
|
||||||
|
return _render_json(form)
|
||||||
|
|
||||||
|
return render_template('security/change_password.html',
|
||||||
|
change_password_form=form,
|
||||||
|
**_ctx('change_password'))
|
||||||
|
|
||||||
|
|
||||||
|
def create_blueprint(state, import_name):
|
||||||
|
"""Creates the security extension blueprint"""
|
||||||
|
|
||||||
|
bp = Blueprint(state.blueprint_name, import_name,
|
||||||
|
url_prefix=state.url_prefix,
|
||||||
|
subdomain=state.subdomain,
|
||||||
|
template_folder='templates')
|
||||||
|
|
||||||
|
bp.route(state.logout_url, endpoint='logout')(logout)
|
||||||
|
|
||||||
|
if state.passwordless:
|
||||||
|
bp.route(state.login_url,
|
||||||
|
methods=['GET', 'POST'],
|
||||||
|
endpoint='login')(send_login)
|
||||||
|
bp.route(state.login_url + '/<token>',
|
||||||
|
endpoint='token_login')(token_login)
|
||||||
|
else:
|
||||||
|
bp.route(state.login_url,
|
||||||
|
methods=['GET', 'POST'],
|
||||||
|
endpoint='login')(login)
|
||||||
|
|
||||||
|
if state.registerable:
|
||||||
|
bp.route(state.register_url,
|
||||||
|
methods=['GET', 'POST'],
|
||||||
|
endpoint='register')(register)
|
||||||
|
|
||||||
|
if state.recoverable:
|
||||||
|
bp.route(state.reset_url,
|
||||||
|
methods=['GET', 'POST'],
|
||||||
|
endpoint='forgot_password')(forgot_password)
|
||||||
|
bp.route(state.reset_url + '/<token>',
|
||||||
|
methods=['GET', 'POST'],
|
||||||
|
endpoint='reset_password')(reset_password)
|
||||||
|
|
||||||
|
if state.changeable:
|
||||||
|
bp.route(state.change_url,
|
||||||
|
methods=['GET', 'POST'],
|
||||||
|
endpoint='change_password')(change_password)
|
||||||
|
|
||||||
|
if state.confirmable:
|
||||||
|
bp.route(state.confirm_url,
|
||||||
|
methods=['GET', 'POST'],
|
||||||
|
endpoint='send_confirmation')(send_confirmation)
|
||||||
|
bp.route(state.confirm_url + '/<token>',
|
||||||
|
methods=['GET', 'POST'],
|
||||||
|
endpoint='confirm_email')(confirm_email)
|
||||||
|
|
||||||
|
return bp
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Flask>=0.9
|
||||||
|
Flask-Login>=0.1.3
|
||||||
|
Flask-Mail>=0.7.3
|
||||||
|
Flask-Principal>=0.3.3
|
||||||
|
Flask-WTF>=0.8
|
||||||
|
itsdangerous>=0.17
|
||||||
|
passlib>=1.6.1
|
||||||
Executable
+189
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
make-release
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Helper script that performs a release. Does pretty much everything
|
||||||
|
automatically for us.
|
||||||
|
|
||||||
|
:copyright: (c) 2011 by Armin Ronacher.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import datetime, date
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
|
_date_clean_re = re.compile(r'(\d+)(st|nd|rd|th)')
|
||||||
|
|
||||||
|
|
||||||
|
def installed_libraries():
|
||||||
|
return Popen(['pip', 'freeze'], stdout=PIPE).communicate()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def has_library_installed(library):
|
||||||
|
return library + '==' in installed_libraries()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_changelog():
|
||||||
|
with open('CHANGES') as f:
|
||||||
|
lineiter = iter(f)
|
||||||
|
for line in lineiter:
|
||||||
|
match = re.search('^Version\s+(.*)', line.strip())
|
||||||
|
|
||||||
|
if match is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
version = match.group(1).strip()
|
||||||
|
|
||||||
|
if lineiter.next().count('-') != len(line.strip()):
|
||||||
|
fail('Invalid hyphen count below version line: %s', line.strip())
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
released = lineiter.next().strip()
|
||||||
|
if released:
|
||||||
|
break
|
||||||
|
|
||||||
|
match = re.search(r'Released (\w+\s+\d+\w+\s+\d+)', released)
|
||||||
|
|
||||||
|
if match is None:
|
||||||
|
fail('Could not find release date in version %s' % version)
|
||||||
|
|
||||||
|
datestr = parse_date(match.group(1).strip())
|
||||||
|
|
||||||
|
return version, datestr
|
||||||
|
|
||||||
|
|
||||||
|
def bump_version(version):
|
||||||
|
try:
|
||||||
|
parts = map(int, version.split('.'))
|
||||||
|
except ValueError:
|
||||||
|
fail('Current version is not numeric')
|
||||||
|
parts[-1] += 1
|
||||||
|
return '.'.join(map(str, parts))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date(string):
|
||||||
|
string = _date_clean_re.sub(r'\1', string)
|
||||||
|
return datetime.strptime(string, '%B %d %Y')
|
||||||
|
|
||||||
|
|
||||||
|
def set_filename_version(filename, version_number, pattern):
|
||||||
|
changed = []
|
||||||
|
|
||||||
|
def inject_version(match):
|
||||||
|
before, old, after = match.groups()
|
||||||
|
changed.append(True)
|
||||||
|
return before + version_number + after
|
||||||
|
|
||||||
|
with open(filename) as f:
|
||||||
|
contents = re.sub(r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern,
|
||||||
|
inject_version, f.read())
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
fail('Could not find %s in %s', pattern, filename)
|
||||||
|
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
|
||||||
|
def set_init_version(version):
|
||||||
|
info('Setting __init__.py version to %s', version)
|
||||||
|
set_filename_version('flask_security/__init__.py', version, '__version__')
|
||||||
|
|
||||||
|
|
||||||
|
def set_setup_version(version):
|
||||||
|
info('Setting setup.py version to %s', version)
|
||||||
|
set_filename_version('setup.py', version, 'version')
|
||||||
|
|
||||||
|
|
||||||
|
def set_docs_version(version):
|
||||||
|
info('Setting docs/conf.py version to %s', version)
|
||||||
|
set_filename_version('docs/conf.py', version, 'version')
|
||||||
|
|
||||||
|
|
||||||
|
def build_and_upload():
|
||||||
|
Popen([sys.executable, 'setup.py', 'sdist', 'build_sphinx', 'upload', 'upload_sphinx']).wait()
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message, *args):
|
||||||
|
print >> sys.stderr, 'Error:', message % args
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def info(message, *args):
|
||||||
|
print >> sys.stderr, message % args
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_tags():
|
||||||
|
return set(Popen(['git', 'tag'], stdout=PIPE).communicate()[0].splitlines())
|
||||||
|
|
||||||
|
|
||||||
|
def git_is_clean():
|
||||||
|
return Popen(['git', 'diff', '--quiet']).wait() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def make_git_commit(message, *args):
|
||||||
|
message = message % args
|
||||||
|
Popen(['git', 'commit', '-am', message]).wait()
|
||||||
|
|
||||||
|
|
||||||
|
def make_git_tag(tag):
|
||||||
|
info('Tagging "%s"', tag)
|
||||||
|
Popen(['git', 'tag', '-a', tag, '-m', '%s release' % tag]).wait()
|
||||||
|
Popen(['git', 'push', '--tags']).wait()
|
||||||
|
|
||||||
|
|
||||||
|
def update_version(version):
|
||||||
|
for f in [set_init_version, set_setup_version, set_docs_version]:
|
||||||
|
f(version)
|
||||||
|
|
||||||
|
|
||||||
|
def get_branches():
|
||||||
|
return set(Popen(['git', 'branch'], stdout=PIPE).communicate()[0].splitlines())
|
||||||
|
|
||||||
|
|
||||||
|
def branch_is(branch):
|
||||||
|
return '* ' + branch in get_branches()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.chdir(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
rv = parse_changelog()
|
||||||
|
|
||||||
|
if rv is None:
|
||||||
|
fail('Could not parse changelog')
|
||||||
|
|
||||||
|
version, release_date = rv
|
||||||
|
|
||||||
|
tags = get_git_tags()
|
||||||
|
|
||||||
|
for lib in ['Sphinx', 'Sphinx-PyPI-upload']:
|
||||||
|
if not has_library_installed(lib):
|
||||||
|
fail('Build requires that %s be installed', lib)
|
||||||
|
|
||||||
|
if version in tags:
|
||||||
|
fail('Version "%s" is already tagged', version)
|
||||||
|
if release_date.date() != date.today():
|
||||||
|
fail('Release date is not today')
|
||||||
|
|
||||||
|
if not branch_is('master'):
|
||||||
|
fail('You are not on the master branch')
|
||||||
|
|
||||||
|
if not git_is_clean():
|
||||||
|
fail('You have uncommitted changes in git')
|
||||||
|
|
||||||
|
info('Releasing %s (release date %s)',
|
||||||
|
version, release_date.strftime('%d/%m/%Y'))
|
||||||
|
|
||||||
|
update_version(version)
|
||||||
|
make_git_commit('Bump version number to %s', version)
|
||||||
|
make_git_tag(version)
|
||||||
|
build_and_upload()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[build_sphinx]
|
||||||
|
source-dir = docs/
|
||||||
|
build-dir = docs/_build
|
||||||
|
|
||||||
|
[upload_sphinx]
|
||||||
|
upload-dir = docs/_build/html
|
||||||
@@ -1,51 +1,55 @@
|
|||||||
"""
|
"""
|
||||||
Flask-Security
|
Flask-Security
|
||||||
--------------
|
==============
|
||||||
|
|
||||||
Flask-Security is a Flask extension that aims to add quick and simple security
|
Flask-Security is a Flask extension that aims to add quick and simple security
|
||||||
via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
|
to your Flask applications.
|
||||||
|
|
||||||
Links
|
Resources
|
||||||
`````
|
---------
|
||||||
|
|
||||||
* `development version
|
* `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>`_
|
<https://github.com/mattupstate/flask-security/raw/develop#egg=Flask-Security-dev>`_
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='Flask-Security',
|
name='Flask-Security',
|
||||||
version='1.2.0',
|
version='1.6.7',
|
||||||
url='https://github.com/mattupstate/flask-security',
|
url='https://github.com/mattupstate/flask-security',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
author='Matthew 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=__doc__,
|
||||||
packages=[
|
packages=[
|
||||||
'flask_security',
|
'flask_security'
|
||||||
'flask_security.datastore'
|
|
||||||
],
|
],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
platforms='any',
|
platforms='any',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'Flask',
|
'Flask>=0.9',
|
||||||
'Flask-Login',
|
'Flask-Login>=0.2.3',
|
||||||
'Flask-Principal',
|
'Flask-Mail>=0.7.3',
|
||||||
'Flask-WTF',
|
'Flask-Principal>=0.3.3',
|
||||||
'passlib'
|
'Flask-WTF>=0.8',
|
||||||
|
'itsdangerous>=0.17',
|
||||||
|
'passlib>=1.6.1',
|
||||||
],
|
],
|
||||||
test_suite='nose.collector',
|
test_suite='nose.collector',
|
||||||
tests_require=[
|
tests_require=[
|
||||||
'nose',
|
'nose',
|
||||||
'Flask-SQLAlchemy',
|
'Flask-SQLAlchemy',
|
||||||
'Flask-MongoEngine',
|
'Flask-MongoEngine',
|
||||||
'py-bcrypt'
|
'Flask-Peewee',
|
||||||
],
|
'py-bcrypt',
|
||||||
dependency_links=[
|
'simplejson'
|
||||||
'http://github.com/sbook/flask-mongoengine/tarball/master#egg=Flask-MongoEngine-0.1.3-dev'
|
|
||||||
],
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# -*- 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
|
||||||
@@ -0,0 +1,799 @@
|
|||||||
|
# -*- 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)
|
||||||
|
|
||||||
|
def test_confirm_email_of_user_different_than_current_user(self):
|
||||||
|
e1 = 'dude@lp.com'
|
||||||
|
e2 = 'lady@lp.com'
|
||||||
|
|
||||||
|
with capture_registrations() as registrations:
|
||||||
|
self.register(e1)
|
||||||
|
self.register(e2)
|
||||||
|
token1 = registrations[0]['confirm_token']
|
||||||
|
token2 = registrations[1]['confirm_token']
|
||||||
|
|
||||||
|
self.client.get('/confirm/' + token1, follow_redirects=True)
|
||||||
|
self.client.get('/logout')
|
||||||
|
self.authenticate(email=e1)
|
||||||
|
r = self.client.get('/confirm/' + token2, follow_redirects=True)
|
||||||
|
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
|
||||||
|
self.assertIn(msg, r.data)
|
||||||
|
self.assertIn('Hello %s' % e2, r.data)
|
||||||
|
|
||||||
|
|
||||||
|
class RecoverableTests(SecurityTest):
|
||||||
|
|
||||||
|
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('Password must be at least 6 characters', r.data)
|
||||||
|
|
||||||
|
def test_change_password_success(self):
|
||||||
|
self.authenticate()
|
||||||
|
with self.app.extensions['mail'].record_messages() as outbox:
|
||||||
|
r = self._post('/change', data={
|
||||||
|
'password': 'password',
|
||||||
|
'new_password': 'newpassword',
|
||||||
|
'new_password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
|
||||||
|
self.assertIn('You successfully changed your password', r.data)
|
||||||
|
self.assertIn('Home Page', r.data)
|
||||||
|
|
||||||
|
self.assertEqual(len(outbox), 1)
|
||||||
|
self.assertIn("Your password has been changed", outbox[0].html)
|
||||||
|
self.assertIn("/reset", outbox[0].html)
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordPostViewTest(SecurityTest):
|
||||||
|
|
||||||
|
AUTH_CONFIG = {
|
||||||
|
'SECURITY_CHANGEABLE': True,
|
||||||
|
'SECURITY_POST_CHANGE_VIEW': '/profile',
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_change_password_success(self):
|
||||||
|
self.authenticate()
|
||||||
|
r = self._post('/change', data={
|
||||||
|
'password': 'password',
|
||||||
|
'new_password': 'newpassword',
|
||||||
|
'new_password_confirm': 'newpassword'
|
||||||
|
}, follow_redirects=True)
|
||||||
|
|
||||||
|
self.assertIn('Profile Page', r.data)
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordDisabledTest(SecurityTest):
|
||||||
|
|
||||||
|
AUTH_CONFIG = {
|
||||||
|
'SECURITY_CHANGEABLE': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_change_password_endpoint_is_404(self):
|
||||||
|
self.authenticate()
|
||||||
|
r = self.client.get('/change', follow_redirects=True)
|
||||||
|
self.assertEqual(404, r.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class TrackableTests(SecurityTest):
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
+239
-104
@@ -1,133 +1,268 @@
|
|||||||
import unittest
|
# -*- coding: utf-8 -*-
|
||||||
from example import app
|
|
||||||
|
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 SecurityTest(unittest.TestCase):
|
|
||||||
|
|
||||||
AUTH_CONFIG = None
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(SecurityTest, self).setUp()
|
|
||||||
|
|
||||||
self.app = self._create_app(self.AUTH_CONFIG or None)
|
|
||||||
self.app.debug = False
|
|
||||||
self.app.config['TESTING'] = True
|
|
||||||
|
|
||||||
self.client = self.app.test_client()
|
|
||||||
|
|
||||||
def _create_app(self, auth_config):
|
|
||||||
return app.create_sqlalchemy_app(auth_config)
|
|
||||||
|
|
||||||
def _get(self, route, content_type=None, follow_redirects=None):
|
|
||||||
return self.client.get(route, follow_redirects=follow_redirects,
|
|
||||||
content_type=content_type or 'text/html')
|
|
||||||
|
|
||||||
def _post(self, route, data=None, content_type=None, follow_redirects=True):
|
|
||||||
return self.client.post(route, data=data,
|
|
||||||
follow_redirects=follow_redirects,
|
|
||||||
content_type=content_type or 'text/html')
|
|
||||||
|
|
||||||
|
|
||||||
def authenticate(self, username, password, endpoint=None):
|
|
||||||
data = dict(username=username, password=password)
|
|
||||||
return self._post(endpoint or '/auth', data=data,
|
|
||||||
content_type='application/x-www-form-urlencoded')
|
|
||||||
|
|
||||||
def logout(self, endpoint=None):
|
|
||||||
return self._get(endpoint or '/logout', follow_redirects=True)
|
|
||||||
|
|
||||||
class DefaultSecurityTests(SecurityTest):
|
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):
|
def test_login_view(self):
|
||||||
r = self._get('/login')
|
r = self._get('/login')
|
||||||
assert 'Login Page' in r.data
|
self.assertIn('<h1>Login</h1>', r.data)
|
||||||
|
|
||||||
def test_authenticate(self):
|
def test_authenticate(self):
|
||||||
r = self.authenticate("matt", "password")
|
r = self.authenticate()
|
||||||
assert 'Home Page' in r.data
|
self.assertIn('Hello matt@lp.com', r.data)
|
||||||
|
|
||||||
def test_unprovided_username(self):
|
def test_unprovided_username(self):
|
||||||
r = self.authenticate("", "password")
|
r = self.authenticate("")
|
||||||
assert "Username not provided" in r.data
|
self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data)
|
||||||
|
|
||||||
def test_unprovided_password(self):
|
def test_unprovided_password(self):
|
||||||
r = self.authenticate("matt", "")
|
r = self.authenticate(password="")
|
||||||
assert "Password not provided" in r.data
|
self.assertIn(self.get_message('PASSWORD_NOT_PROVIDED'), r.data)
|
||||||
|
|
||||||
def test_invalid_user(self):
|
def test_invalid_user(self):
|
||||||
r = self.authenticate("bogus", "password")
|
r = self.authenticate(email="bogus@bogus.com")
|
||||||
assert "Specified user does not exist" in r.data
|
self.assertIn(self.get_message('USER_DOES_NOT_EXIST'), r.data)
|
||||||
|
|
||||||
def test_bad_password(self):
|
def test_bad_password(self):
|
||||||
r = self.authenticate("matt", "bogus")
|
r = self.authenticate(password="bogus")
|
||||||
assert "Password does not match" in r.data
|
self.assertIn(self.get_message('INVALID_PASSWORD'), r.data)
|
||||||
|
|
||||||
def test_inactive_user(self):
|
def test_inactive_user(self):
|
||||||
r = self.authenticate("tiya", "password")
|
r = self.authenticate("tiya@lp.com", "password")
|
||||||
assert "Inactive user" in r.data
|
self.assertIn(self.get_message('DISABLED_ACCOUNT'), r.data)
|
||||||
|
|
||||||
def test_logout(self):
|
def test_logout(self):
|
||||||
self.authenticate("matt", "password")
|
self.authenticate()
|
||||||
r = self.logout()
|
r = self.logout()
|
||||||
assert 'Home Page' in r.data
|
self.assertIsHomePage(r.data)
|
||||||
|
|
||||||
def test_unauthorized_access(self):
|
def test_unauthorized_access(self):
|
||||||
r = self._get('/profile', follow_redirects=True)
|
r = self._get('/profile', follow_redirects=True)
|
||||||
assert 'Please log in to access this page' in r.data
|
self.assertIn('<li class="info">Please log in to access this page.</li>', r.data)
|
||||||
|
|
||||||
def test_authorized_access(self):
|
def test_authorized_access(self):
|
||||||
self.authenticate("matt", "password")
|
self.authenticate()
|
||||||
r = self._get("/profile")
|
r = self._get("/profile")
|
||||||
assert 'profile' in r.data
|
self.assertIn('profile', r.data)
|
||||||
|
|
||||||
def test_valid_admin_role(self):
|
def test_valid_admin_role(self):
|
||||||
self.authenticate("matt", "password")
|
self.authenticate()
|
||||||
r = self._get("/admin")
|
r = self._get("/admin")
|
||||||
assert 'Admin Page' in r.data
|
self.assertIn('Admin Page', r.data)
|
||||||
|
|
||||||
def test_invalid_admin_role(self):
|
def test_invalid_admin_role(self):
|
||||||
self.authenticate("joe", "password")
|
self.authenticate("joe@lp.com")
|
||||||
r = self._get("/admin", follow_redirects=True)
|
r = self._get("/admin", follow_redirects=True)
|
||||||
assert 'Home Page' in r.data
|
self.assertIsHomePage(r.data)
|
||||||
|
|
||||||
def test_roles_accepted(self):
|
def test_roles_accepted(self):
|
||||||
for user in ("matt", "joe"):
|
for user in ("matt@lp.com", "joe@lp.com"):
|
||||||
self.authenticate(user, "password")
|
self.authenticate(user)
|
||||||
r = self._get("/admin_or_editor")
|
r = self._get("/admin_or_editor")
|
||||||
self.assertIn('Admin or Editor Page', r.data)
|
self.assertIn('Admin or Editor Page', r.data)
|
||||||
self.logout()
|
self.logout()
|
||||||
|
|
||||||
self.authenticate("jill", "password")
|
self.authenticate("jill@lp.com")
|
||||||
r = self._get("/admin_or_editor", follow_redirects=True)
|
r = self._get("/admin_or_editor", follow_redirects=True)
|
||||||
self.assertIn('Home Page', r.data)
|
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 ConfiguredSecurityTests(SecurityTest):
|
|
||||||
|
|
||||||
AUTH_CONFIG = {
|
|
||||||
'SECURITY_PASSWORD_HASH': 'bcrypt',
|
|
||||||
'SECURITY_USER_DATASTORE': 'custom_datastore_name',
|
|
||||||
'SECURITY_AUTH_URL': '/custom_auth',
|
|
||||||
'SECURITY_LOGOUT_URL': '/custom_logout',
|
|
||||||
'SECURITY_LOGIN_VIEW': '/custom_login',
|
|
||||||
'SECURITY_POST_LOGIN': '/post_login',
|
|
||||||
'SECURITY_POST_LOGOUT': '/post_logout'
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_login_view(self):
|
|
||||||
r = self._get('/custom_login')
|
|
||||||
assert "Custom Login Page" in r.data
|
|
||||||
|
|
||||||
def test_authenticate(self):
|
|
||||||
r = self.authenticate("matt", "password", endpoint="/custom_auth")
|
|
||||||
assert 'Post Login' in r.data
|
|
||||||
|
|
||||||
def test_logout(self):
|
|
||||||
self.authenticate("matt", "password", endpoint="/custom_auth")
|
|
||||||
r = self.logout(endpoint="/custom_logout")
|
|
||||||
assert 'Post Logout' in r.data
|
|
||||||
|
|
||||||
|
|
||||||
class MongoEngineSecurityTests(DefaultSecurityTests):
|
class MongoEngineSecurityTests(DefaultSecurityTests):
|
||||||
|
|
||||||
def _create_app(self, auth_config):
|
def _create_app(self, auth_config, **kwargs):
|
||||||
return app.create_mongoengine_app(auth_config)
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
from flask_security.utils import capture_registrations, \
|
||||||
|
capture_reset_password_requests, capture_signals
|
||||||
|
from flask_security.signals import user_registered, user_confirmed, \
|
||||||
|
confirm_instructions_sent, login_instructions_sent, \
|
||||||
|
password_reset, password_changed, reset_password_instructions_sent
|
||||||
|
from tests import SecurityTest
|
||||||
|
|
||||||
|
|
||||||
|
def compare_user(a, b):
|
||||||
|
"""Helper to compare two users."""
|
||||||
|
return a.id == b.id and a.email == b.email and a.password == b.password
|
||||||
|
|
||||||
|
|
||||||
|
class SignalTest(SecurityTest):
|
||||||
|
|
||||||
|
def _create_app(self, auth_config, **kwargs):
|
||||||
|
from tests.test_app.mongoengine import create_app
|
||||||
|
return create_app(auth_config, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterableSignalsTests(SignalTest):
|
||||||
|
|
||||||
|
AUTH_CONFIG = {
|
||||||
|
'SECURITY_CONFIRMABLE': True,
|
||||||
|
'SECURITY_REGISTERABLE': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_register(self):
|
||||||
|
e = 'dude@lp.com'
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self.register(e)
|
||||||
|
user = self.app.security.datastore.find_user(email='dude@lp.com')
|
||||||
|
self.assertEqual(mocks.signals_sent(), set([user_registered]))
|
||||||
|
calls = mocks[user_registered]
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
args, kwargs = calls[0]
|
||||||
|
self.assertTrue(compare_user(kwargs['user'], user))
|
||||||
|
self.assertIn('confirm_token', kwargs)
|
||||||
|
self.assertEqual(args[0], self.app)
|
||||||
|
|
||||||
|
def test_register_without_password(self):
|
||||||
|
e = 'dude@lp.com'
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self.register(e, password='')
|
||||||
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmableSignalsTests(SignalTest):
|
||||||
|
|
||||||
|
AUTH_CONFIG = {
|
||||||
|
'SECURITY_CONFIRMABLE': True,
|
||||||
|
'SECURITY_REGISTERABLE': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_confirm(self):
|
||||||
|
e = 'dude@lp.com'
|
||||||
|
with capture_registrations() as registrations:
|
||||||
|
self.register(e)
|
||||||
|
token = registrations[0]['confirm_token']
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self.client.get('/confirm/' + token, follow_redirects=True)
|
||||||
|
user = self.app.security.datastore.find_user(email='dude@lp.com')
|
||||||
|
self.assertTrue(mocks.signals_sent(), set([user_confirmed]))
|
||||||
|
calls = mocks[user_confirmed]
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
args, kwargs = calls[0]
|
||||||
|
self.assertEqual(args[0], self.app)
|
||||||
|
self.assertTrue(compare_user(kwargs['user'], user))
|
||||||
|
|
||||||
|
def test_confirm_bad_token(self):
|
||||||
|
e = 'dude@lp.com'
|
||||||
|
with capture_registrations():
|
||||||
|
self.register(e)
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self.client.get('/confirm/bogus', follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
def test_confirm_twice(self):
|
||||||
|
e = 'dude@lp.com'
|
||||||
|
with capture_registrations() as registrations:
|
||||||
|
self.register(e)
|
||||||
|
token = registrations[0]['confirm_token']
|
||||||
|
self.client.get('/confirm/' + token, follow_redirects=True)
|
||||||
|
self.logout()
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self.client.get('/confirm/' + token, follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set([user_confirmed]))
|
||||||
|
# TODO: is that the desired behaviour?
|
||||||
|
|
||||||
|
def test_resend_confirmation(self):
|
||||||
|
e = 'dude@lp.com'
|
||||||
|
self.register(e)
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self._post('/confirm', data={'email': e})
|
||||||
|
user = self.app.security.datastore.find_user(email='dude@lp.com')
|
||||||
|
self.assertEqual(mocks.signals_sent(), set([confirm_instructions_sent]))
|
||||||
|
calls = mocks[confirm_instructions_sent]
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
args, kwargs = calls[0]
|
||||||
|
self.assertTrue(compare_user(kwargs['user'], user))
|
||||||
|
self.assertEqual(args[0], self.app)
|
||||||
|
|
||||||
|
def test_send_confirmation_bad_email(self):
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self._post('/confirm', data=dict(email='bogus@bogus.com'))
|
||||||
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
|
||||||
|
class RecoverableSignalsTests(SignalTest):
|
||||||
|
|
||||||
|
AUTH_CONFIG = {
|
||||||
|
'SECURITY_RECOVERABLE': True,
|
||||||
|
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
|
||||||
|
'SECURITY_POST_FORGOT_VIEW': '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_reset_password_request(self):
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self._post('/reset', data=dict(email='joe@lp.com'),
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set([reset_password_instructions_sent]))
|
||||||
|
user = self.app.security.datastore.find_user(email='joe@lp.com')
|
||||||
|
calls = mocks[reset_password_instructions_sent]
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
args, kwargs = calls[0]
|
||||||
|
self.assertTrue(compare_user(kwargs['user'], user))
|
||||||
|
self.assertIn('token', kwargs)
|
||||||
|
self.assertEqual(args[0], self.app)
|
||||||
|
|
||||||
|
def test_reset_password(self):
|
||||||
|
with capture_reset_password_requests() as requests:
|
||||||
|
self._post('/reset', data=dict(email='joe@lp.com'),
|
||||||
|
follow_redirects=True)
|
||||||
|
token = requests[0]['token']
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
data = dict(password='newpassword', password_confirm='newpassword')
|
||||||
|
self._post('/reset/' + token, data, follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set([password_reset]))
|
||||||
|
user = self.app.security.datastore.find_user(email='joe@lp.com')
|
||||||
|
calls = mocks[password_reset]
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
args, kwargs = calls[0]
|
||||||
|
self.assertTrue(compare_user(kwargs['user'], user))
|
||||||
|
self.assertEqual(args[0], self.app)
|
||||||
|
|
||||||
|
def test_reset_password_invalid_emails(self):
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self._post('/reset', data=dict(email='nobody@lp.com'),
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
def test_reset_password_invalid_token(self):
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
data = dict(password='newpassword', password_confirm='newpassword')
|
||||||
|
self._post('/reset/bogus', data, follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeableSignalsTests(SignalTest):
|
||||||
|
|
||||||
|
AUTH_CONFIG = {
|
||||||
|
'SECURITY_CHANGEABLE': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_change_password(self):
|
||||||
|
self.authenticate('joe@lp.com')
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
with self.client as client:
|
||||||
|
client.post('/change',
|
||||||
|
data=dict(password='password',
|
||||||
|
new_password='newpassword',
|
||||||
|
new_password_confirm='newpassword',
|
||||||
|
csrf_token=self.csrf_token))
|
||||||
|
self.assertEqual(mocks.signals_sent(), set([password_changed]))
|
||||||
|
user = self.app.security.datastore.find_user(email='joe@lp.com')
|
||||||
|
calls = mocks[password_changed]
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
args, kwargs = calls[0]
|
||||||
|
self.assertTrue(compare_user(args[0], user))
|
||||||
|
self.assertEqual(kwargs['app'], self.app)
|
||||||
|
|
||||||
|
def test_change_password_invalid_password(self):
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self.client.post('/change',
|
||||||
|
data=dict(password='notpassword',
|
||||||
|
new_password='newpassword',
|
||||||
|
new_password_confirm='newpassword'),
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
def test_change_password_bad_password(self):
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self.client.post('/change',
|
||||||
|
data=dict(password='notpassword',
|
||||||
|
new_password='a',
|
||||||
|
new_password_confirm='a'),
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
def test_change_password_mismatch_password(self):
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self.client.post('/change',
|
||||||
|
data=dict(password='password',
|
||||||
|
new_password='newpassword',
|
||||||
|
new_password_confirm='notnewpassword'),
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordlessTests(SignalTest):
|
||||||
|
|
||||||
|
AUTH_CONFIG = {
|
||||||
|
'SECURITY_PASSWORDLESS': True
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_login_request_for_inactive_user(self):
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self._post('/login', data=dict(email='tiya@lp.com'),
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
def test_login_request_for_invalid_email(self):
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self._post('/login', data=dict(email='nobody@lp.com'),
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertEqual(mocks.signals_sent(), set())
|
||||||
|
|
||||||
|
def test_request_login_token_sends_email_and_can_login(self):
|
||||||
|
e = 'matt@lp.com'
|
||||||
|
|
||||||
|
with capture_signals() as mocks:
|
||||||
|
self._post('/login', data=dict(email=e), follow_redirects=True)
|
||||||
|
|
||||||
|
self.assertEqual(mocks.signals_sent(), set([login_instructions_sent]))
|
||||||
|
user = self.app.security.datastore.find_user(email='matt@lp.com')
|
||||||
|
calls = mocks[login_instructions_sent]
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
args, kwargs = calls[0]
|
||||||
|
self.assertTrue(compare_user(kwargs['user'], user))
|
||||||
|
self.assertIn('login_token', kwargs)
|
||||||
|
self.assertEqual(args[0], self.app)
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# -*- 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()
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# -*- 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()
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.pop(0)
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
|
||||||
|
from flask_peewee.db import Database
|
||||||
|
from peewee import *
|
||||||
|
from flask.ext.security import Security, UserMixin, RoleMixin, \
|
||||||
|
PeeweeUserDatastore
|
||||||
|
|
||||||
|
from tests.test_app import create_app as create_base_app, populate_data, \
|
||||||
|
add_context_processors
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(config, **kwargs):
|
||||||
|
app = create_base_app(config)
|
||||||
|
app.config['DATABASE'] = {
|
||||||
|
'name': 'example2.db',
|
||||||
|
'engine': 'peewee.SqliteDatabase'
|
||||||
|
}
|
||||||
|
db = Database(app)
|
||||||
|
|
||||||
|
class Role(db.Model, RoleMixin):
|
||||||
|
name = TextField(unique=True)
|
||||||
|
description = TextField(null=True)
|
||||||
|
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
email = TextField()
|
||||||
|
password = TextField()
|
||||||
|
last_login_at = DateTimeField(null=True)
|
||||||
|
current_login_at = DateTimeField(null=True)
|
||||||
|
last_login_ip = TextField(null=True)
|
||||||
|
current_login_ip = TextField(null=True)
|
||||||
|
login_count = IntegerField(null=True)
|
||||||
|
active = BooleanField(default=True)
|
||||||
|
confirmed_at = DateTimeField(null=True)
|
||||||
|
|
||||||
|
class UserRoles(db.Model):
|
||||||
|
""" Peewee does not have built-in many-to-many support, so we have to
|
||||||
|
create this mapping class to link users to roles."""
|
||||||
|
user = ForeignKeyField(User, related_name='roles')
|
||||||
|
role = ForeignKeyField(Role, related_name='users')
|
||||||
|
name = property(lambda self: self.role.name)
|
||||||
|
description = property(lambda self: self.role.description)
|
||||||
|
|
||||||
|
@app.before_first_request
|
||||||
|
def before_first_request():
|
||||||
|
for Model in (Role, User, UserRoles):
|
||||||
|
Model.drop_table(fail_silently=True)
|
||||||
|
Model.create_table(fail_silently=True)
|
||||||
|
populate_data(app.config.get('USER_COUNT', None))
|
||||||
|
|
||||||
|
app.security = Security(app, datastore=PeeweeUserDatastore(db, User, Role, UserRoles), **kwargs)
|
||||||
|
|
||||||
|
add_context_processors(app.security)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
create_app({}).run()
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# -*- 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()
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
{% endif -%}
|
{% endif -%}
|
||||||
<li>
|
<li>
|
||||||
{%- if current_user.is_authenticated() -%}
|
{%- if current_user.is_authenticated() -%}
|
||||||
<a href="{{ url_for('auth.logout') }}">Log out</a>
|
<a href="{{ url_for('security.logout') }}">Log out</a>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
<a href="{{ url_for('login') }}">Log in</a>
|
<a href="{{ url_for('security.login') }}">Log in</a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CUSTOM FORGOT PASSWORD
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CUSTOM LOGIN USER
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CUSTOM REGISTER USER
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CUSTOM RESET PASSWORD
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CUSTOM SEND CONFIRMATION
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CUSTOM SEND LOGIN
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{% include "_messages.html" %}
|
||||||
|
{% include "_nav.html" %}
|
||||||
|
<h1>Register</h1>
|
||||||
|
<form action="{{ url_for_security('register') }}" method="POST" name="register_form">
|
||||||
|
{{ register_user_form.hidden_tag() }}
|
||||||
|
{{ register_user_form.email.label }} {{ register_user_form.email }}<br/>
|
||||||
|
{{ register_user_form.password.label }} {{ register_user_form.password }}<br/>
|
||||||
|
{{ register_user_form.password_confirm.label }} {{ register_user_form.password_confirm }}<br/>
|
||||||
|
{{ register_user_form.submit }}
|
||||||
|
</form>
|
||||||
|
<p>{{ content }}</p>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{% include "_messages.html" %}
|
||||||
|
{% include "_nav.html" %}
|
||||||
|
<h1>You are not allowed to access the requested resouce</h1>
|
||||||
+63
-19
@@ -1,44 +1,88 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import flask_security
|
|
||||||
from flask_security import RoleMixin, UserMixin, AnonymousUser
|
from flask_security import RoleMixin, UserMixin, AnonymousUser
|
||||||
|
from flask_security.datastore import Datastore, UserDatastore
|
||||||
|
|
||||||
|
|
||||||
class Role(RoleMixin):
|
class Role(RoleMixin):
|
||||||
def __init__(self, name, description=None):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin):
|
class User(UserMixin):
|
||||||
def __init__(self, username, email, roles):
|
def __init__(self, email, roles):
|
||||||
self.username = username
|
|
||||||
self.email = email
|
self.email = email
|
||||||
self.roles = roles
|
self.roles = roles
|
||||||
|
|
||||||
# set the models or we'll get errors
|
|
||||||
flask_security.User = User
|
|
||||||
flask_security.Role = Role
|
|
||||||
|
|
||||||
admin = Role('admin')
|
admin = Role('admin')
|
||||||
admin2 = Role('admin')
|
admin2 = Role('admin')
|
||||||
editor = Role('editor')
|
editor = Role('editor')
|
||||||
|
|
||||||
user = User('matt', 'matt@lp.com', [admin, editor])
|
user = User('matt@lp.com', [admin, editor])
|
||||||
|
|
||||||
|
|
||||||
class SecurityEntityTests(unittest.TestCase):
|
class SecurityEntityTests(unittest.TestCase):
|
||||||
|
|
||||||
def test_role_mixin_equal(self):
|
def test_role_mixin_equal(self):
|
||||||
self.assertEqual(admin, admin2)
|
self.assertEqual(admin, admin2)
|
||||||
|
|
||||||
def test_role_mixin_not_equal(self):
|
def test_role_mixin_not_equal(self):
|
||||||
self.assertNotEqual(admin, editor)
|
self.assertNotEqual(admin, editor)
|
||||||
|
|
||||||
def test_user_mixin_has_role_with_string(self):
|
def test_user_mixin_has_role_with_string(self):
|
||||||
self.assertTrue(user.has_role('admin'))
|
self.assertTrue(user.has_role('admin'))
|
||||||
|
|
||||||
def test_user_mixin_has_role_with_role_obj(self):
|
def test_user_mixin_has_role_with_role_obj(self):
|
||||||
self.assertTrue(user.has_role(Role('admin')))
|
self.assertTrue(user.has_role(Role('admin')))
|
||||||
|
|
||||||
def test_anonymous_user_has_no_roles(self):
|
def test_anonymous_user_has_no_roles(self):
|
||||||
au = AnonymousUser()
|
au = AnonymousUser()
|
||||||
self.assertEqual(0, len(au.roles))
|
self.assertEqual(0, len(au.roles))
|
||||||
self.assertFalse(au.has_role('admin'))
|
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))
|
||||||
|
|||||||
Reference in New Issue
Block a user