Compare commits

...
328 Commits
Author SHA1 Message Date
Matt Wright c7d0ea9cce Add additional item to CHANGES 2014-10-13 13:47:35 -04:00
Matt Wright 94c7c09dc2 Bump version number to 1.7.4 2014-10-13 13:38:44 -04:00
Matt Wright f6405797f1 Update CHANGES 2014-10-13 13:38:16 -04:00
Matt Wright 2cffb6634d Fix case sensitivity when searching for users by email address. Fixes #323. 2014-10-13 13:34:11 -04:00
Matt Wright 824a52b883 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2014-10-13 13:25:08 -04:00
Matt Wright fe7e4c4afc Add additional steps to the test_change_hash_type and make change to verify_and_update_password to make the test pass. Fixes #328. 2014-10-13 13:25:01 -04:00
Matt Wright 591bc27a5e Merge pull request #318 from boydgreenfield/develop
Prevent open redirects when a malformed URL is passed to ?next=
2014-10-13 11:54:32 -04:00
Nick Greenfield 5bc37add88 Update to use (url_next.netloc or url_next.scheme) in the validate_redirect_url open redirect patch. 2014-10-01 09:49:00 -07:00
Nick Greenfield 8b036f2a3e Prevent open redirects when a malformed URL is passed to ?next=
Example: "/login?next=http:///google.com" (note 3rd slash)
2014-09-26 11:08:58 -07:00
Matt Wright 76ad77a233 Compare a string (not bytes) for PY3 support 2014-09-17 11:39:19 -04:00
Matt Wright 679cee7969 Add default/global context processor. Fixes #306 2014-09-17 11:27:44 -04:00
Matt Wright 3d7b97ac31 Forgot password form should not validate if user has not confirmed their email address yet. Fixes #298 2014-09-17 11:21:31 -04:00
Matt Wright 3a0af73231 Merge pull request #281 from sjml/patch-1
Fixing typo in documentation
2014-09-17 10:03:53 -04:00
Matt Wright 3458391791 Merge pull request #284 from tony/patch-1
Typo
2014-09-17 10:02:27 -04:00
Matt Wright 37908ca335 Merge pull request #303 from graup/patch-1
Docs: Fixed typo and made punctuation more consistent.
2014-09-17 10:02:16 -04:00
Matt Wright f83cd56ff2 Merge pull request #311 from moorereason/develop
Fix small issues in docs
2014-09-17 10:01:57 -04:00
Cameron Moore c10ec29fe5 Fix small issues in docs 2014-09-07 21:37:33 -05:00
Matt Wright d2fe7aefcb Merge pull request #307 from mickey06/develop
Save changes to db after removal of role from user
2014-08-29 12:19:26 -04:00
Matt Wright 708ddeb6c4 Merge pull request #309 from gregeinfrank/develop
Fix two typos in docstrings
2014-08-29 12:15:20 -04:00
Greg Einfrank 2aeee348d4 Fix two typos in docstrings 2014-08-27 23:37:58 -04:00
Khalil El Kouhen 52b177cd2e Save changes to db after removal of role from user 2014-08-26 16:43:23 +01:00
Paul Grau dab2fc8c8b Docs: Fixed typo and made punctuation more consistent. 2014-08-19 18:01:03 +09:00
Matt Wright 31e3ab5470 Merge pull request #289 from scollinson/fix_menu_typo
Fix a typo in the menu template
2014-08-13 15:44:20 -04:00
Matt Wright 6bdcd2930d Merge pull request #278 from dokterbob/post_register_redirect
Enable ‘next’ redirection after registration.
2014-07-29 13:23:42 -04:00
Sam Collinson 1076887900 fix typo in menu template 2014-07-21 19:07:12 +12:00
Tony Narlock 285fe888da Typo
s/tempalte/template
2014-07-16 14:35:47 -07:00
Shane Liesegang bf139847cb Fixing typo in documentation
Just a small usage error. :)
2014-07-11 23:45:00 -07:00
Mathijs de Bruin 15c9ef1d07 Attempt to fix tests. 2014-07-03 15:48:55 +02:00
Mathijs de Bruin d1ec38ea18 Keep value of ‘next’ in menu links.
This allows moving from the login to registration (and vice versa) while keeping the redirection URL, enabling a much smoother user experience.
2014-07-03 15:22:56 +02:00
Mathijs de Bruin 577a3a8110 Enable ‘next’ redirection after registration. 2014-07-03 15:21:03 +02:00
Matt Wright 7b474efd7a Bump version number to 1.7.3 2014-06-10 13:05:24 -04:00
Matt Wright f12bc17e7e Merge branch 'develop' 2014-06-10 13:05:12 -04:00
Matt Wright 999f882f61 Update CHANGES 2014-06-10 13:05:06 -04:00
Matt Wright a140c01b57 Add python3.4 to travis and tox 2014-06-10 12:40:12 -04:00
Matt Wright 76cf3eaf6a Do not expose user info in /reset responses. Fixes #249 2014-06-10 12:24:19 -04:00
Matt Wright a6b5d3053c Use safe_str_cmp when evaluating tokens. Fixes #252 2014-06-10 12:14:58 -04:00
Matt Wright 0a48997fdd Improve encoding of strings. Addresses #231 and #253 2014-06-10 11:47:35 -04:00
Matt Wright 96f1b3e0d1 Fix tests to pass python 3 2014-06-10 11:12:40 -04:00
Matt Wright 0facdaacd9 Make validate_redirect_url smarter. Fixes #261. 2014-06-10 10:48:56 -04:00
Matt Wright 9cadf855a4 Adjust POST_LOGIN_VIEW and POST_LOGOUT_VIEW test 2014-06-10 10:42:28 -04:00
Matt Wright f387759cee Bump version number to 1.7.2 2014-05-06 14:05:27 -04:00
Matt Wright faa9f65596 Update CHANGES 2014-05-06 14:04:44 -04:00
Matt Wright 3d5b37cef2 Polish for static analysis 2014-05-06 13:14:25 -04:00
Matt Wright d185407ac4 Add mock X-Forwarded-For header in trackable tests 2014-05-06 13:14:14 -04:00
Matt Wright 58b7fa8e2e Check X-Forwarded-For header value when tracking IP addresses. Fixes #234 2014-05-06 12:35:50 -04:00
Matt Wright 721b985bc4 Something about a bug... 2014-05-06 11:59:28 -04:00
Matt Wright b08d7c2085 Clean up tests with some pytest marking 2014-05-06 11:58:39 -04:00
Matt Wright fff71e3e04 Polish 2014-05-06 07:57:00 -04:00
Matt Wright 546680a94b Polish 2014-03-14 15:33:25 -04:00
Matt Wright 00e121c869 Remove duplicated line 2014-03-14 15:32:39 -04:00
Matt Wright 7098a6757c Update build files 2014-03-14 15:30:30 -04:00
Matt Wright 510d1356a2 A bunch of adjustments to satisfy existing tests and even some new ones 2014-03-14 15:26:53 -04:00
Matt Wright e5111dbb0c Add moar tests! 2014-03-14 15:26:20 -04:00
Matt Wright 44b91a351a Update gitignore 2014-03-13 18:33:22 -04:00
Matt Wright 4b58a00abd Update tox.ini and MANIFEST.in 2014-03-13 18:32:39 -04:00
Matt Wright f3014d01df Convert all tests to use pytest. Phew! 2014-03-13 18:28:25 -04:00
Matt Wright ed645b51f2 Fix confirmation endpoint to flash an "already confirmed" message if user attempts to confirm after already being confirmed 2014-03-13 17:18:39 -04:00
Matt Wright bc60c021a3 Fix password_changed signal to behave like the other signals. Fixes #222 2014-03-13 09:35:54 -04:00
Matt Wright 9f00613906 Merge pull request #225 from nfvs/fix_emails_with_celery_link
Fixed Celery link in "Emails with Celery".
2014-03-13 09:09:39 -04:00
Nuno Santos 0f872af03a Fixed Celery link in "Emails with Celery". 2014-02-27 16:45:25 +01:00
Matt Wright 1d3a75d77b Merge pull request #223 from ahtik/issue-verify_password
verify_password in 1.7.0 was no longer behaving like in 1.6.9
2014-02-20 10:10:44 -05:00
Ahti Kitsik 1395df334e Changing verify_password so it works like verify_and_update_password. Currently verify_password was not only creating a hmac hash but also encrypting (encrypt_password is first hmac-signing and then encrypting).
Removed unneccessary and wrong tests.
2014-02-20 16:46:49 +02:00
Matt Wright 0268a2d568 Merge pull request #217 from swarmer/logout-fix
Make logout view do nothing if the user is already logged out
2014-02-04 07:24:41 -08:00
Anton Barkovsky 09f6f64e5b Make logout view do nothing if the user is already logged out 2014-02-04 16:51:00 +03:00
Matt Wright 8e8d581f0d Add images to README.rst 2014-01-24 14:17:07 -05:00
Matt Wright 65b454ca7e Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2014-01-24 14:09:46 -05:00
Matt Wright 2006d2ae3d Add coverage and coveralls to .travis.yml 2014-01-24 14:09:39 -05:00
Matt Wright c0673c1239 Merge pull request #180 from pythonhub/develop
Allow overwritten on render_template to use Flask-Themes based app #179
2014-01-24 08:15:00 -08:00
Bruno Rocha 922a038d28 Merge https://github.com/mattupstate/flask-security into develop 2014-01-24 14:06:03 -02:00
Matt Wright 668b4ee651 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2014-01-24 10:32:09 -05:00
Matt Wright f854c24094 Merge pull request #205 from HereLabsInc/catch_decoding_errors
catch possible TypeError and ValueError from serializer
2014-01-24 07:31:51 -08:00
Matt Wright d314e54680 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2014-01-24 10:25:01 -05:00
Matt Wright 9400032a1f Merge pull request #214 from kleink/develop
Update script.py for Python 2's print statement vs. Python 3's function.
2014-01-24 07:24:55 -08:00
Matt Wright 2e57734d1f Add @anonymous_user_required to register endpoint. Fixes #212 2014-01-24 10:23:42 -05:00
Klaus Klein c658ee4500 Update script.py for Python 2's print statement vs. Python 3's function. 2014-01-24 10:21:41 +01:00
Jameson 45c8951877 passes tests 2014-01-24 04:41:41 +00:00
Jameson b4d1a7c921 update to make test run red 2014-01-23 23:33:11 +00:00
Jameson f47fce9365 add test to show TypeError can occur 2014-01-23 23:26:18 +00:00
Jameson b2174bf035 catch possible TypeError and ValueError from serializer 2014-01-23 23:26:18 +00:00
Matt Wright 316d945d96 Bump version number to 1.7.1 2014-01-14 10:41:53 -05:00
Matt Wright 6eb77c06ad Update CHANGES 2014-01-14 10:41:38 -05:00
Matt Wright 35fd08772b Add configured password hash test back and fix bug with checking passwords 2014-01-14 10:34:57 -05:00
Matt Wright 76fc578cf5 Bump version number to 1.7.0 2014-01-10 14:40:24 -05:00
Matt Wright 91ee026ff7 Merge branch 'master' into develop 2014-01-10 14:39:22 -05:00
Matt Wright e3b74acc5a Update CHANGES 2014-01-10 14:38:58 -05:00
Matt Wright 968f85a39c Update CHANGES 2014-01-10 14:03:45 -05:00
Matt Wright 68ecc7bb10 Update test dependencies in setup.py 2014-01-07 16:40:59 -05:00
Matt Wright ac2dfe964b Update dependencies in setup.py 2014-01-07 16:40:35 -05:00
Matt Wright 730b86fbc7 Add more python envs 2014-01-07 16:37:07 -05:00
Matt Wright 850b0e714b Additional test fixes 2014-01-07 16:31:26 -05:00
Matt Wright 8d2815798f WIP 2013-12-24 12:38:05 -05:00
Bruno Rocha 01a4199a16 Merge https://github.com/mattupstate/flask-security into develop
Conflicts:
	flask_security/views.py
2013-12-23 13:55:13 -02:00
Bruno Rocha 910d80d511 Fix version to be able to merge 2013-12-23 13:46:14 -02:00
Matt Wright f7b645005e work in progress 2013-12-20 13:39:41 -05:00
Matt Wright afaf6c7d62 Polish 2013-12-20 13:38:44 -05:00
Matt Wright f1447b2adc Work in progress 2013-12-19 16:12:29 -05:00
Matt Wright d95a8c9364 Nope, nevermind. Dont use tox for tests. 2013-12-19 15:11:50 -05:00
Matt Wright 06ce1c68fd Use tox for running tests 2013-12-19 15:10:20 -05:00
Matt Wright c302fb8e30 Update .gitignore 2013-12-19 14:45:53 -05:00
Matt Wright d88299fc9b Add test to check SECURITY_LOGIN_WITHOUT_CONFIRMATION feature 2013-12-19 14:40:43 -05:00
Matt Wright ce439b7fec Merge pull request #193 from nickretallack/develop
Fix attempts to log in without a password, or without confirming your email
2013-12-19 11:39:47 -08:00
Matt Wright af8e9f7ca5 Render auth token when registering a use with JSON 2013-12-19 14:26:47 -05:00
Matt Wright 6e461a81bf Add AUTHORS file 2013-12-19 14:09:37 -05:00
Matt Wright d7d090afc6 Polish 2013-12-19 14:00:44 -05:00
Matt Wright 11b8222ec5 Add SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL config option to optionally send password reset notice emails. Addresses #199 2013-12-19 13:56:34 -05:00
Matt Wright be8448a7cf Merge pull request #197 from kelvinhammond/patch-1
Update NextFormMixin.validate_next to check if next was even specified
2013-12-19 10:46:45 -08:00
Matt Wright fe170e6eb3 Fixes #196 2013-12-19 13:45:14 -05:00
Matt Wright c1ff98cdf3 Document . Addresses #194 2013-12-19 13:35:03 -05:00
Matt Wright b88b5bf2c3 Merge pull request #183 from NeuralGlue/master
Update features.rst
2013-12-19 10:17:26 -08:00
Matt Wright f0e60dd2d1 Merge pull request #177 from srijan/patch-1
Use get_json instead of json
2013-12-19 10:09:20 -08:00
Matt Wright 095ade8bbc Merge pull request #176 from lojack/develop
this fixes #175
2013-12-19 10:08:51 -08:00
Matt Wright 615bc00c26 Add flask_security.utils documentation for selected functions. Addressed #169 2013-12-19 12:57:11 -05:00
Matt Wright be9af2e107 Merge branch 'develop' into configurable_login_fields 2013-12-19 12:16:02 -05:00
Matt Wright 2f21523d47 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-12-19 12:10:09 -05:00
kelvinhammond 986a48c5e0 Update forms.py
Fixed: If login.next is not passed form throws error
2013-12-14 13:01:41 -06:00
Nick Retallack 1596ef75d4 login_without_confirmation should allow you to log in without confirmation 2013-12-11 03:12:29 -08:00
Nick Retallack 292f89c204 Prevent it from exploding if you try to log in with a user who has no password in the database. 2013-12-11 03:08:58 -08:00
Matt Wright 1b8a4bcab4 Merge pull request #187 from surfstudio/change_password_config_fix
Added a config parameter for change password template
2013-11-25 08:34:15 -08:00
Alexander Sukharev 4549a02219 Added a doc entry for parameter from the previous commit 2013-11-22 18:41:08 +04:00
sanek e0881ed6a0 Added a config parameter for change password template 2013-11-22 18:02:35 +04:00
Charles Young b1ae7b4352 Update features.rst
Changed HTTP header name to reflect reality in code
2013-11-20 10:58:32 -08:00
Bruno Rocha 6494275c8c typo 2013-11-16 22:01:49 -02:00
Bruno Rocha aa80198047 Try to fix a problem with version installed 2013-11-16 21:58:27 -02:00
Bruno Rocha 23b128bcbd Allow overwritten on render_template to use Flask-Themes based app 2013-11-07 04:57:54 -02:00
Srijan Choudhary e1c7ec303f Use get_json instead of json
The `request.json` method now calls `get_json`, which raises `BadRequest` if there is no json data or some error with it. So, it cannot be directly used as a check for presence of json data. This code currently returns a bad request if content type is `application/json` but json data is empty.

https://github.com/mitsuhiko/flask/blob/master/flask/wrappers.py#L110
2013-10-31 10:19:12 +05:30
Robert Clark b6007cb515 this fixes #175 2013-10-30 18:04:42 -04:00
Matt Wright 1ae6bc3cf1 Add the ability to specify additional fields on the user model that can be used for logging in. 2013-10-16 14:00:36 -04:00
Matt Wright 80f5fa8dbb Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-10-16 11:16:17 -04:00
Matt Wright 75794d5527 Merge pull request #158 from kleschenko/patch-2
removed unnecessary utcnow call
2013-10-16 08:16:09 -07:00
Matt Wright 9999325ffb Show an error if a user tries to change their password and its the same as before. Fixes #160 2013-10-16 11:15:17 -04:00
Matt Wright b1ac2598c0 Be sure to save user model when using add_role_to_user method for MongoEngineUserDatastore. Fixes #170 2013-10-16 10:55:56 -04:00
Matt Wright cfd0efb91d Fix #167 2013-10-03 10:10:06 -04:00
Matt Wright 7808870fcd Merge pull request #163 from joehand/develop
Add option to disable password change email.
2013-10-03 07:05:54 -07:00
Joe Hand 55ffe2563e Update test names and add docstring. 2013-10-02 08:24:27 -06:00
Matt Wright a054241144 Merge pull request #168 from iurisilvio/patch-1
Fix typo: algorith -> algorithm
2013-10-02 07:11:59 -07:00
Iuri de Silvio a112ea06bb Fix typo: algorith -> algorithm 2013-09-27 21:04:51 -03:00
Joe Hand 20c16107e8 Add test for 'SECURITY_SEND_PASSWORD_CHANGE_EMAIL' configuration 2013-09-22 09:55:07 -06:00
Matt Wright 7121ebff22 Merge pull request #159 from martinmaillard/fix-typo-docs
Fix email templates paths in docs
2013-09-18 06:38:36 -07:00
Joe Hand 389d944aaf Add option to disable password change email. 2013-09-15 18:38:36 -06:00
Matt Wright 73ff1c00b3 Fixes #146 2013-09-11 01:10:16 -04:00
Kostyantyn Leschenko 5a780ce741 simplified get_max_age return value 2013-09-05 18:12:31 +03:00
Kostyantyn Leschenko 20eed621ed Merge branch 'develop' into patch-2 2013-09-05 18:10:05 +03:00
Matt Wright 0103bf4269 Remove pypy tests 2013-09-03 13:08:13 -04:00
Matt Wright 13422e046f Use bcrypt instead of py-bcrypt 2013-09-03 13:00:03 -04:00
Matt Wright 6b3d65d6d6 Fix flask-login test setting 2013-09-03 12:20:56 -04:00
Matt Wright 1e958115e1 Fix tests 2013-09-03 11:55:13 -04:00
Martin Maillard b87c6fe1aa Fix email templates path in docs 2013-09-03 16:45:44 +02:00
Matt Wright 1d8a5f7c2e Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-09-03 10:25:38 -04:00
Matt Wright 40409af1dd Fix documentation issue #145 2013-09-03 10:22:57 -04:00
Matt Wright af0f0c202f Update MANIFEST.in 2013-09-03 10:15:39 -04:00
Kostyantyn Leschenko 9ddb909342 removed unnecessary utcnow call
is this code required here?
2013-09-03 15:32:49 +03:00
Matt Wright 07bb00f75c Bump version number to 1.6.9 2013-08-20 12:35:29 -04:00
Matt Wright 9a10be5c90 Merge branch 'develop' 2013-08-20 12:35:12 -04:00
Matt Wright 829bb135d7 Update CHANGES 2013-08-20 12:35:08 -04:00
Matt Wright ab64406b6c Merge pull request #152 from AleshGood/develop
Fix imports from Flask-WTF
2013-08-20 09:31:32 -07:00
Alexey Poryadin 6f68f1d540 Fix imports from Flask-WTF 2013-08-15 12:46:09 +04:00
Matt Wright 510c7bfa2a Merge pull request #151 from lupiter/fix-peewee-delete-role
Commit deleting role from user (peewee).
2013-08-12 06:58:47 -07:00
Matt Wright 67d8134e3a Merge pull request #150 from vmalloc/fix-147
#147: only get by email_or_id if it is numeric
2013-08-12 06:58:35 -07:00
Wise, Catherine (ICT Centre, Acton) 7bef96c261 Commit deleting role from user (peewee). 2013-08-12 15:07:13 +10:00
Rotem YaariandRotem Yaari 7d6b42e960 #147: only get by email_or_id if it is numeric 2013-08-11 20:15:06 +03:00
Matt Wright e773232e5a Merge branch 'develop' 2013-08-01 16:28:18 -04:00
Matt Wright f401eac496 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-08-01 16:28:11 -04:00
Matt Wright f6b532103d Bump version number to 1.6.8 2013-08-01 16:27:32 -04:00
Matt Wright 27a4bbac6a Update CHANGES 2013-08-01 16:27:25 -04:00
Matt Wright 0cf7d563dc Update changes 2013-08-01 16:24:46 -04:00
Matt Wright 7c906de427 When logging in user lookups on email should be case insensitive 2013-08-01 16:22:28 -04:00
Matt Wright 9c03884441 Merge pull request #140 from joeyespo/ignore-validation-errors
Ignore validation errors in find_user
2013-07-25 14:28:01 -07:00
Joe Esposito 4ed4579738 Ignore validation errors in find_user 2013-07-24 16:08:19 -04:00
Matt Wright 156ccaecc1 Attempting a fix to get tests to pass with travis. Very odd... 2013-07-24 11:35:04 -04:00
Matt Wright a5482922f3 Merge pull request #139 from codebutler/develop
Use token_callback for checking tokens.
2013-07-24 07:01:29 -07:00
Eric Butler d3cfddfcac Use token_callback for checking tokens.
Fixes error if user is not found.
2013-07-23 15:37:28 -07:00
Matt Wright 312a527498 PEP8 and general clean up on tests 2013-07-22 12:42:20 -04:00
Matt Wright d87765fc3b PEP8 polish 2013-07-22 12:37:44 -04:00
Matt Wright d05e699a15 Clean up 2013-07-22 12:19:29 -04:00
Matt Wright e7855488df Docs polish 2013-07-22 11:44:19 -04:00
Matt Wright 01bdc0d356 A little more documentation. Fixes #131 2013-07-22 11:35:45 -04:00
Matt Wright 5e2a016bcd Update docs. Fixes #137 2013-07-22 11:26:11 -04:00
Matt Wright 0f46f35981 Bump version number to 1.6.7 2013-07-11 14:58:46 -04:00
Matt Wright c0d9eecf10 Set release date. 2013-07-11 14:58:36 -04:00
Matt Wright 04bb2c4041 Update CHANGES 2013-07-11 14:54:22 -04:00
Matt Wright 8eeb832d2e Conditionally logout the current user when confirming an email address to prevent unnecessary code/signals to be fired. Fixes #133 2013-07-11 14:50:21 -04:00
Matt Wright 8f760aadbd Merge pull request #134 from rxl/email_confirmation_fix
Email confirmation fix
2013-07-11 11:21:05 -07:00
rxl 03d27cd600 add logout_user() to the beginning of confirm_email() 2013-07-11 14:14:50 -04:00
rxl d30a27b3bb remove '@anonymous_user_required' from confirm_email() 2013-07-11 14:14:28 -04:00
Matt Wright 78903fa2e5 Make password length message configurable. 2013-07-02 10:36:22 -04:00
Matt Wright 514de64303 Bump version number to 1.6.6 2013-06-28 17:24:50 -04:00
Matt Wright bad63265f8 Update CHANGES 2013-06-28 17:24:40 -04:00
Matt Wright 66a9dcd2e6 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-06-28 17:23:36 -04:00
Matt Wright df1647f1f9 Fix install_requires in setup.py 2013-06-28 17:23:16 -04:00
Matt Wright 89ecded480 Merge pull request #128 from vkotovv/develop
Fix issue tracker link
2013-06-26 08:04:10 -07:00
Vadim Kotov 2b35b37a66 Update sidebarintro.html
Fixed link to the issue tracker
2013-06-26 18:30:44 +04:00
Matt Wright 4d8a813004 Bump version number to 1.6.5 2013-06-20 16:01:36 -04:00
Matt Wright 06312ef50f Fix typo with _external parameter in confirmable.py. Fixes #126 2013-06-20 16:01:22 -04:00
Matt Wright c3ad5b2fa6 Take --use-mirrors out of pip calls in .travis.yml 2013-06-18 15:53:32 -04:00
Matt Wright 637fc913cd Take Flask-Mail out of .travis.yml 2013-06-18 15:32:32 -04:00
Matt Wright e00522f331 Add SECURITY_CHANGEABLE to docs. Fixes #115 2013-06-18 15:05:32 -04:00
Matt Wright 3d34d87a97 Bump version number to 1.6.4 2013-06-18 15:00:41 -04:00
Matt Wright 461ace9303 Update docs 2013-06-18 15:00:34 -04:00
Matt Wright d19bb98abd Version 1.6.4 changes. Refer to CHANGES for updates. Fixes #123 #121 #120 $119 2013-06-18 14:56:12 -04:00
Matt Wright c24af5ca6e Whitespace! 2013-05-28 11:11:37 -04:00
Matt Wright 26045fc4dc Use the _external parameter when generating links for emails 2013-05-28 11:11:19 -04:00
Matt Wright bf260d4b7e Add optional next parameter to registration endpoint. Fixes #117. 2013-05-28 11:01:42 -04:00
Matt Wright db56ff74a9 Bump version number to 1.6.3 2013-05-08 12:29:48 -04:00
Matt Wright e03efe0b34 Update CHANGES 2013-05-08 12:29:35 -04:00
Matt Wright c587988a3a Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-05-03 12:14:14 -04:00
Matt Wright f2d5245bd8 Import check to account for new version of MongoEngine 2013-05-03 12:13:58 -04:00
Matt Wright 97e1960abd Merge pull request #112 from poundifdef/login_flash
Make flask-login respect SECURITY_FLASH_MESSAGES
2013-04-14 13:40:57 -07:00
Jay Goel e749b77ca7 Make flask-login respect SECURITY_FLASH_MESSAGES 2013-04-14 16:37:23 -04:00
Matt Wright 6f3c163ee7 Merge pull request #111 from joshpurvis/issue110
Changed has_role to accept strings with mongoengine. Fixes #110
2013-04-14 10:05:34 -07:00
Josh Purvis 3b81ec57ea Changed has_role to accept strings with mongoengine. Fixes #110 2013-04-13 15:11:56 -04:00
Matt Wright 38874433c7 Add tests for Peewee support 2013-04-04 18:09:55 -04:00
Matt Wright 4eda3e756f PEP8 polish 2013-04-04 16:39:50 -04:00
Matt Wright 4815b1afed Make find_user method for MongoEngineUserDatastore add contraints to query 2013-04-04 15:50:46 -04:00
Matt Wright 99ac732d10 Bump version number to 1.6.2 2013-04-04 10:24:03 -04:00
Matt Wright e8b0c62818 Update CHANGES and a little polish 2013-04-04 10:23:51 -04:00
Matt Wright 1108f1670c Merge pull request #104 from rodcloutier/http_auth_fix
Fixed http_auth when authorization is not provided in header
2013-04-04 07:21:27 -07:00
Rodrigue Cloutier 3575a2df18 Fixed http_auth when authorization is not provided in header 2013-04-03 21:29:04 -04:00
Matt Wright c84c485493 Bump version number to 1.6.1 2013-04-03 11:07:36 -04:00
Matt Wright 8298ac461e Update CHANGES 2013-04-03 11:07:16 -04:00
Matt Wright 105d04768e Merge pull request #103 from immon/issue94
sending signals fixed
2013-04-03 08:04:49 -07:00
Paweł Krześniak f1cca43d9c sending signals fixed 2013-04-03 12:36:53 +02:00
Matt Wright e8352fa265 Merge pull request #102 from andrewcamenga/develop
corrected link for Flask-WTF
2013-03-29 08:53:38 -07:00
Andrew J. Camenga 37d84ddd73 corrected link for Flask-WTF 2013-03-29 08:37:51 -04:00
Matt Wright 6f9869e9c2 import auth_required into top level package 2013-03-27 17:20:31 -04:00
Matt Wright abc061ba46 Change .travis.yml 2013-03-19 14:11:23 -04:00
Matt Wright 95c80e5677 See if pypy works 2013-03-19 13:28:42 -04:00
Matt Wright ba1758e5c7 Bump version number to 1.6.0 2013-03-13 14:06:34 -04:00
Matt Wright 36198c1993 Update CHANGE 2013-03-13 14:06:08 -04:00
Matt Wright 8708fd8514 Update form messaging to be more flexible. Fixes #80 2013-03-13 13:40:35 -04:00
Matt Wright 44a320ee74 Fix a failling test 2013-03-13 12:28:26 -04:00
Matt Wright 520b8ecef4 Show an invalid confirmation token message if a user attempts to confirm their account after it has been deleted. Fixes #93 2013-03-13 12:27:26 -04:00
Matt Wright 34aa43ead3 Merge pull request #100 from chrishaines/develop
Make subdomain configurable
2013-03-13 09:10:16 -07:00
Matt Wright 8ecc3b9a78 Add user to request context for http basic and token auth 2013-03-13 12:09:28 -04:00
Chris Haines c0d1d0566f Make subdomain configurable 2013-03-13 00:13:54 -04:00
Matt Wright 2e01cab3f8 polish 2013-03-07 15:38:41 -05:00
Matt Wright bbe99b5436 Fixes #98 2013-03-07 15:38:34 -05:00
Matt Wright 246ab41479 Merge pull request #96 from invernizzi/develop
NextFormMixin security bug fixed: open redirect
2013-03-05 13:58:08 -08:00
Luca Invernizzi 48dd3fa5bf NextFormMixin security bug fixed: open redirect
NextFormMixin was missing validations check on redirection [1]. Only internal redirections
are now allowed.
Attack Example: http://127.0.0.1:5000/login?next=http://google.com (it should not redirect to google.com)
wq
[1] https://www.owasp.org/index.php/Top_10_2010-A10-Unvalidated_Redirects_and_Forwards
2013-03-05 21:20:45 +00:00
Matt Wright 7db5fe32a8 Turn on testing flag for test app 2013-03-04 14:57:29 -05:00
Matt Wright 38a1dfa336 Merge pull request #85 from chrishaines/template_list
Template paths can be specified in config
2013-03-03 18:38:35 -08:00
Matt Wright 8b41b531a6 Merge pull request #91 from intonarumori/develop
added option to disable register email
2013-02-20 15:06:46 -08:00
rumori 4a048a4918 updated doc with new registration email option 2013-02-20 17:37:30 +01:00
rumori ae64370478 added option to disable register email 2013-02-20 17:04:47 +01:00
Matt Wright b03f355fae Merge pull request #88 from andreev-artem/develop
Password should be encoded as 'utf-8' before creating hmac to support passwords with non-latin symbols
2013-02-03 12:12:04 -08:00
Artem Andreev 8085e0031e Password should be encoded as 'utf-8' before creating hmac to support passwords with non-latin symbols 2013-02-03 22:14:32 +04:00
Chris Haines dbc2dcc625 Removed flask-script as a dependency in setup.py 2013-02-01 19:43:13 -05:00
Chris Haines ad6227006e Fixed lack of newline in requirements.txt 2013-02-01 19:41:42 -05:00
Chris Haines 4f414cf70f Merge branch 'develop' of git://github.com/mattupstate/flask-security into template_list
Conflicts:
	requirements.txt
2013-02-01 19:40:01 -05:00
Matt Wright adb2680289 Add change password endpoint 2013-02-01 18:21:43 -05:00
Matt Wright f1f621d178 Merge pull request #78 from eskil/change_password_form
Change password form
2013-02-01 15:16:45 -08:00
Matt Wright 996e162d11 Merge pull request #70 from eskil/formsdocs
Clarify user model/register form interaction in docs.
2013-02-01 14:59:40 -08:00
Matt Wright 840f72a589 Merge pull request #82 from maebert/flask-peewee
Flask-Peewee support
2013-02-01 14:44:52 -08:00
Matt Wright eca83f83ac Test configured login form messages better 2013-02-01 17:37:03 -05:00
Matt Wright 0f070b494a Merge branch 'feature/login_form_messages' into develop 2013-02-01 17:33:22 -05:00
Matt Wright 0ea1e0c03d Update CHANGES 2013-02-01 17:33:15 -05:00
Matt Wright c49d9b57ed Make login form messages configurable 2013-02-01 17:32:54 -05:00
Matt Wright 8d7e0f5190 Merge branch 'feature/csrf_testing' into develop 2013-02-01 17:25:36 -05:00
Matt Wright 012781103a Update CHANGES 2013-02-01 17:25:30 -05:00
Matt Wright 34b3bf9e80 Fix CSRF functionality for LoginForm
The login form was not respecting csrf validation. I've adjusted the tests as well to always send a CSRF token along. This now requires all requests to pass a csrf token. If performing plain AJAX requests the token will have to be extracted from the form in some way. Fixes #86
2013-02-01 17:23:18 -05:00
Chris Haines 4c203a4777 Removed extra line. 2013-01-29 22:29:31 -05:00
Chris Haines 02c49ee423 Paths for templates are now configurable 2013-01-29 22:24:11 -05:00
Chris Haines 3f243d1c11 Updated flask version and added flask-script to setup.py. 2013-01-29 19:51:25 -05:00
Chris Haines cf37d59d47 Updated requirements to reflect packages in setup.py 2013-01-29 19:27:05 -05:00
Chris Haines 75c10c75d9 Fixed typo in requirements.txt 2013-01-29 19:13:20 -05:00
Manuel Ebert 51e06bdbb0 Fixes typo in find_or_create_role 2013-01-29 15:46:59 -08:00
Manuel Ebert 462fb1ae7e Convenience method for finding or creating a role
`datastore. find_or_create_role("admin")` will now always return a role
with the name admin; useful for initialisation,
2013-01-28 18:58:11 -08:00
Manuel Ebert aea5b91649 Method stub parameters and docs for find_role didn't match implementations. 2013-01-28 18:57:19 -08:00
Manuel Ebert e3e96d546a Another small fix for the peewee docs 2013-01-25 16:59:48 -08:00
Manuel Ebert 46c2355a7e FIxes peewee description on quickstart 2013-01-25 16:58:21 -08:00
Manuel Ebert 423e430e04 Docs for flask-peewee 2013-01-25 16:54:18 -08:00
Manuel Ebert 70b11d9015 Unit-tests for flask-peewee 2013-01-25 16:53:01 -08:00
Manuel Ebert 5687f2f5a9 Adds support for flask-peewee 2013-01-25 16:52:50 -08:00
Matt Wright b82a8d681d Merge pull request #79 from maebert/develop
Fixes dependencies
2013-01-25 08:08:05 -08:00
Manuel Ebert 29af22bd6e Updated requirements.txt to reflect setup.py
Also fixes a typo (`passlib=1.5.3` is not a valid line)
2013-01-24 16:24:50 -08:00
Manuel Ebert 68f8127286 Updates dependencies to Flask>=0.9
after_this_request was introduced in 0.9-dev
2013-01-24 16:23:48 -08:00
Eskil Heyn Olsen 647e1a06d5 Add test to ensure it is disabled 2013-01-17 20:35:23 -08:00
Matt Wright 84759b5dbd Merge pull request #73 from apahomov/multiple-auth-mechanisms
Multiple auth mechanisms
2013-01-16 19:37:55 -08:00
apahomov 39f62374aa Added tests 2013-01-15 10:30:48 +04:00
Matt Wright 4de2cbcf9f Merge pull request #72 from eskil/please_login_msg
Fix passing category to login required message
2013-01-14 07:17:05 -08:00
apahomov 3f9ca423bd Calling auth methods 2013-01-14 16:11:09 +04:00
apahomov bbed019ca5 Add auth_required decorator that allows multiple auth mechanisms 2013-01-14 15:45:18 +04:00
Eskil Heyn Olsen 3081d76787 Fix passing category to login required message 2013-01-13 23:25:16 -08:00
apahomov 1a0ddff82b Get auth token from JSON request. 2013-01-14 10:54:48 +04:00
Matt Wright e9b40a12c8 Fix for Python 2.6 2013-01-14 00:26:46 -05:00
Eskil Heyn Olsen 3adb4afd60 Minor wording fix 2013-01-12 23:58:47 -08:00
Eskil Heyn Olsen c7af67f3a3 Fix signals test, following redirect is a trap 2013-01-12 23:53:31 -08:00
Eskil Heyn Olsen cca9298e74 Fix and test redir to configurable view post change 2013-01-12 19:56:50 -08:00
Eskil Heyn Olsen 4f9e23e0bc Fix email forms to have externally available links 2013-01-12 19:34:53 -08:00
Eskil Heyn Olsen ded62a556b Add a password-changed signal 2013-01-12 19:03:02 -08:00
Eskil Heyn Olsen 508f4d1b52 Fix change password form 2013-01-12 15:57:52 -08:00
Eskil Heyn Olsen 050ccb847a Forgot to add form 2013-01-12 14:55:30 -08:00
Eskil Heyn Olsen b67e61d625 Change password form 2013-01-12 14:40:42 -08:00
Eskil Heyn Olsen 9a47ec1ed9 Working on change password form 2013-01-11 22:35:54 -08:00
Eskil Heyn Olsen c5c27768f2 First pieces of change password form 2013-01-11 19:07:07 -08:00
Eskil Heyn Olsen 4dd944a8c1 Another fix to docs 2013-01-11 18:38:05 -08:00
Eskil Heyn Olsen e22aff51a4 Clarify user model/register form interaction in docs. 2013-01-10 07:57:44 -08:00
Matt Wright 6adc26a897 Get rid of strftime in favor of total_seconds. Fixes #67 2013-01-09 14:02:42 -05:00
Matt Wright 9726726e5a Merge pull request #68 from eskil/fix_tests
remove import that was left by accident
2013-01-09 07:35:07 -08:00
Eskil Heyn Olsen 6202abb75c remove import that was left by accident 2013-01-08 18:41:16 -08:00
Matt Wright 3c65923d33 Merge pull request #66 from eskil/registerform
Update documentation reg. configurable forms
2013-01-08 08:16:30 -08:00
Eskil Heyn Olsen daed6e1064 Update documentation reg. configurable forms 2013-01-08 07:45:13 -08:00
Matt Wright f566f41fb3 Merge pull request #58 from eskil/registerform
Configurable forms
2013-01-08 07:10:14 -08:00
Eskil Heyn Olsen e4190a0315 Add kwargs for configurable forms.
Specifically list out the kwargs so we'll get an interpreter error
on a bad name.
2013-01-07 21:43:33 -08:00
Eskil Heyn Olsen ca0d1d0b50 All unit-tests for configurable forms 2013-01-07 21:43:27 -08:00
Matt Wright 4e57226a1b Merge pull request #65 from KangOl/develop
take advantage of passlib CryptContext
2013-01-07 21:34:08 -08:00
Christophe Simonis a89b76d648 do not break API. add a new function to verify and update password 2013-01-08 01:01:02 +01:00
Christophe Simonis d0497fc886 update password automatically 2013-01-08 00:49:20 +01:00
Christophe Simonis a1c007599f allow change of hash scheme 2013-01-08 00:15:21 +01:00
Eskil Heyn Olsen 81040a57a6 Views get forms from _security 2013-01-06 20:20:06 -08:00
Eskil Heyn Olsen 09fe5a2cb7 Make test_app take kwargs 2013-01-06 20:00:13 -08:00
Matt Wright d760aa41c5 Merge branch 'master' into develop 2013-01-06 21:01:53 -05:00
Matt Wright 53576c6013 Set Flask-Login messages 2013-01-06 21:01:35 -05:00
Matt Wright 029466830d Bump version number to 1.5.4 2013-01-06 20:43:02 -05:00
Matt Wright 31595196fc Update CHANGES 2013-01-06 20:42:51 -05:00
Matt Wright 2a0b582911 Change csrf_enabled parameter in forms to check for incoming JSON data. Fix #63 2013-01-06 20:41:01 -05:00
Matt Wright 63fb30daf8 Merge pull request #64 from eskil/signals
Document and unit-test signals.
2013-01-06 17:38:44 -08:00
Eskil Heyn Olsen 675b29b4fe Minor style fixes 2013-01-06 14:40:09 -08:00
Eskil Heyn Olsen 075ccf6a82 Add some more tests of signals 2013-01-06 14:33:15 -08:00
Eskil Heyn Olsen ae6f3b6753 Document and unit-test the signals.
Adds description of signals to docmentation. Adds unit-tests of
signal behaviour and tests parameters.
2013-01-06 14:12:18 -08:00
Eskil Heyn Olsen 1a87a4cd0c Fix to RegisterForm.to_dict.
Only add fields that are also attributes on the
datastorage.user_model.
2013-01-03 23:29:50 -08:00
Eskil Heyn Olsen f83092865b Configurable forms, issue:49 2013-01-03 22:00:29 -08:00
Eskil Heyn Olsen b15736accd RegisterFormMixin can now to_dict all fields.
It adds a to_dict function that uses inspect to add all wtf Field
to the returned dict. This allows extensions to the register form
to easily add fields that will be passed to the datastore's
create_user function.
2013-01-03 19:07:00 -08:00
Matt Wright 79ce6f00d3 Merge pull request #56 from eskil/fix_add_role
Fix query for find_user
2013-01-01 15:14:04 -08:00
Eskil Heyn Olsen 7f43acc167 Fix query for find_user 2013-01-01 11:28:31 -08:00
Matt Wright f8fbd6cec8 Bump version number to 1.5.3 2012-12-23 16:47:20 -05:00
Matt Wright 9d11dd0787 Update dependency rules 2012-12-23 16:47:14 -05:00
Matt Wright 4c31b6b689 Merge pull request #51 from joshpurvis/mongo-quickstart
Reorganized Quickstart and added MongoDB example.
2012-12-19 08:26:42 -08:00
Josh Purvis d468fae0ea Reorganized Quickstart and added MongoDB example. 2012-12-19 11:17:06 -05:00
75 changed files with 3695 additions and 1615 deletions
+9
View File
@@ -25,3 +25,12 @@ pip-log.txt
#Mr Developer
.mr.developer.cfg
#Virtualenv
env/
#Editor temporaries
*~
*.db
*cache*
+10 -7
View File
@@ -3,19 +3,22 @@ language: python
python:
- "2.6"
- "2.7"
- "3.3"
- "3.4"
- "pypy"
install:
- pip install . --quiet --use-mirrors
- "if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then pip install importlib --quiet --use-mirrors; fi"
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors
before_script:
- mysql -e 'create database flask_security_test;'
- pip install -r requirements.txt -r requirements-dev.txt -e .
- pip install coverage coveralls
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib; fi"
services:
- mongodb
script: nosetests
script: py.test --cov flask_security --cov-report term-missing --pep8 --flakes
after_script:
- coveralls
branches:
only:
+38
View File
@@ -0,0 +1,38 @@
Flask-Security is written and maintained by Matt Wright and
various contributors:
Development Lead
````````````````
- Matt Wright <matt+github@nobien.net>
Patches and Suggestions
```````````````````````
Alexander Sukharev
Alexey Poryadin
Andrew J. Camenga
Anthony Plunkett
Artem Andreev
Catherine Wise
Chris Haines
Christophe Simonis
David Ignacio
Eric Butler
Eskil Heyn Olsen
Iuri de Silvio
Jay Goel
Joe Esposito
Joe Hand
Josh Purvis
Kostyantyn Leschenko
Luca Invernizzi
Manuel Ebert
Martin Maillard
Paweł Krześniak
Robert Clark
Rodrigue Cloutier
Rotem Yaari
Srijan Choudhary
Tristan Escalada
Vadim Kotov
+185 -3
View File
@@ -3,6 +3,188 @@ Flask-Security Changelog
Here you can see the full list of changes between each Flask-Security release.
Version 1.7.4
-------------
Released October 13th 2014
- Fixed a bug related to changing existing passwords from plaintext to hashed
- Fixed a bug in form validation that did not enforce case insensivitiy
- Fixed a bug with validating redirects
Version 1.7.3
-------------
Released June 10th 2014
- Fixed a bug where redirection to `SECURITY_POST_LOGIN_VIEW` was not respected
- Fixed string encoding in various places to be friendly to unicode
- Now using `werkzeug.security.safe_str_cmp` to check tokens
- Removed user information from JSON output on `/reset` responses
- Added Python 3.4 support
Version 1.7.2
-------------
Released May 6th 2014
- Updated IP tracking to check for `X-Forwarded-For` header
- Fixed a bug regarding the re-hashing of passwords with a new algorithm
- Fixed a bug regarding the `password_changed` signal.
Version 1.7.1
-------------
Released January 14th 2014
- Fixed a bug where passwords would fail to verify when specifying a password hash algorithm
Version 1.7.0
-------------
Released January 10th 2014
- Python 3.3 support!
- Dependency updates
- Fixed a bug when `SECURITY_LOGIN_WITHOUT_CONFIRMATION = True` did not allow users to log in
- Added `SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL` configuraiton option to optionally send password reset notice emails
- Add documentation for `@security.send_mail_task`
- Move to `request.get_json` as `request.json` is now deprecated in Flask
- Fixed a bug when using AJAX to change a user's password
- Added documentation for select functions in the `flask_security.utils` module
- Fixed a bug in `flask_security.forms.NextFormMixin`
- Added `CHANGE_PASSWORD_TEMPLATE` configuration option to optionally specify a different change password template
- Added the ability to specify addtional fields on the user model to be used for identifying the user via the `USER_IDENTITY_ATTRIBUTES` configuration option
- An error is now shown if a user tries to change their password and the password is the same as before. The message can be customed with the `SECURITY_MSG_PASSWORD_IS_SAME` configuration option
- Fixed a bug in `MongoEngineUserDatastore` where user model would not be updated when using the `add_role_to_user` method
- Added `SECURITY_SEND_PASSWORD_CHANGE_EMAIL` configuration option to optionally disable password change email from being sent
- Fixed a bug in the `find_or_create_role` method of the PeeWee datastore
- Removed pypy tests
- Fixed some tests
- Include CHANGES and LICENSE in MANIFEST.in
- A bit of documentation cleanup
- A bit of code cleanup including removal of unnecessary utcnow call and simplification of get_max_age method
Version 1.6.9
-------------
Released August 20th 2013
- Fix bug in SQLAlchemy datastore's `get_user` function
- Fix bug in PeeWee datastore's `remove_role_from_user` function
- Fixed import error caused by new Flask-WTF release
Version 1.6.8
-------------
Released August 1st 2013
- Fixed bug with case sensitivity of email address during login
- Code cleanup regarding token_callback
- Ignore validation errors in find_user function for MongoEngineUserDatastore
Version 1.6.7
-------------
Released July 11th 2013
- Made password length form error message configurable
- Fixed email confirmation bug that prevented logged in users from confirming their email
Version 1.6.6
-------------
Released June 28th 2013
- Fixed dependency versions
Version 1.6.5
-------------
Released June 20th 2013
- Fixed bug in `flask.ext.security.confirmable.generate_confirmation_link`
Version 1.6.4
-------------
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
-------------
@@ -56,10 +238,10 @@ Version 1.2.0
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.
Version 1.1.0
-------------
Initial release
Initial release
+7 -2
View File
@@ -1,2 +1,7 @@
recursive-include tests *.py
recursive-include flask_security/templates *.*
include README.rst
include requirements.txt
include requirements-dev.txt
include tox.ini
recursive-include flask_security/templates *.*
recursive-include tests *
recursive-exclude tests/__pycache__ *
+16 -1
View File
@@ -3,6 +3,21 @@ Flask-Security
.. image:: https://secure.travis-ci.org/mattupstate/flask-security.png?branch=develop
.. image:: https://pypip.in/v/Flask-Security/badge.png
:target: https://pypi.python.org/pypi/Flask-Security/
:alt: Latest Version
.. image:: https://coveralls.io/repos/mattupstate/flask-security/badge.png?branch=develop
:target: https://coveralls.io/r/mattupstate/flask-security
.. image:: https://pypip.in/d/Flask-Security/badge.png
:target: https://pypi.python.org/pypi//Flask-Security/
:alt: Downloads
.. image:: https://pypip.in/license/Flask-Security/badge.png
:target: https://pypi.python.org/pypi/Flask-Security/
:alt: License
Flask-Security quickly adds security features to your Flask application.
Resources
@@ -12,4 +27,4 @@ Resources
- `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>`_
<http://github.com/mattupstate/flask-security/zipball/develop#egg=Flask-Security-dev>`_
+2 -2
View File
@@ -9,9 +9,9 @@
<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/jfinkels/flask-security/issues">Issue Tracker</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>
</ul>
+74 -2
View File
@@ -49,9 +49,81 @@ Datastores
:members:
:inherited-members:
.. autoclass:: flask_security.datastore.PeeweeUserDatastore
:members:
:inherited-members:
Utils
-----
.. autofunction:: flask_security.utils.login_user
.. autofunction:: flask_security.utils.logout_user
.. autofunction:: flask_security.utils.get_hmac
.. autofunction:: flask_security.utils.verify_password
.. autofunction:: flask_security.utils.verify_and_update_password
.. autofunction:: flask_security.utils.encrypt_password
.. autofunction:: flask_security.utils.url_for_security
.. autofunction:: flask_security.utils.get_within_delta
.. autofunction:: flask_security.utils.send_mail
.. autofunction:: flask_security.utils.get_token_status
Signals
-------
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. Flask-Security does not provide any additional
signals.
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/
+1
View File
@@ -0,0 +1 @@
.. include:: ../AUTHORS
+1 -1
View File
@@ -49,7 +49,7 @@ copyright = u'2012, Matt Wright'
# built documents.
#
# The short X.Y version.
version = '1.5.2'
version = '1.7.4'
# The full version, including alpha/beta/rc tags.
release = version
+133 -62
View File
@@ -15,10 +15,10 @@ Core
``SECURITY_URL_PREFIX`` Specifies the URL prefix for the
Flask-Security blueprint. Defaults to
``None``.
``SECURITY_FLASH_MESSAGES`` Specifies wether or not to flash
``SECURITY_FLASH_MESSAGES`` Specifies whether or not to flash
messages during security procedures.
Defaults to ``True``.
``SECURITY_PASSWORD_HASH`` Specifies the password hash algorith to
``SECURITY_PASSWORD_HASH`` Specifies the password hash algorithm to
use when encrypting and decrypting
passwords. Recommended values for
production systems are ``bcrypt``,
@@ -56,6 +56,8 @@ URLs and Views
``/register``.
``SECURITY_RESET_URL`` Specifies the password reset URL. Defaults to
``/reset``.
``SECURITY_CHANGE_URL`` Specifies the password change URL. Defaults to
``/change``.
``SECURITY_CONFIRM_URL`` Specifies the email confirmation URL. Defaults
to ``/confirm``.
``SECURITY_POST_LOGIN_VIEW`` Specifies the default view to redirect to after
@@ -67,35 +69,73 @@ URLs and Views
``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
``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
``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 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``.
value is ``None``, the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``.
``SECURITY_POST_CHANGE_VIEW`` Specifies the view to redirect to after a user
successfully changes their password. This value
can be set to a URL or an endpoint name. If this
value is ``None``, the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``.
``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user
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
``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_CHANGE_PASSWORD_TEMPLATE`` Specifies the path to the template for
the change password page. Defaults to
``security/change_password.html``.
``SECURITY_SEND_CONFIRMATION_TEMPLATE`` Specifies the path to the template for
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
-------------
@@ -104,7 +144,7 @@ Feature Flags
========================= ======================================================
``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
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.
@@ -118,15 +158,19 @@ Feature Flags
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
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
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
@@ -134,59 +178,86 @@ 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``
=========================================== ====================================
================================================= ==============================
``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 email. Defaults
to ``Password reset
instructions``
``SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE`` Sets the subject for the
password change notice.
Defaults to ``Your password
has been changed``
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
confirmation message. Defaults
to ``Please confirm your
email``
================================================= ==============================
Miscellaneous
-------------
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
======================================= ========================================
``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_SEND_REGISTER_EMAIL`` Specifies whether registration
email is sent. Defaults to
``True``.
``SECURITY_SEND_PASSWORD_CHANGE_EMAIL`` Specifies whether password change
email is sent. Defaults to
``True``.
``SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL`` Specifies whether password reset
notice 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``.
============================================= ==================================
+2 -1
View File
@@ -10,4 +10,5 @@ Contents
models
customizing
api
changelog
changelog
authors
+86 -19
View File
@@ -2,7 +2,7 @@ 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,
configured features to get you up and running as quickly as possible. However,
you'll probably want to change the way these views look to be more in line with
your application's visual design.
@@ -18,6 +18,7 @@ following is a list of view templates:
* `security/login_user.html`
* `security/register_user.html`
* `security/reset_password.html`
* `security/change_password.html`
* `security/send_confirmation.html`
* `security/send_login.html`
@@ -26,14 +27,16 @@ 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 processory:
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
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)
@@ -55,27 +58,70 @@ The following is a list of all the available context processor decorators:
* ``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``.
Flask-Security is also packaged with a default template for each email that it
may send. Templates are located within the subfolder named ``security/email``.
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/reset_notice.txt`
* `security/mail/welcome.html`
* `security/mail/welcome.txt`
* `security/email/confirmation_instructions.html`
* `security/email/confirmation_instructions.txt`
* `security/email/login_instructions.html`
* `security/email/login_instructions.txt`
* `security/email/reset_instructions.html`
* `security/email/reset_instructions.txt`
* `security/email/reset_notice.html`
* `security/email/change_notice.txt`
* `security/email/change_notice.html`
* `security/email/reset_notice.txt`
* `security/email/welcome.html`
* `security/email/welcome.txt`
Overriding these templates is simple:
@@ -85,12 +131,33 @@ Overriding these templates is simple:
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::
templates, you can specify an email context processor with the
``mail_context_processor`` decorator. For example::
security = Security(app, user_datastore)
# This processor is added to all emails
@security.email_context_processor
@security.mail_context_processor
def security_mail_processor():
return dict(hello="world")
return dict(hello="world")
Emails with Celery
------------------
Sometimes it makes sense to send emails via a task queue, such as `Celery`_.
To delay the sending of emails, you can use the ``@security.send_mail_task``
decorator like so::
# Setup the task
@celery.task
def send_security_email(msg):
# Use the Flask-Mail extension instance to send the incoming ``msg`` parameter
# which is an instance of `flask_mail.Message`
mail.send(msg)
@security.send_mail_task
def delay_security_email(msg):
send_security_email.delay(msg)
.. _Celery: http://www.celeryproject.org/
+18 -3
View File
@@ -24,7 +24,7 @@ instance, you may assign roles such as `Admin`, `Editor`, `SuperUser`, or a
combination of said roles to a user. Access control is based on the role name
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`_.
control, you can refer to the Flask-Principal `documentation on this topic`_.
Password Encryption
@@ -34,7 +34,8 @@ Password encryption is enabled with `passlib`_. Passwords are stored in plain
text by default but you can easily configure the encryption algorithm. You
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.
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
@@ -54,7 +55,7 @@ authentication endpoint. A successful call to this endpoint will return the
user's ID and their authentication token. This token can be used in subsequent
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
name is `Authentication-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
@@ -104,6 +105,20 @@ statistics. They include:
* Total login count
JSON/Ajax Support
-----------------
Flask-Security supports JSON/Ajax requests where appropriate. Just remember that
all endpoints require a CSRF token just like HTML views. More specifically
JSON is supported for the following operations:
* Login requests
* Registration requests
* Change password requests
* Confirmation requests
* Forgot password requests
* Passwordless login requests
.. _Flask-Login: http://packages.python.org/Flask-Login/
.. _alternative token: http://packages.python.org/Flask-Login/#alternative-tokens
+8 -6
View File
@@ -10,9 +10,10 @@ Flask application. They include:
4. Basic HTTP authentication
5. Token based authentication
6. Token based account activation (optional)
7. Token based password recovery/resetting (optional)
7. Token based password recovery / resetting (optional)
8. User registration (optional)
9. Login tracking (optional)
10. JSON/Ajax Support
Many of these features are made possible by integrating various Flask extensions
and libraries. They include:
@@ -21,16 +22,17 @@ and libraries. They include:
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-Mail/>`_
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 persistance:
extensions out of the box for data persistence:
1. `Flask-SQLAlchemy <http://packages.python.org/Flask-SQLAlchemy/>`_
2. `Flask-MongoEngine <http://packages.python.org/Flask-MongoEngine/>`_
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/>`_
.. include:: contents.rst.inc
.. include:: contents.rst.inc
+9 -9
View File
@@ -1,12 +1,12 @@
Models
======
Flask-Security assumes you'll be using libraries such as SQLAlchemy or
MongoEngine 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:
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 bare
minimum your `User` and `Role` model should include the following fields:
**User**
@@ -32,7 +32,7 @@ Confirmable
^^^^^^^^^^^
If you enable account confirmation by setting your application's
`SECURITY_CONFIRMABLE` configuration value to `True` your `User` model will
`SECURITY_CONFIRMABLE` configuration value to `True`, your `User` model will
require the following additional field:
* ``confirmed_at``
@@ -41,11 +41,11 @@ Trackable
^^^^^^^^^
If you enable user tracking by setting your application's `SECURITY_TRACKABLE`
configuration value to `True` your `User` model will require the following
configuration value to `True`, your `User` model will require the following
additional fields:
* ``last_login_at``
* ``current_login_at``
* ``last_login_ip``
* ``current_login_ip``
* ``login_count``
* ``login_count``
+176 -24
View File
@@ -1,26 +1,35 @@
Quick Start
===========
- `Basic SQLAlchemy Application <#basic-sqlalchemy-application>`_
- `Basic MongoEngine Application <#basic-mongoengine-application>`_
- `Basic Peewee Application <#basic-peewee-application>`_
- `Mail Configuration <#mail-configuration>`_
Installation
------------
Basic SQLAlchemy Application
=============================
Install requirements:
SQLAlchemy Install requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ mkvirtualenv <your-app-name>
$ pip install flask-security, flask-sqlalchemy
::
$ mkvirtualenv <your-app-name>
$ pip install flask-security flask-sqlalchemy
Basic Application
-----------------
SQLAlchemy Application
~~~~~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as possible
using SQLAlchemy.::
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
UserMixin, RoleMixin, login_required
# Create app
app = Flask(__name__)
@@ -63,21 +72,169 @@ using SQLAlchemy.::
# 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 = CharField(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.
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::
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
@@ -90,11 +247,6 @@ basic application code in the previous section::
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/>`_
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/>`_.
+5 -5
View File
@@ -10,14 +10,14 @@
:license: MIT, see LICENSE for more details.
"""
__version__ = '1.5.2'
__version__ = '1.7.4'
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
from .decorators import auth_token_required, http_auth_required, \
login_required, roles_accepted, roles_required
login_required, roles_accepted, roles_required, auth_required
from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
from .signals import confirm_instructions_sent, password_reset, \
reset_password_instructions_sent, user_confirmed, user_registered
reset_password_instructions_sent, user_confirmed, user_registered
from .utils import login_user, logout_user, url_for_security
+45
View File
@@ -0,0 +1,45 @@
# -*- 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
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
"""
if config_value('SEND_PASSWORD_CHANGE_EMAIL'):
subject = config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE')
send_mail(subject, 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(app._get_current_object(), user=user)
+11 -7
View File
@@ -11,11 +11,11 @@
from datetime import datetime
from flask import current_app as app, request
from flask import current_app as app
from werkzeug.local import LocalProxy
from .utils import send_mail, md5, url_for_security, get_token_status,\
config_value
config_value
from .signals import user_confirmed, confirm_instructions_sent
@@ -27,8 +27,7 @@ _datastore = LocalProxy(lambda: _security.datastore)
def generate_confirmation_link(user):
token = generate_confirmation_token(user)
url = url_for_security('confirm_email', token=token)
return request.url_root[:-1] + url, token
return url_for_security('confirm_email', token=token, _external=True), token
def send_confirmation_instructions(user):
@@ -44,7 +43,7 @@ def send_confirmation_instructions(user):
'confirmation_instructions', user=user,
confirmation_link=confirmation_link)
confirm_instructions_sent.send(user, app=app._get_current_object())
confirm_instructions_sent.send(app._get_current_object(), user=user)
return token
@@ -59,7 +58,9 @@ def generate_confirmation_token(user):
def requires_confirmation(user):
"""Returns `True` if the user requires confirmation."""
return _security.confirmable and user.confirmed_at == None
return (_security.confirmable and
not _security.login_without_confirmation and
user.confirmed_at is None)
def confirm_email_token_status(token):
@@ -78,6 +79,9 @@ def confirm_user(user):
:param user: The user to confirm
"""
if user.confirmed_at is not None:
return False
user.confirmed_at = datetime.utcnow()
_datastore.put(user)
user_confirmed.send(user, app=app._get_current_object())
user_confirmed.send(app._get_current_object(), user=user)
return True
+172 -38
View File
@@ -9,18 +9,22 @@
:license: MIT, see LICENSE for more details.
"""
from flask import current_app
from flask.ext.login import AnonymousUser as AnonymousUserBase, \
UserMixin as BaseUserMixin, LoginManager, current_user
from flask import current_app, render_template
from flask.ext.login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
LoginManager, current_user
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \
identity_loaded
identity_loaded
from itsdangerous import URLSafeTimedSerializer
from passlib.context import CryptContext
from werkzeug.datastructures import ImmutableList
from werkzeug.local import LocalProxy
from werkzeug.security import safe_str_cmp
from .utils import config_value as cv, get_config, md5, url_for_security
from .utils import config_value as cv, get_config, md5, url_for_security, string_types
from .views import create_blueprint
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \
SendConfirmationForm, PasswordlessLoginForm
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
@@ -30,6 +34,7 @@ _security = LocalProxy(lambda: current_app.extensions['security'])
_default_config = {
'BLUEPRINT_NAME': 'security',
'URL_PREFIX': None,
'SUBDOMAIN': None,
'FLASH_MESSAGES': True,
'PASSWORD_HASH': 'plaintext',
'PASSWORD_SALT': None,
@@ -37,6 +42,7 @@ _default_config = {
'LOGOUT_URL': '/logout',
'REGISTER_URL': '/register',
'RESET_URL': '/reset',
'CHANGE_URL': '/change',
'CONFIRM_URL': '/confirm',
'POST_LOGIN_VIEW': '/',
'POST_LOGOUT_VIEW': '/',
@@ -44,12 +50,24 @@ _default_config = {
'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',
'CHANGE_PASSWORD_TEMPLATE': 'security/change_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,
'SEND_PASSWORD_CHANGE_EMAIL': True,
'SEND_PASSWORD_RESET_NOTICE_EMAIL': True,
'LOGIN_WITHIN': '1 days',
'CONFIRM_EMAIL_WITHIN': '5 days',
'RESET_PASSWORD_WITHIN': '5 days',
@@ -60,36 +78,111 @@ _default_config = {
'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_RESET': 'Password reset instructions'
'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed',
'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions',
'USER_IDENTITY_ATTRIBUTES': ['email'],
'PASSWORD_SCHEMES': [
'bcrypt',
'des_crypt',
'pbkdf2_sha256',
'pbkdf2_sha512',
'sha256_crypt',
'sha512_crypt',
# And always last one...
'plaintext'
],
'DEPRECATED_PASSWORD_SCHEMES': ['auto']
}
#: Default Flask-Security messages
_default_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'),
'ALREADY_CONFIRMED': ('This email has already been confirmed', 'info'),
'PASSWORD_MISMATCH': ('Password does not match', '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'),
'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'),
'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success')
'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_NOT_SET': (
'No password is set for this user', 'error'),
'PASSWORD_INVALID_LENGTH': (
'Password must be at least 6 characters', 'error'),
'USER_DOES_NOT_EXIST': (
'Specified user does not exist', 'error'),
'INVALID_PASSWORD': (
'Invalid password', 'error'),
'PASSWORDLESS_LOGIN_SUCCESSFUL': (
'You have successfuly logged in.', 'success'),
'PASSWORD_RESET': (
'You successfully reset your password and you have been logged in automatically.',
'success'),
'PASSWORD_IS_THE_SAME': (
'Your new password must be different than your previous password.', 'error'),
'PASSWORD_CHANGE': (
'You successfully changed your password.', 'success'),
'LOGIN': (
'Please log in to access this page.', 'info'),
'REFRESH': (
'Please reauthenticate to access this page.', 'info'),
}
_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,
}
@@ -101,12 +194,11 @@ 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]:
if user and safe_str_cmp(md5(user.password), data[1]):
return user
except:
pass
return None
return AnonymousUser()
def _identity_loader():
@@ -131,6 +223,14 @@ def _get_login_manager(app):
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
@@ -143,7 +243,12 @@ def _get_principal(app):
def _get_pwd_context(app):
pw_hash = cv('PASSWORD_HASH', app=app)
return CryptContext(schemes=[pw_hash], default=pw_hash)
schemes = cv('PASSWORD_SCHEMES', app=app)
deprecated = cv('DEPRECATED_PASSWORD_SCHEMES', app=app)
if pw_hash not in schemes:
allowed = (', '.join(schemes[:-1]) + ' and ' + schemes[-1])
raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed))
return CryptContext(schemes=schemes, default=pw_hash, deprecated=deprecated)
def _get_serializer(app, name):
@@ -170,6 +275,10 @@ def _get_state(app, datastore, **kwargs):
_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)
@@ -179,13 +288,16 @@ def _context_processor():
class RoleMixin(object):
"""Mixin for `Role` model definitions"""
def __eq__(self, other):
return (self.name == other or \
return (self.name == other or
self.name == getattr(other, 'name', None))
def __ne__(self, other):
return (self.name != other and
self.name != getattr(other, 'name', None))
return not self.__eq__(other)
def __hash__(self):
return hash(self.name)
class UserMixin(BaseUserMixin):
@@ -204,14 +316,16 @@ class UserMixin(BaseUserMixin):
"""Returns `True` if the user identifies with the specified role.
:param role: A role name or `Role` instance"""
return role in self.roles
if isinstance(role, string_types):
return role in (role.name for role in self.roles)
else:
return role in self.roles
class AnonymousUser(AnonymousUserBase):
class AnonymousUser(AnonymousUserMixin):
"""AnonymousUser definition"""
def __init__(self):
super(AnonymousUser, self).__init__()
self.roles = ImmutableList()
def has_role(self, *args):
@@ -230,7 +344,7 @@ class _SecurityState(object):
fn not in group and group.append(fn)
def _run_ctx_processor(self, endpoint):
rv, fns = {}, []
rv = {}
for g in [None, endpoint]:
for fn in self._context_processors.setdefault(g, []):
rv.update(fn())
@@ -251,6 +365,9 @@ class _SecurityState(object):
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)
@@ -277,12 +394,17 @@ class Security(object):
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):
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
@@ -294,15 +416,27 @@ class Security(object):
identity_loaded.connect_via(app)(_on_identity_loaded)
state = _get_state(app, datastore)
state = _get_state(app, datastore,
login_form=login_form,
confirm_register_form=confirm_register_form,
register_form=register_form,
forgot_password_form=forgot_password_form,
reset_password_form=reset_password_form,
change_password_form=change_password_form,
send_confirmation_form=send_confirmation_form,
passwordless_login_form=passwordless_login_form)
if register_blueprint:
app.register_blueprint(create_blueprint(state, __name__))
app.context_processor(_context_processor)
state.render_template = self.render_template
app.extensions['security'] = state
return state
def render_template(self, *args, **kwargs):
return render_template(*args, **kwargs)
def __getattr__(self, name):
return getattr(self._state, name, None)
+168 -16
View File
@@ -9,6 +9,9 @@
:license: MIT, see LICENSE for more details.
"""
from .utils import get_identity_attributes, string_types
class Datastore(object):
def __init__(self, db):
self.db = db
@@ -44,6 +47,15 @@ class MongoEngineDatastore(Datastore):
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.
@@ -56,9 +68,9 @@ class UserDatastore(object):
self.role_model = role_model
def _prepare_role_modify_args(self, user, role):
if isinstance(user, basestring):
user = self.find_user(email=user.email)
if isinstance(role, basestring):
if isinstance(user, string_types):
user = self.find_user(email=user)
if isinstance(role, string_types):
role = self.find_role(role)
return user, role
@@ -72,29 +84,33 @@ class UserDatastore(object):
kwargs['roles'] = roles
return kwargs
def find_user(self, **kwargs):
"""Returns a user matching the provided paramters."""
def get_user(self, id_or_email):
"""Returns a user matching the specified ID or email address."""
raise NotImplementedError
def find_role(self, **kwargs):
"""Returns a role matching the provided paramters."""
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
"""Adds a role to a user.
:param user: The user to manipulate
:param role: The role to add to the user
"""
rv = False
user, role = self._prepare_role_modify_args(user, role)
if role not in user.roles:
rv = True
user.roles.append(role)
return rv
self.put(user)
return True
return False
def remove_role_from_user(self, user, role):
"""Removes a role from a user
"""Removes a role from a user.
:param user: The user to manipulate
:param role: The role to remove from the user
@@ -104,6 +120,7 @@ class UserDatastore(object):
if role in user.roles:
rv = True
user.roles.remove(role)
self.put(user)
return rv
def toggle_active(self, user):
@@ -137,14 +154,21 @@ class UserDatastore(object):
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))
kwargs = self._prepare_create_user_args(**kwargs)
user = self.user_model(**kwargs)
return self.put(user)
def delete_user(self, user):
"""Delete the specified user
"""Deletes the specified user.
:param user: The user to delete
"""
@@ -159,6 +183,22 @@ class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
SQLAlchemyDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
def get_user(self, identifier):
if self._is_numeric(identifier):
return self.user_model.query.get(identifier)
for attr in get_identity_attributes():
query = getattr(self.user_model, attr).ilike(identifier)
rv = self.user_model.query.filter(query).first()
if rv is not None:
return rv
def _is_numeric(self, value):
try:
int(value)
except ValueError:
return False
return True
def find_user(self, **kwargs):
return self.user_model.query.filter_by(**kwargs).first()
@@ -174,8 +214,120 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
MongoEngineDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
def get_user(self, identifier):
from mongoengine import ValidationError
try:
return self.user_model.objects(id=identifier).first()
except ValidationError:
pass
for attr in get_identity_attributes():
query_key = '%s__iexact' % attr
query = {query_key: identifier}
rv = self.user_model.objects(**query).first()
if rv is not None:
return rv
def find_user(self, **kwargs):
return self.user_model.objects(**kwargs).first()
try:
from mongoengine.queryset import Q, QCombination
except ImportError:
from mongoengine.queryset.visitor import Q, QCombination
from mongoengine.errors import ValidationError
queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items())
query = QCombination(QCombination.AND, queries)
try:
return self.user_model.objects(query).first()
except ValidationError: # pragma: no cover
return None
def find_role(self, role):
return self.role_model.objects(name=role).first()
# TODO: Not sure why this was added but tests pass without it
# def add_role_to_user(self, user, role):
# rv = super(MongoEngineUserDatastore, self).add_role_to_user(user, role)
# if rv:
# self.put(user)
# return rv
class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
"""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 get_user(self, identifier):
try:
return self.user_model.get(self.user_model.id == identifier)
except ValueError:
pass
for attr in get_identity_attributes():
column = getattr(self.user_model, attr)
try:
return self.user_model.get(column ** identifier)
except self.user_model.DoesNotExist:
pass
def find_user(self, **kwargs):
try:
return self.user_model.filter(**kwargs).get()
except self.user_model.DoesNotExist:
return None
def find_role(self, role):
try:
return self.role_model.filter(name=role).get()
except self.role_model.DoesNotExist:
return None
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)
self.put(user)
return user
def add_role_to_user(self, user, role):
"""Adds a role to 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.put(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():
query = self.UserRole.delete().where(
self.UserRole.user == user, self.UserRole.role == role)
query.execute()
return True
else:
return False
+46 -12
View File
@@ -9,10 +9,11 @@
:license: MIT, see LICENSE for more details.
"""
from collections import namedtuple
from functools import wraps
from flask import current_app, Response, request, redirect
from flask.ext.login import current_user, login_required
from flask import current_app, Response, request, redirect, _request_ctx_stack
from flask.ext.login import current_user, login_required # pragma: no flakes
from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed
from werkzeug.local import LocalProxy
@@ -30,6 +31,8 @@ _default_unauthorized_html = """
or your browser doesn't understand how to supply the credentials required.</p>
"""
BasicAuth = namedtuple('BasicAuth', 'username, password')
def _get_unauthorized_response(text=None, headers=None):
text = text or _default_unauthorized_html
@@ -48,27 +51,28 @@ def _check_token():
args_key = _security.token_authentication_key
header_token = request.headers.get(header_key, None)
token = request.args.get(args_key, header_token)
serializer = _security.remember_token_serializer
if request.get_json(silent=True):
token = request.json.get(args_key, token)
try:
data = serializer.loads(token)
except:
return False
user = _security.login_manager.token_callback(token)
user = _security.datastore.find_user(id=data[0])
if utils.md5(user.password) == data[1]:
if user and user.is_authenticated():
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 _check_http_auth():
auth = request.authorization or dict(username=None, password=None)
auth = request.authorization or BasicAuth(username=None, password=None)
user = _security.datastore.find_user(email=auth.username)
if user and utils.verify_password(auth.password, user.password):
if user and utils.verify_and_update_password(auth.password, user):
_security.datastore.commit()
app = current_app._get_current_object()
_request_ctx_stack.top.user = user
identity_changed.send(app, identity=Identity(user.id))
return True
@@ -112,6 +116,36 @@ def auth_token_required(fn):
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::
+159 -54
View File
@@ -9,91 +9,152 @@
:license: MIT, see LICENSE for more details.
"""
from flask import request, current_app
from flask.ext.wtf import Form as BaseForm, TextField, PasswordField, \
SubmitField, HiddenField, Required, BooleanField, EqualTo, Email, \
ValidationError, Length
import inspect
from flask import request, current_app, flash
from flask_wtf import Form as BaseForm
from wtforms import TextField, PasswordField, validators, \
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_password, get_message
from .utils import verify_and_update_password, get_message, config_value, validate_redirect_url
# Convenient reference
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
email_required = Required(message='Email not provided')
_default_field_labels = {
'email': 'Email Address',
'password': 'Password',
'remember_me': 'Remember Me',
'login': 'Login',
'retype_password': 'Retype Password',
'register': 'Register',
'send_confirmation': 'Resend Confirmation Instructions',
'recover_password': 'Recover Password',
'reset_password': 'Reset Password',
'retype_password': 'Retype Password',
'new_password': 'New Password',
'change_password': 'Change Password',
'send_login_link': 'Send Login Link'
}
email_validator = Email(message='Invalid email address')
password_required = Required(message="Password not provided")
class ValidatorMixin(object):
def __call__(self, form, field):
if self.message and self.message.isupper():
self.message = get_message(self.message)[0]
return super(ValidatorMixin, self).__call__(form, field)
class EqualTo(ValidatorMixin, validators.EqualTo):
pass
class Required(ValidatorMixin, validators.Required):
pass
class Email(ValidatorMixin, validators.Email):
pass
class Length(ValidatorMixin, validators.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:
raise ValidationError(field.data +
' is already associated with an account')
if _datastore.get_user(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)
form.user = _datastore.get_user(field.data)
if form.user is None:
raise ValidationError('Specified user does not exist')
raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0])
class Form(BaseForm):
def __init__(self, *args, **kwargs):
kwargs.setdefault('csrf_enabled', not current_app.testing)
if current_app.testing:
self.TIME_LIMIT = None
super(Form, self).__init__(*args, **kwargs)
class EmailFormMixin():
email = TextField("Email Address",
validators=[email_required,
email_validator])
email = TextField(
get_form_field_label('email'),
validators=[email_required, email_validator])
class UserEmailFormMixin():
user = None
email = TextField("Email Address",
validators=[email_required,
email_validator,
valid_user_email])
email = TextField(
get_form_field_label('email'),
validators=[email_required, email_validator, valid_user_email])
class UniqueEmailFormMixin():
email = TextField("Email Address",
validators=[email_required,
email_validator,
unique_user_email])
email = TextField(
get_form_field_label('email'),
validators=[email_required, email_validator, unique_user_email])
class PasswordFormMixin():
password = PasswordField("Password",
validators=[password_required])
password = PasswordField(
get_form_field_label('password'), validators=[password_required])
class NewPasswordFormMixin():
password = PasswordField("Password",
validators=[password_required,
Length(min=6, max=128)])
password = PasswordField(
get_form_field_label('password'),
validators=[password_required, password_length])
class PasswordConfirmFormMixin():
password_confirm = PasswordField("Retype Password",
validators=[EqualTo('password', message="Passwords do not match")])
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):
if field.data and not validate_redirect_url(field.data):
field.data = ''
flash(*get_message('INVALID_REDIRECT'))
raise ValidationError(get_message('INVALID_REDIRECT')[0])
class RegisterFormMixin():
submit = SubmitField("Register")
submit = SubmitField(get_form_field_label('register'))
def to_dict(form):
def is_field_and_user_attr(member):
return isinstance(member, Field) and \
hasattr(_datastore.user_model, member.name)
fields = inspect.getmembers(form, is_field_and_user_attr)
return dict((key, value.data) for key, value in fields)
class SendConfirmationForm(Form, UserEmailFormMixin):
"""The default forgot password form"""
submit = SubmitField("Resend Confirmation Instructions")
submit = SubmitField(get_form_field_label('send_confirmation'))
def __init__(self, *args, **kwargs):
super(SendConfirmationForm, self).__init__(*args, **kwargs)
@@ -112,13 +173,21 @@ class SendConfirmationForm(Form, UserEmailFormMixin):
class ForgotPasswordForm(Form, UserEmailFormMixin):
"""The default forgot password form"""
submit = SubmitField("Recover Password")
submit = SubmitField(get_form_field_label('recover_password'))
def validate(self):
if not super(ForgotPasswordForm, self).validate():
return False
if requires_confirmation(self.user):
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
return False
return True
class PasswordlessLoginForm(Form, UserEmailFormMixin):
"""The passwordless login form"""
submit = SubmitField("Send Login Link")
submit = SubmitField(get_form_field_label('send_login_link'))
def __init__(self, *args, **kwargs):
super(PasswordlessLoginForm, self).__init__(*args, **kwargs)
@@ -134,32 +203,40 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
class LoginForm(Form, NextFormMixin):
"""The default login form"""
email = TextField('Email Address')
password = PasswordField('Password')
remember = BooleanField("Remember Me")
submit = SubmitField("Login")
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)
if not self.next.data:
self.next.data = request.args.get('next', '')
self.remember.default = config_value('DEFAULT_REMEMBER_ME')
def validate(self):
super(LoginForm, self).validate()
if not super(LoginForm, self).validate():
return False
if self.email.data.strip() == '':
self.email.errors.append('Email not provided')
self.email.errors.append(get_message('EMAIL_NOT_PROVIDED')[0])
return False
if self.password.data.strip() == '':
self.password.errors.append('Password not provided')
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
return False
self.user = _datastore.find_user(email=self.email.data)
self.user = _datastore.get_user(self.email.data)
if self.user is None:
self.email.errors.append('Specified user does not exist')
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
return False
if not verify_password(self.password.data, self.user.password):
self.password.errors.append('Invalid password')
if not self.user.password:
self.password.errors.append(get_message('PASSWORD_NOT_SET')[0])
return False
if not verify_and_update_password(self.password.data, self.user):
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])
@@ -172,16 +249,44 @@ class LoginForm(Form, NextFormMixin):
class ConfirmRegisterForm(Form, RegisterFormMixin,
UniqueEmailFormMixin, NewPasswordFormMixin):
def to_dict(self):
return dict(email=self.email.data,
password=self.password.data)
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
pass
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin,
NextFormMixin):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
if not self.next.data:
self.next.data = request.args.get('next', '')
class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
"""The default reset password form"""
submit = SubmitField("Reset Password")
submit = SubmitField(get_form_field_label('reset_password'))
class ChangePasswordForm(Form, PasswordFormMixin):
"""The default change password form"""
new_password = PasswordField(
get_form_field_label('new_password'),
validators=[password_required, password_length])
new_password_confirm = PasswordField(
get_form_field_label('retype_password'),
validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
submit = SubmitField(get_form_field_label('change_password'))
def validate(self):
if not super(ChangePasswordForm, self).validate():
return False
if not verify_and_update_password(self.password.data, current_user):
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False
if self.password.data.strip() == self.new_password.data.strip():
self.password.errors.append(get_message('PASSWORD_IS_THE_SAME')[0])
return False
return True
+5 -6
View File
@@ -9,12 +9,12 @@
:license: MIT, see LICENSE for more details.
"""
from flask import request, current_app as app
from flask import current_app as app
from werkzeug.local import LocalProxy
from .signals import login_instructions_sent
from .utils import send_mail, url_for_security, get_token_status, \
config_value
config_value
# Convenient references
@@ -30,14 +30,13 @@ def send_login_instructions(user):
:param token: The login token
"""
token = generate_login_token(user)
url = url_for_security('token_login', token=token)
login_link = request.url_root[:-1] + url
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(dict(user=user, login_token=token),
app=app._get_current_object())
login_instructions_sent.send(app._get_current_object(),
user=user, login_token=token)
def generate_login_token(user):
+10 -9
View File
@@ -9,12 +9,12 @@
:license: MIT, see LICENSE for more details.
"""
from flask import current_app as app, request
from flask import current_app as app
from werkzeug.local import LocalProxy
from .signals import password_reset, reset_password_instructions_sent
from .utils import send_mail, md5, encrypt_password, url_for_security, \
get_token_status, config_value
get_token_status, config_value
# Convenient references
@@ -29,15 +29,14 @@ def send_reset_password_instructions(user):
:param user: The user to send the instructions to
"""
token = generate_reset_password_token(user)
url = url_for_security('reset_password', token=token)
reset_link = request.url_root[:-1] + url
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(dict(user=user, token=token),
app=app._get_current_object())
reset_password_instructions_sent.send(app._get_current_object(),
user=user, token=token)
def send_password_reset_notice(user):
@@ -45,8 +44,9 @@ def send_password_reset_notice(user):
:param user: The user to send the notice to
"""
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
'reset_notice', user=user)
if config_value('SEND_PASSWORD_RESET_NOTICE_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
'reset_notice', user=user)
def generate_reset_password_token(user):
@@ -68,6 +68,7 @@ def reset_password_token_status(token):
"""
return get_token_status(token, 'reset', 'RESET_PASSWORD')
def update_password(user, password):
"""Update the specified user's password
@@ -77,4 +78,4 @@ def update_password(user, password):
user.password = encrypt_password(password)
_datastore.put(user)
send_password_reset_notice(user)
password_reset.send(user, app=app._get_current_object())
password_reset.send(app._get_current_object(), user=user)
+6 -5
View File
@@ -15,7 +15,7 @@ from werkzeug.local import LocalProxy
from .confirmable import generate_confirmation_link
from .signals import user_registered
from .utils import do_flash, get_message, send_mail, encrypt_password, \
config_value
config_value
# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
@@ -33,10 +33,11 @@ def register_user(**kwargs):
confirmation_link, token = generate_confirmation_link(user)
do_flash(*get_message('CONFIRM_REGISTRATION', email=user.email))
user_registered.send(dict(user=user, confirm_token=token),
app=app._get_current_object())
user_registered.send(app._get_current_object(),
user=user, confirm_token=token)
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
user=user, confirmation_link=confirmation_link)
if config_value('SEND_REGISTER_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
user=user, confirmation_link=confirmation_link)
return user
+12 -10
View File
@@ -8,6 +8,8 @@
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from __future__ import print_function
try:
import simplejson as json
except ImportError:
@@ -26,7 +28,7 @@ _datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
def pprint(obj):
print json.dumps(obj, sort_keys=True, indent=4)
print(json.dumps(obj, sort_keys=True, indent=4))
def commit(fn):
@@ -40,9 +42,9 @@ class CreateUserCommand(Command):
"""Create a user"""
option_list = (
Option('-e', '--email', dest='email', default=None),
Option('-e', '--email', dest='email', default=None),
Option('-p', '--password', dest='password', default=None),
Option('-a', '--active', dest='active', default=''),
Option('-a', '--active', dest='active', default=''),
)
@commit
@@ -59,11 +61,11 @@ class CreateUserCommand(Command):
if form.validate():
kwargs['password'] = encrypt_password(kwargs['password'])
_datastore.create_user(**kwargs)
print 'User created successfully.'
print('User created successfully.')
kwargs['password'] = '****'
pprint(kwargs)
else:
print 'Error creating user'
print('Error creating user')
pprint(form.errors)
@@ -78,7 +80,7 @@ class CreateRoleCommand(Command):
@commit
def run(self, **kwargs):
_datastore.create_role(**kwargs)
print 'Role "%(name)s" created successfully.' % kwargs
print('Role "%(name)s" created successfully.' % kwargs)
class _RoleCommand(Command):
@@ -94,7 +96,7 @@ class AddRoleCommand(_RoleCommand):
@commit
def run(self, 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):
@@ -103,7 +105,7 @@ class RemoveRoleCommand(_RoleCommand):
@commit
def run(self, 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):
@@ -118,7 +120,7 @@ class DeactivateUserCommand(_ToggleActiveCommand):
@commit
def run(self, 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):
@@ -127,4 +129,4 @@ class ActivateUserCommand(_ToggleActiveCommand):
@commit
def run(self, user_identifier):
_datastore.activate_user(user_identifier)
print "User '%s' has been activated" % user_identifier
print("User '%s' has been activated" % user_identifier)
+2 -1
View File
@@ -11,7 +11,6 @@
import blinker
signals = blinker.Namespace()
user_registered = signals.signal("user-registered")
@@ -24,4 +23,6 @@ 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")
+4 -4
View File
@@ -1,9 +1,9 @@
{% if security.registerable or security.recoverable or security.confirmabled %}
{% if security.registerable or security.recoverable or security.confirmable %}
<h2>Menu</h2>
<ul>
<li><a href="{{ url_for_security('login') }}">Login</a></li>
<li><a href="{{ url_for_security('login') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">Login</a></li>
{% if security.registerable %}
<li><a href="{{ url_for_security('register') }}">Register</a><br/></li>
<li><a href="{{ url_for_security('register') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">Register</a><br/></li>
{% endif %}
{% if security.recoverable %}
<li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li>
@@ -12,4 +12,4 @@
<li><a href="{{ url_for_security('send_confirmation') }}">Confirm account</a></li>
{% endif %}
</ul>
{% endif %}
{% endif %}
@@ -0,0 +1,11 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% include "security/_messages.html" %}
<h1>Change password</h1>
<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
{{ change_password_form.hidden_tag() }}
{{ render_field_with_errors(change_password_form.password) }}
{{ render_field_with_errors(change_password_form.new_password) }}
{{ render_field_with_errors(change_password_form.new_password_confirm) }}
{{ render_field(change_password_form.submit) }}
</form>
@@ -0,0 +1,4 @@
<p>Your password has been changed.</p>
{% if security.recoverable %}
<p>If you did not change your password, <a href="{{ url_for_security('forgot_password', _external=True) }}">click here to reset it</a>.</p>
{% endif %}
@@ -0,0 +1,5 @@
Your password has been changed
{% if security.recoverable %}
If you did not change your password, click the link below to reset it.
{{ url_for_security('forgot_password', _external=True) }}
{% endif %}
+145 -31
View File
@@ -12,20 +12,24 @@
import base64
import hashlib
import hmac
import sys
try:
from urlparse import urlsplit
except ImportError: # pragma: no cover
from urllib.parse import urlsplit
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.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, reset_password_instructions_sent, \
login_instructions_sent
from .signals import user_registered, login_instructions_sent, reset_password_instructions_sent
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
@@ -34,16 +38,36 @@ _datastore = LocalProxy(lambda: _security.datastore)
_pwd_context = LocalProxy(lambda: _security.pwd_context)
PY3 = sys.version_info[0] == 3
def login_user(user, remember=True):
"""Performs the login and sends the appropriate signal."""
if PY3: # pragma: no cover
string_types = str, # pragma: no flakes
text_type = str # pragma: no flakes
else: # pragma: no cover
string_types = basestring, # pragma: no flakes
text_type = unicode # pragma: no flakes
if not _login_user(user, remember):
def login_user(user, remember=None):
"""Performs the login routine.
:param user: The user to login
:param remember: Flag specifying if the remember cookie should be set. Defaults to ``False``
"""
if remember is None:
remember = config_value('DEFAULT_REMEMBER_ME')
if not _login_user(user, remember): # pragma: no cover
return False
if _security.trackable:
if 'X-Forwarded-For' not in request.headers:
remote_addr = request.remote_addr or 'untrackable'
else:
remote_addr = request.headers.getlist("X-Forwarded-For")[0]
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
@@ -60,6 +84,8 @@ def login_user(user, remember=True):
def logout_user():
"""Logs out the current. This will also clean up the remember me cookie if it exists."""
for key in ('identity.name', 'identity.auth_type'):
session.pop(key, None)
identity_changed.send(current_app._get_current_object(),
@@ -68,28 +94,75 @@ def logout_user():
def get_hmac(password):
if _security.password_hash == 'plaintext':
return password
"""Returns a Base64 encoded HMAC+SHA512 of the password signed with the salt specified
by ``SECURITY_PASSWORD_SALT``.
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 '
:param password: The password to sign
"""
salt = _security.password_salt
if salt is None:
raise RuntimeError(
'The configuration value `SECURITY_PASSWORD_SALT` must '
'not be None when the value of `SECURITY_PASSWORD_HASH` is '
'set to "%s"' % _security.password_hash)
h = hmac.new(_security.password_salt, password, hashlib.sha512)
h = hmac.new(encode_string(salt), encode_string(password), hashlib.sha512)
return base64.b64encode(h.digest())
def verify_password(password, password_hash):
return _pwd_context.verify(get_hmac(password), password_hash)
"""Returns ``True`` if the password matches the supplied hash.
:param password: A plaintext password to verify
:param password_hash: The expected hash value of the password (usually from your database)
"""
if _security.password_hash != 'plaintext':
password = get_hmac(password)
return _pwd_context.verify(password, password_hash)
def verify_and_update_password(password, user):
"""Returns ``True`` if the password is valid for the specified user. Additionally, the hashed
password in the database is updated if the hashing algorithm happens to have changed.
:param password: A plaintext password to verify
:param user: The user to verify against
"""
if _pwd_context.identify(user.password) != 'plaintext':
password = get_hmac(password)
verified, new_password = _pwd_context.verify_and_update(password, user.password)
if verified and new_password:
user.password = encrypt_password(password)
_datastore.put(user)
return verified
def encrypt_password(password):
return _pwd_context.encrypt(get_hmac(password))
"""Encrypts the specified plaintext password using the configured encryption options.
:param password: The plaintext password to encrypt
"""
if _security.password_hash == 'plaintext':
return password
signed = get_hmac(password).decode('ascii')
return _pwd_context.encrypt(signed)
def encode_string(string):
"""Encodes a string to bytes, if it isn't already.
:param string: The string to encode"""
if isinstance(string, text_type):
string = string.encode('utf-8')
return string
def md5(data):
return hashlib.md5(data).hexdigest()
return hashlib.md5(encode_string(data)).hexdigest()
def do_flash(message, category=None):
@@ -134,11 +207,35 @@ def url_for_security(endpoint, **values):
return url_for(endpoint, **values)
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('SECURITY_POST_LOGIN_VIEW'))
def validate_redirect_url(url):
if url is None or url.strip() == '':
return False
url_next = urlsplit(url)
url_base = urlsplit(request.host_url)
if (url_next.netloc or url_next.scheme) and url_next.netloc != url_base.netloc:
return False
return True
def get_post_action_redirect(config_key, declared=None):
urls = [
get_url(request.args.get('next')),
get_url(request.form.get('next')),
find_redirect(config_key)
]
if declared:
urls.insert(0, declared)
for url in urls:
if validate_redirect_url(url):
return url
def get_post_login_redirect(declared=None):
return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW', declared)
def get_post_register_redirect(declared=None):
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW', declared)
def find_redirect(key):
@@ -184,9 +281,8 @@ def config_value(key, app=None, default=None):
def get_max_age(key, app=None):
now = datetime.utcnow()
expires = now + get_within_delta(key + '_WITHIN', app)
return int(expires.strftime('%s')) - int(now.strftime('%s'))
td = get_within_delta(key + '_WITHIN', app)
return td.seconds + td.days * 24 * 3600
def get_within_delta(key, app=None):
@@ -238,6 +334,14 @@ def send_mail(subject, recipient, template, **context):
def get_token_status(token, serializer, max_age=None):
"""Get the status of a token.
:param token: The token to check
:param serializer: The name of the seriailzer. Can be one of the
following: ``confirm``, ``login``, ``reset``
:param max_age: The name of the max age config option. Can be on of
the following: ``CONFIRM_EMAIL``, ``LOGIN``, ``RESET_PASSWORD``
"""
serializer = getattr(_security, serializer + '_serializer')
max_age = get_max_age(max_age)
user, data = None, None
@@ -248,7 +352,7 @@ def get_token_status(token, serializer, max_age=None):
except SignatureExpired:
d, data = serializer.loads_unsafe(token)
expired = True
except BadSignature:
except (BadSignature, TypeError, ValueError):
invalid = True
if data:
@@ -258,11 +362,21 @@ def get_token_status(token, serializer, max_age=None):
return expired, invalid, user
def get_identity_attributes(app=None):
app = app or current_app
attrs = app.config['SECURITY_USER_IDENTITY_ATTRIBUTES']
try:
attrs = [f.strip() for f in attrs.split(',')]
except AttributeError:
pass
return attrs
@contextmanager
def capture_passwordless_login_requests():
login_requests = []
def _on(data, app):
def _on(app, **data):
login_requests.append(data)
login_instructions_sent.connect(_on)
@@ -282,7 +396,7 @@ def capture_registrations():
"""
registrations = []
def _on(data, app):
def _on(app, **data):
registrations.append(data)
user_registered.connect(_on)
@@ -302,8 +416,8 @@ def capture_reset_password_requests(reset_password_sent_at=None):
"""
reset_requests = []
def _on(request, app):
reset_requests.append(request)
def _on(app, **data):
reset_requests.append(data)
reset_password_instructions_sent.connect(_on)
+114 -62
View File
@@ -9,25 +9,24 @@
:license: MIT, see LICENSE for more details.
"""
from flask import current_app, redirect, request, render_template, jsonify, \
after_this_request, Blueprint
from flask import current_app, redirect, request, 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
confirm_user, confirm_email_token_status
from .decorators import login_required, anonymous_user_required
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
from .passwordless import send_login_instructions, \
login_token_status
login_token_status
from .recoverable import reset_password_token_status, \
send_reset_password_instructions, update_password
send_reset_password_instructions, update_password
from .changeable import change_user_password
from .registerable import register_user
from .utils import get_url, get_post_login_redirect, do_flash, \
get_message, login_user, logout_user, url_for_security as url_for
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'])
@@ -35,7 +34,7 @@ _security = LocalProxy(lambda: current_app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
def _render_json(form, include_auth_token=False):
def _render_json(form, include_user=True, include_auth_token=False):
has_errors = len(form.errors) > 0
if has_errors:
@@ -43,7 +42,9 @@ def _render_json(form, include_auth_token=False):
response = dict(errors=form.errors)
else:
code = 200
response = dict(user=dict(id=str(form.user.id)))
response = dict()
if include_user:
response['user'] = dict(id=str(form.user.id))
if include_auth_token:
token = form.user.get_auth_token()
response['user']['authentication_token'] = token
@@ -64,46 +65,46 @@ def _ctx(endpoint):
def login():
"""View function for login view"""
form_class = _security.login_form
if request.json:
form = LoginForm(MultiDict(request.json))
form = form_class(MultiDict(request.json))
else:
form = LoginForm()
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 ''
return redirect(get_post_login_redirect(form.next.data))
if request.json:
return _render_json(form, True)
return _render_json(form, include_auth_token=True)
return render_template('security/login_user.html',
login_user_form=form,
**_ctx('login'))
return _security.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()
if current_user.is_authenticated():
logout_user()
return redirect(request.args.get('next', None) or
get_url(_security.post_logout_view))
@anonymous_user_required
def register():
"""View function which handles a registration request."""
if _security.confirmable or request.json:
form_class = ConfirmRegisterForm
form_class = _security.confirm_register_form
else:
form_class = RegisterForm
form_class = _security.register_form
if request.json:
form_data = MultiDict(request.json)
@@ -121,25 +122,31 @@ def register():
login_user(user)
if not request.json:
post_register_url = get_url(_security.post_register_view)
post_login_url = get_url(_security.post_login_view)
return redirect(post_register_url or post_login_url)
if 'next' in form:
redirect_url = get_post_register_redirect(form.next.data)
else:
redirect_url = get_post_register_redirect()
return redirect(redirect_url)
return _render_json(form, include_auth_token=True)
if request.json:
return _render_json(form)
return render_template('security/register_user.html',
register_user_form=form,
**_ctx('register'))
return _security.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 = PasswordlessLoginForm(MultiDict(request.json))
form = form_class(MultiDict(request.json))
else:
form = PasswordlessLoginForm()
form = form_class()
if form.validate_on_submit():
send_login_instructions(form.user)
@@ -149,9 +156,9 @@ def send_login():
if request.json:
return _render_json(form)
return render_template('security/send_login.html',
send_login_form=form,
**_ctx('send_login'))
return _security.render_template(config_value('SEND_LOGIN_TEMPLATE'),
send_login_form=form,
**_ctx('send_login'))
@anonymous_user_required
@@ -169,7 +176,7 @@ def token_login(token):
if invalid or expired:
return redirect(url_for('login'))
login_user(user, True)
login_user(user)
after_this_request(_commit)
do_flash(*get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'))
@@ -179,10 +186,12 @@ def token_login(token):
def send_confirmation():
"""View function which sends confirmation instructions."""
form_class = _security.send_confirmation_form
if request.json:
form = SendConfirmationForm(MultiDict(request.json))
form = form_class(MultiDict(request.json))
else:
form = SendConfirmationForm()
form = form_class()
if form.validate_on_submit():
send_confirmation_instructions(form.user)
@@ -192,18 +201,18 @@ def send_confirmation():
if request.json:
return _render_json(form)
return render_template('security/send_confirmation.html',
send_confirmation_form=form,
**_ctx('send_confirmation'))
return _security.render_template(config_value('SEND_CONFIRMATION_TEMPLATE'),
send_confirmation_form=form,
**_ctx('send_confirmation'))
@anonymous_user_required
def confirm_email(token):
"""View function which handles a email confirmation request."""
expired, invalid, user = confirm_email_token_status(token)
if invalid:
if not user or invalid:
invalid = True
do_flash(*get_message('INVALID_CONFIRMATION_TOKEN'))
if expired:
send_confirmation_instructions(user)
@@ -213,10 +222,17 @@ def confirm_email(token):
return redirect(get_url(_security.confirm_error_view) or
url_for('send_confirmation'))
confirm_user(user)
login_user(user, True)
after_this_request(_commit)
do_flash(*get_message('EMAIL_CONFIRMED'))
if user != current_user:
logout_user()
login_user(user)
if confirm_user(user):
after_this_request(_commit)
msg = 'EMAIL_CONFIRMED'
else:
msg = 'ALREADY_CONFIRMED'
do_flash(*get_message(msg))
return redirect(get_url(_security.post_confirm_view) or
get_url(_security.post_login_view))
@@ -225,10 +241,12 @@ def confirm_email(token):
def forgot_password():
"""View function that handles a forgotten password request."""
form_class = _security.forgot_password_form
if request.json:
form = ForgotPasswordForm(MultiDict(request.json))
form = form_class(MultiDict(request.json))
else:
form = ForgotPasswordForm()
form = form_class()
if form.validate_on_submit():
send_reset_password_instructions(form.user)
@@ -236,11 +254,11 @@ def forgot_password():
do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email))
if request.json:
return _render_json(form)
return _render_json(form, include_user=False)
return render_template('security/forgot_password.html',
forgot_password_form=form,
**_ctx('forgot_password'))
return _security.render_template(config_value('FORGOT_PASSWORD_TEMPLATE'),
forgot_password_form=form,
**_ctx('forgot_password'))
@anonymous_user_required
@@ -257,20 +275,48 @@ def reset_password(token):
if invalid or expired:
return redirect(url_for('forgot_password'))
form = ResetPasswordForm()
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, True)
login_user(user)
return redirect(get_url(_security.post_reset_view) or
get_url(_security.post_login_view))
return render_template('security/reset_password.html',
reset_password_form=form,
reset_password_token=token,
**_ctx('reset_password'))
return _security.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:
form.user = current_user
return _render_json(form)
return _security.render_template(config_value('CHANGE_PASSWORD_TEMPLATE'),
change_password_form=form,
**_ctx('change_password'))
def create_blueprint(state, import_name):
@@ -278,6 +324,7 @@ def create_blueprint(state, import_name):
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)
@@ -306,6 +353,11 @@ def create_blueprint(state, import_name):
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'],
+10
View File
@@ -0,0 +1,10 @@
Flask-SQLAlchemy>=1.0
bcrypt>=1.0.2
flask-mongoengine>=0.7.0
flask-peewee>=0.6.5
pytest>=2.5.2
pytest-cache>=1.0
pytest-cov>=1.6
pytest-flakes>=0.2
pytest-pep8>=1.0.5
tox>=1.7.0
+7 -6
View File
@@ -1,6 +1,7 @@
Flask==0.8
Flask-Login==0.1
Flask-Principal==0.2
Flask-Script==0.3.2
Flask-WTF==0.5.4
passlib=1.5.3
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
+14 -1
View File
@@ -3,4 +3,17 @@ source-dir = docs/
build-dir = docs/_build
[upload_sphinx]
upload-dir = docs/_build/html
upload-dir = docs/_build/html
[pytest]
pep8maxlinelength = 99
pep8ignore =
docs/* ALL
scripts/* ALL
flakes-ignore =
ImportStarUsed
flask_security/__init__.py UnusedImport
docs/* ALL
scripts/* ALL
+44 -36
View File
@@ -1,55 +1,63 @@
"""
Flask-Security
==============
Flask-Security is a Flask extension that aims to add quick and simple security
to your Flask applications.
Resources
---------
* `Documentation <http://packages.python.org/Flask-Security/>`_
* `Issue Tracker <https://github.com/mattupstate/flask-security/issues>`_
* `Source <https://github.com/mattupstate/flask-security>`_
* `Development Version
<https://github.com/mattupstate/flask-security/raw/develop#egg=Flask-Security-dev>`_
"""
from setuptools import setup
import multiprocessing # pragma: no flakes
import sys
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
def get_requirements(suffix=''):
with open('requirements%s.txt' % suffix) as f:
rv = f.read().splitlines()
return rv
def get_long_description():
with open('README.rst') as f:
rv = f.read()
return rv
class PyTest(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = [
'-xrs',
'--cov', 'flask_security',
'--cov-report', 'term-missing',
'--pep8',
'--flakes',
'--clearcache'
]
self.test_suite = True
def run_tests(self):
import pytest
errno = pytest.main(self.test_args)
sys.exit(errno)
setup(
name='Flask-Security',
version='1.5.2',
version='1.7.4',
url='https://github.com/mattupstate/flask-security',
license='MIT',
author='Matt Wright',
author_email='matt@nobien.net',
description='Simple security for Flask apps',
long_description=__doc__,
packages=[
'flask_security'
],
long_description=get_long_description(),
packages=find_packages(),
zip_safe=False,
include_package_data=True,
platforms='any',
install_requires=[
'Flask>=0.8',
'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',
],
test_suite='nose.collector',
tests_require=[
'nose',
'Flask-SQLAlchemy',
'Flask-MongoEngine',
'py-bcrypt',
'simplejson'
],
install_requires=get_requirements(),
tests_require=get_requirements('-dev'),
cmdclass={'test': PyTest},
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
-79
View File
@@ -1,79 +0,0 @@
# -*- coding: utf-8 -*-
from unittest import TestCase
from tests.test_app.sqlalchemy import create_app
class SecurityTest(TestCase):
AUTH_CONFIG = None
def setUp(self):
super(SecurityTest, self).setUp()
app = self._create_app(self.AUTH_CONFIG or {})
app.debug = False
app.config['TESTING'] = True
self.app = app
self.client = app.test_client()
def _create_app(self, auth_config, register_blueprint=True):
return create_app(auth_config, register_blueprint)
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):
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)
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')
r = self._post(endpoint or '/login', data=data, **kwargs)
return r
def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None):
data = """
{
"email": "%s",
"password": "%s"
}
"""
return self._post(endpoint or '/login',
content_type="application/json",
data=data % (email, password))
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
-485
View File
@@ -1,485 +0,0 @@
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 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_json(self):
data = '{ "email": "dude@lp.com", "password": "password" }'
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 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.client.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.client.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)
self.assertIn('Invalid confirmation token', 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'))
self.assertIn('Specified user does not exist', 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)
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.client.post('/register', data=data, follow_redirects=True)
self.assertIn(e, r.data)
class RecoverableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
'SECURITY_POST_FORGOT_VIEW': '/'
}
def test_reset_view(self):
with capture_reset_password_requests() as requests:
r = self.client.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.client.post('/reset', data=dict(email='joe@lp.com'))
self.assertEqual(len(outbox), 1)
def test_forgot_password_json(self):
r = self.client.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.client.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.client.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.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
time.sleep(1)
r = self.client.post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('You did not reset your password within', r.data)
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.client.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"}'
r = self.client.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.client.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.client.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.client.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.client.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.client.post('/reset', data=dict(email='matt@lp.com'))
self.assertTrue(self.mail_sent)
class NoBlueprintTests(SecurityTest):
AUTH_CONFIG = {
'USER_COUNT': 1
}
def _create_app(self, auth_config):
return super(NoBlueprintTests, self)._create_app(auth_config, False)
def test_login_endpoint_is_404(self):
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)
+287
View File
@@ -0,0 +1,287 @@
# -*- coding: utf-8 -*-
"""
conftest
~~~~~~~~
Test fixtures and what not
"""
import os
import tempfile
import time
import pytest
from flask import Flask, render_template
from flask_mail import Mail
from flask_security import Security, MongoEngineUserDatastore, SQLAlchemyUserDatastore, \
PeeweeUserDatastore, UserMixin, RoleMixin, http_auth_required, login_required, \
auth_token_required, auth_required, roles_required, roles_accepted
from utils import populate_data, Response
@pytest.fixture()
def app(request):
app = Flask(__name__)
app.response_class = Response
app.debug = True
app.config['SECRET_KEY'] = 'secret'
app.config['TESTING'] = True
app.config['LOGIN_DISABLED'] = False
app.config['WTF_CSRF_ENABLED'] = False
for opt in ['changeable', 'recoverable', 'registerable',
'trackable', 'passwordless', 'confirmable']:
app.config['SECURITY_' + opt.upper()] = opt in request.keywords
if 'settings' in request.keywords:
for key, value in request.keywords['settings'].kwargs.items():
app.config['SECURITY_' + key.upper()] = value
mail = Mail(app)
app.mail = mail
@app.route('/')
def index():
return render_template('index.html', content='Home Page')
@app.route('/profile')
@login_required
def profile():
return render_template('index.html', content='Profile Page')
@app.route('/post_login')
@login_required
def post_login():
return render_template('index.html', content='Post Login')
@app.route('/http')
@http_auth_required
def http():
return 'HTTP Authentication'
@app.route('/http_custom_realm')
@http_auth_required('My Realm')
def http_custom_realm():
return render_template('index.html', content='HTTP Authentication')
@app.route('/token', methods=['GET', 'POST'])
@auth_token_required
def token():
return render_template('index.html', content='Token Authentication')
@app.route('/multi_auth')
@auth_required('session', 'token', 'basic')
def multi_auth():
return render_template('index.html', content='Session, Token, Basic auth')
@app.route('/post_logout')
def post_logout():
return render_template('index.html', content='Post Logout')
@app.route('/post_register')
def post_register():
return render_template('index.html', content='Post Register')
@app.route('/admin')
@roles_required('admin')
def admin():
return render_template('index.html', content='Admin Page')
@app.route('/admin_and_editor')
@roles_required('admin', 'editor')
def admin_and_editor():
return render_template('index.html', content='Admin and Editor Page')
@app.route('/admin_or_editor')
@roles_accepted('admin', 'editor')
def admin_or_editor():
return render_template('index.html', content='Admin or Editor Page')
@app.route('/unauthorized')
def unauthorized():
return render_template('unauthorized.html')
@app.route('/page1')
def page_1():
return 'Page 1'
return app
@pytest.fixture()
def mongoengine_datastore(request, app):
from flask_mongoengine import MongoEngine
db_name = 'flask_security_test_%s' % str(time.time()).replace('.', '_')
app.config['MONGODB_SETTINGS'] = {
'db': db_name,
'host': 'localhost',
'port': 27017,
'alias': db_name
}
db = MongoEngine(app)
class Role(db.Document, RoleMixin):
name = db.StringField(required=True, unique=True, max_length=80)
description = db.StringField(max_length=255)
meta = {"db_alias": db_name}
class User(db.Document, UserMixin):
email = db.StringField(unique=True, max_length=255)
username = db.StringField(max_length=255)
password = db.StringField(required=False, max_length=255)
last_login_at = db.DateTimeField()
current_login_at = db.DateTimeField()
last_login_ip = db.StringField(max_length=100)
current_login_ip = db.StringField(max_length=100)
login_count = db.IntField()
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
meta = {"db_alias": db_name}
request.addfinalizer(lambda: db.connection.drop_database(db_name))
return MongoEngineUserDatastore(db, User, Role)
@pytest.fixture()
def sqlalchemy_datastore(request, app, tmpdir):
from flask_sqlalchemy import SQLAlchemy
f, path = tempfile.mkstemp(prefix='flask-security-test-db', suffix='.db', dir=str(tmpdir))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + path
db = SQLAlchemy(app)
roles_users = db.Table(
'roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
username = db.Column(db.String(255))
password = db.Column(db.String(255))
last_login_at = db.Column(db.DateTime())
current_login_at = db.Column(db.DateTime())
last_login_ip = db.Column(db.String(100))
current_login_ip = db.Column(db.String(100))
login_count = db.Column(db.Integer)
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
with app.app_context():
db.create_all()
request.addfinalizer(lambda: os.remove(path))
return SQLAlchemyUserDatastore(db, User, Role)
@pytest.fixture()
def peewee_datastore(request, app, tmpdir):
from peewee import TextField, DateTimeField, IntegerField, BooleanField, ForeignKeyField
from flask_peewee.db import Database
f, path = tempfile.mkstemp(prefix='flask-security-test-db', suffix='.db', dir=str(tmpdir))
app.config['DATABASE'] = {
'name': path,
'engine': 'peewee.SqliteDatabase'
}
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
description = TextField(null=True)
class User(db.Model, UserMixin):
email = TextField()
username = TextField()
password = TextField(null=True)
last_login_at = DateTimeField(null=True)
current_login_at = DateTimeField(null=True)
last_login_ip = TextField(null=True)
current_login_ip = TextField(null=True)
login_count = IntegerField(null=True)
active = BooleanField(default=True)
confirmed_at = DateTimeField(null=True)
class UserRoles(db.Model):
""" Peewee does not have built-in many-to-many support, so we have to
create this mapping class to link users to roles."""
user = ForeignKeyField(User, related_name='roles')
role = ForeignKeyField(Role, related_name='users')
name = property(lambda self: self.role.name)
description = property(lambda self: self.role.description)
with app.app_context():
for Model in (Role, User, UserRoles):
Model.create_table()
request.addfinalizer(lambda: os.remove(path))
return PeeweeUserDatastore(db, User, Role, UserRoles)
@pytest.fixture()
def sqlalchemy_app(app, sqlalchemy_datastore):
def create():
app.security = Security(app, datastore=sqlalchemy_datastore)
return app
return create
@pytest.fixture()
def peewee_app(app, peewee_datastore):
def create():
app.security = Security(app, datastore=peewee_datastore)
return app
return create
@pytest.fixture()
def mongoengine_app(app, mongoengine_datastore):
def create():
app.security = Security(app, datastore=mongoengine_datastore)
return app
return create
@pytest.fixture()
def client(request, sqlalchemy_app):
app = sqlalchemy_app()
populate_data(app)
return app.test_client()
@pytest.fixture()
def get_message(app):
def fn(key, **kwargs):
rv = app.config['SECURITY_MSG_' + key][0] % kwargs
return rv.encode('utf-8')
return fn
@pytest.fixture(params=['sqlalchemy', 'mongoengine', 'peewee'])
def datastore(request, sqlalchemy_datastore, mongoengine_datastore, peewee_datastore):
if request.param == 'sqlalchemy':
rv = sqlalchemy_datastore
elif request.param == 'mongoengine':
rv = mongoengine_datastore
elif request.param == 'peewee':
rv = peewee_datastore
return rv
-236
View File
@@ -1,236 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
import base64
import simplejson as json
from cookielib import Cookie
from werkzeug.utils import parse_cookie
from tests import SecurityTest
def get_cookies(rv):
cookies = {}
for value in rv.headers.get_all("Set-Cookie"):
cookies.update(parse_cookie(value))
return cookies
class DefaultSecurityTests(SecurityTest):
def test_instance(self):
self.assertIsNotNone(self.app)
self.assertIsNotNone(self.app.security)
self.assertIsNotNone(self.app.security.pwd_context)
def test_login_view(self):
r = self._get('/login')
self.assertIn('<h1>Login</h1>', r.data)
def test_authenticate(self):
r = self.authenticate()
self.assertIn('Hello matt@lp.com', r.data)
def test_unprovided_username(self):
r = self.authenticate("")
self.assertIn("Email not provided", r.data)
def test_unprovided_password(self):
r = self.authenticate(password="")
self.assertIn("Password not provided", r.data)
def test_invalid_user(self):
r = self.authenticate(email="bogus@bogus.com")
self.assertIn("Specified user does not exist", r.data)
def test_bad_password(self):
r = self.authenticate(password="bogus")
self.assertIn("Invalid password", r.data)
def test_inactive_user(self):
r = self.authenticate("tiya@lp.com", "password")
self.assertIn("Account is disabled", r.data)
def test_logout(self):
self.authenticate()
r = self.logout()
self.assertIsHomePage(r.data)
def test_unauthorized_access(self):
r = self._get('/profile', follow_redirects=True)
self.assertIn('Please log in to access this page', r.data)
def test_authorized_access(self):
self.authenticate()
r = self._get("/profile")
self.assertIn('profile', r.data)
def test_valid_admin_role(self):
self.authenticate()
r = self._get("/admin")
self.assertIn('Admin Page', r.data)
def test_invalid_admin_role(self):
self.authenticate("joe@lp.com")
r = self._get("/admin", follow_redirects=True)
self.assertIsHomePage(r.data)
def test_roles_accepted(self):
for user in ("matt@lp.com", "joe@lp.com"):
self.authenticate(user)
r = self._get("/admin_or_editor")
self.assertIn('Admin or Editor Page', r.data)
self.logout()
self.authenticate("jill@lp.com")
r = self._get("/admin_or_editor", follow_redirects=True)
self.assertIsHomePage(r.data)
def test_unauthenticated_role_required(self):
r = self._get('/admin', follow_redirects=True)
self.assertIn(self.get_message('UNAUTHORIZED'), r.data)
def test_multiple_role_required(self):
for user in ("matt@lp.com", "joe@lp.com"):
self.authenticate(user)
r = self._get("/admin_and_editor", follow_redirects=True)
self.assertIsHomePage(r.data)
self._get('/logout')
self.authenticate('dave@lp.com')
r = self._get("/admin_and_editor", follow_redirects=True)
self.assertIn('Admin and Editor Page', r.data)
def test_ok_json_auth(self):
r = self.json_authenticate()
data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200)
self.assertIn('authentication_token', data['response']['user'])
def test_invalid_json_auth(self):
r = self.json_authenticate(password='junk')
self.assertIn('"code": 400', r.data)
def test_token_auth_via_querystring_valid_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
r = self._get('/token?auth_token=' + token)
self.assertIn('Token Authentication', r.data)
def test_token_auth_via_header_valid_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
headers = {"Authentication-Token": token}
r = self._get('/token', headers=headers)
self.assertIn('Token Authentication', r.data)
def test_token_auth_via_querystring_invalid_token(self):
r = self._get('/token?auth_token=X')
self.assertEqual(401, r.status_code)
def test_token_auth_via_header_invalid_token(self):
r = self._get('/token', headers={"Authentication-Token": 'X'})
self.assertEqual(401, r.status_code)
def test_http_auth(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password")
})
self.assertIn('HTTP Authentication', r.data)
def test_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_user_deleted_during_session_reverts_to_anonymous_user(self):
self.authenticate()
with self.app.test_request_context('/'):
user = self.app.security.datastore.find_user(email='matt@lp.com')
self.app.security.datastore.delete_user(user)
self.app.security.datastore.commit()
r = self._get('/')
self.assertNotIn('Hello matt@lp.com', r.data)
def test_remember_token(self):
r = self.authenticate(follow_redirects=False)
self.client.cookie_jar.clear_session_cookies()
r = self._get('/profile')
self.assertIn('profile', r.data)
def test_token_loader_does_not_fail_with_invalid_token(self):
c = Cookie(version=0, name='remember_token', value='None', port=None,
port_specified=False, domain='www.example.com',
domain_specified=False, domain_initial_dot=False, path='/',
path_specified=True, secure=False, expires=None,
discard=True, comment=None, comment_url=None,
rest={'HttpOnly': None}, rfc2109=False)
self.client.cookie_jar.set_cookie(c)
r = self._get('/')
self.assertNotIn('BadSignature', r.data)
class MongoEngineSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config):
from tests.test_app.mongoengine import create_app
return create_app(auth_config)
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):
from tests.test_app.mongoengine import create_app
return create_app(auth_config)
@@ -0,0 +1,3 @@
CUSTOM CHANGE PASSWORD
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM FORGOT PASSWORD
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM LOGIN USER
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM REGISTER USER
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM RESET PASSWORD
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM SEND CONFIRMATION
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM SEND LOGIN
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM RESET INSTRUCTIONS
{{ global }}
{{ foo }}
-165
View File
@@ -1,165 +0,0 @@
# -*- coding: utf-8 -*-
from flask import Flask, render_template, current_app
from flask.ext.mail import Mail
from flask.ext.security import login_required, roles_required, roles_accepted
from flask.ext.security.decorators import http_auth_required, \
auth_token_required
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'
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('/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'
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()
-56
View File
@@ -1,56 +0,0 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask.ext.mongoengine import MongoEngine
from flask.ext.security import Security, UserMixin, RoleMixin, \
MongoEngineUserDatastore
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config):
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, MongoEngineUserDatastore(db, User, Role))
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
-61
View File
@@ -1,61 +0,0 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, UserMixin, RoleMixin, \
SQLAlchemyUserDatastore
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config, register_blueprint=True):
app = create_base_app(config)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
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, SQLAlchemyUserDatastore(db, User, Role),
register_blueprint=register_blueprint)
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
+127
View File
@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
"""
test_changeable
~~~~~~~~~~~~~~~
Changeable tests
"""
import pytest
from flask_security.signals import password_changed
from utils import authenticate
pytestmark = pytest.mark.changeable()
def test_recoverable_flag(app, client, get_message):
recorded = []
@password_changed.connect_via(app)
def on_password_changed(app, user):
recorded.append(user)
authenticate(client)
# Test change view
response = client.get('/change', follow_redirects=True)
assert b'Change password' in response.data
# Test wrong original password
response = client.post('/change', data={
'password': 'notpassword',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('INVALID_PASSWORD') in response.data
# Test mismatch
response = client.post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'notnewpassword'
}, follow_redirects=True)
assert get_message('RETYPE_PASSWORD_MISMATCH') in response.data
# Test missing password
response = client.post('/change', data={
'password': ' ',
'new_password': '',
'new_password_confirm': ''
}, follow_redirects=True)
assert get_message('PASSWORD_NOT_PROVIDED') in response.data
# Test bad password
response = client.post('/change', data={
'password': 'password',
'new_password': 'a',
'new_password_confirm': 'a'
}, follow_redirects=True)
assert get_message('PASSWORD_INVALID_LENGTH') in response.data
# Test same as previous
response = client.post('/change', data={
'password': 'password',
'new_password': 'password',
'new_password_confirm': 'password'
}, follow_redirects=True)
assert get_message('PASSWORD_IS_THE_SAME') in response.data
# Test successful submit sends email notification
with app.mail.record_messages() as outbox:
response = client.post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('PASSWORD_CHANGE') in response.data
assert b'Home Page' in response.data
assert len(recorded) == 1
assert len(outbox) == 1
assert "Your password has been changed" in outbox[0].html
# Test JSON
data = ('{"password": "newpassword", "new_password": "newpassword2", '
'"new_password_confirm": "newpassword2"}')
response = client.post('/change', data=data, headers={'Content-Type': 'application/json'})
assert response.status_code == 200
assert response.headers['Content-Type'] == 'application/json'
@pytest.mark.settings(change_url='/custom_change')
def test_custom_change_url(client):
authenticate(client)
response = client.get('/custom_change')
assert response.status_code == 200
@pytest.mark.settings(change_password_template='custom_security/change_password.html')
def test_custom_change_template(client):
authenticate(client)
response = client.get('/change')
assert b'CUSTOM CHANGE PASSWORD' in response.data
@pytest.mark.settings(send_password_change_email=False)
def test_disable_change_emails(app, client):
with app.mail.record_messages() as outbox:
client.post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
assert len(outbox) == 0
@pytest.mark.settings(post_change_view='/profile')
def test_custom_post_change_view(client):
authenticate(client)
response = client.post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
assert b'Profile Page' in response.data
+279
View File
@@ -0,0 +1,279 @@
# -*- coding: utf-8 -*-
"""
test_common
~~~~~~~~~~~
Test common functionality
"""
import base64
try:
from cookielib import Cookie
except ImportError:
from http.cookiejar import Cookie
from utils import authenticate, json_authenticate, logout
def test_login_view(client):
response = client.get('/login')
assert b'<h1>Login</h1>' in response.data
def test_authenticate(client):
response = authenticate(client)
assert response.status_code == 302
response = authenticate(client, follow_redirects=True)
assert b'Hello matt@lp.com' in response.data
def test_authenticate_with_next(client):
data = dict(email='matt@lp.com', password='password')
response = client.post('/login?next=/page1', data=data, follow_redirects=True)
assert b'Page 1' in response.data
def test_authenticate_with_invalid_next(client, get_message):
data = dict(email='matt@lp.com', password='password')
response = client.post('/login?next=http://google.com', data=data)
assert get_message('INVALID_REDIRECT') in response.data
def test_authenticate_with_invalid_malformed_next(client, get_message):
data = dict(email='matt@lp.com', password='password')
response = client.post('/login?next=http:///google.com', data=data)
assert get_message('INVALID_REDIRECT') in response.data
def test_authenticate_case_insensitive_email(app, client):
response = authenticate(client, 'MATT@lp.com', follow_redirects=True)
assert b'Hello matt@lp.com' in response.data
def test_unprovided_username(client, get_message):
response = authenticate(client, "")
assert get_message('EMAIL_NOT_PROVIDED') in response.data
def test_unprovided_password(client, get_message):
response = authenticate(client, password="")
assert get_message('PASSWORD_NOT_PROVIDED') in response.data
def test_invalid_user(client, get_message):
response = authenticate(client, email="bogus@bogus.com")
assert get_message('USER_DOES_NOT_EXIST') in response.data
def test_bad_password(client, get_message):
response = authenticate(client, password="bogus")
assert get_message('INVALID_PASSWORD') in response.data
def test_inactive_user(client, get_message):
response = authenticate(client, "tiya@lp.com", "password")
assert get_message('DISABLED_ACCOUNT') in response.data
def test_unset_password(client, get_message):
response = authenticate(client, "jess@lp.com", "password")
assert get_message('PASSWORD_NOT_SET') in response.data
def test_logout(client):
authenticate(client)
response = logout(client, follow_redirects=True)
assert b'Home Page' in response.data
def test_missing_session_access(client, get_message):
response = client.get('/profile', follow_redirects=True)
assert get_message('LOGIN') in response.data
def test_has_session_access(client):
authenticate(client)
response = client.get("/profile", follow_redirects=True)
assert b'profile' in response.data
def test_authorized_access(client):
authenticate(client)
response = client.get("/admin")
assert b'Admin Page' in response.data
def test_unauthorized_access(client, get_message):
authenticate(client, "joe@lp.com")
response = client.get("/admin", follow_redirects=True)
assert get_message('UNAUTHORIZED') in response.data
def test_roles_accepted(client):
for user in ("matt@lp.com", "joe@lp.com"):
authenticate(client, user)
response = client.get("/admin_or_editor")
assert b'Admin or Editor Page' in response.data
logout(client)
authenticate(client, "jill@lp.com")
response = client.get("/admin_or_editor", follow_redirects=True)
assert b'Home Page' in response.data
def test_unauthenticated_role_required(client, get_message):
response = client.get('/admin', follow_redirects=True)
assert get_message('UNAUTHORIZED') in response.data
def test_multiple_role_required(client):
for user in ("matt@lp.com", "joe@lp.com"):
authenticate(client, user)
response = client.get("/admin_and_editor", follow_redirects=True)
assert b'Home Page' in response.data
client.get('/logout')
authenticate(client, 'dave@lp.com')
response = client.get("/admin_and_editor", follow_redirects=True)
assert b'Admin and Editor Page' in response.data
def test_ok_json_auth(client):
response = json_authenticate(client)
assert response.jdata['meta']['code'] == 200
assert 'authentication_token' in response.jdata['response']['user']
def test_invalid_json_auth(client):
response = json_authenticate(client, password='junk')
assert b'"code": 400' in response.data
def test_token_auth_via_querystring_valid_token(client):
response = json_authenticate(client)
token = response.jdata['response']['user']['authentication_token']
response = client.get('/token?auth_token=' + token)
assert b'Token Authentication' in response.data
def test_token_auth_via_header_valid_token(client):
response = json_authenticate(client)
token = response.jdata['response']['user']['authentication_token']
headers = {"Authentication-Token": token}
response = client.get('/token', headers=headers)
assert b'Token Authentication' in response.data
def test_token_auth_via_querystring_invalid_token(client):
response = client.get('/token?auth_token=X')
assert 401 == response.status_code
def test_token_auth_via_header_invalid_token(client):
response = client.get('/token', headers={"Authentication-Token": 'X'})
assert 401 == response.status_code
def test_http_auth(client):
response = client.get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:password").decode('utf-8')
})
assert b'HTTP Authentication' in response.data
def test_http_auth_no_authorization(client):
response = client.get('/http', headers={})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
def test_invalid_http_auth_invalid_username(client):
response = client.get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"bogus:bogus").decode('utf-8')
})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
def test_invalid_http_auth_bad_password(client):
response = client.get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus").decode('utf-8')
})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
def test_custom_http_auth_realm(client):
response = client.get('/http_custom_realm', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus").decode('utf-8')
})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="My Realm"' == response.headers['WWW-Authenticate']
def test_multi_auth_basic(client):
response = client.get('/multi_auth', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:password").decode('utf-8')
})
assert b'Basic' in response.data
response = client.get('/multi_auth')
assert response.status_code == 401
def test_multi_auth_token(client):
response = json_authenticate(client)
token = response.jdata['response']['user']['authentication_token']
response = client.get('/multi_auth?auth_token=' + token)
assert b'Token' in response.data
def test_multi_auth_session(client):
authenticate(client, )
response = client.get('/multi_auth')
assert b'Session' in response.data
def test_user_deleted_during_session_reverts_to_anonymous_user(app, client):
authenticate(client)
with app.test_request_context('/'):
user = app.security.datastore.find_user(email='matt@lp.com')
app.security.datastore.delete_user(user)
app.security.datastore.commit()
response = client.get('/')
assert b'Hello matt@lp.com' not in response.data
def test_remember_token(client):
response = authenticate(client, follow_redirects=False)
client.cookie_jar.clear_session_cookies()
response = client.get('/profile')
assert b'profile' in response.data
def test_token_loader_does_not_fail_with_invalid_token(client):
c = Cookie(version=0, name='remember_token', value='None', port=None,
port_specified=False, domain='www.example.com',
domain_specified=False, domain_initial_dot=False, path='/',
path_specified=True, secure=False, expires=None,
discard=True, comment=None, comment_url=None,
rest={'HttpOnly': None}, rfc2109=False)
client.cookie_jar.set_cookie(c)
response = client.get('/')
assert b'BadSignature' not in response.data
def test_sending_auth_token_with_json(client):
response = json_authenticate(client)
token = response.jdata['response']['user']['authentication_token']
data = '{"auth_token": "%s"}' % token
response = client.post('/token', data=data, headers={'Content-Type': 'application/json'})
assert b'Token Authentication' in response.data
+45
View File
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""
test_configuration
~~~~~~~~~~~~~~~~~~
Basic configuration tests
"""
import base64
import pytest
from utils import authenticate, logout
@pytest.mark.settings(
logout_url='/custom_logout',
login_url='/custom_login',
post_login_view='/post_login',
post_logout_view='/post_logout',
default_http_auth_realm='Custom Realm')
def test_view_configuration(client):
response = client.get('/custom_login')
assert b"<h1>Login</h1>" in response.data
response = authenticate(client, endpoint='/custom_login')
assert 'location' in response.headers
assert response.headers['Location'] == 'http://localhost/post_login'
response = logout(client, endpoint='/custom_logout')
assert 'location' in response.headers
assert response.headers['Location'] == 'http://localhost/post_logout'
response = client.get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus")
})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="Custom Realm"' == response.headers['WWW-Authenticate']
@pytest.mark.settings(login_user_template='custom_security/login_user.html')
def test_template_configuration(client):
response = client.get('/login')
assert b'CUSTOM LOGIN USER' in response.data
+161
View File
@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
"""
test_confirmable
~~~~~~~~~~~~~~~~
Confirmable tests
"""
import time
import pytest
from flask_security.signals import user_confirmed, confirm_instructions_sent
from flask_security.utils import capture_registrations
from utils import authenticate, logout
pytestmark = pytest.mark.confirmable()
@pytest.mark.registerable()
def test_confirmable_flag(app, client, sqlalchemy_datastore, get_message):
recorded_confirms = []
recorded_instructions_sent = []
@user_confirmed.connect_via(app)
def on_confirmed(app, user):
recorded_confirms.append(user)
@confirm_instructions_sent.connect_via(app)
def on_instructions_sent(app, user):
recorded_instructions_sent.append(user)
# Test login before confirmation
email = 'dude@lp.com'
with capture_registrations() as registrations:
data = dict(email=email, password='password', next='')
response = client.post('/register', data=data)
assert response.status_code == 302
response = authenticate(client, email=email)
assert get_message('CONFIRMATION_REQUIRED') in response.data
# Test invalid token
response = client.get('/confirm/bogus', follow_redirects=True)
assert get_message('INVALID_CONFIRMATION_TOKEN') in response.data
# Test JSON
response = client.post('/confirm', data='{"email": "matt@lp.com"}', headers={
'Content-Type': 'application/json'
})
assert response.status_code == 200
assert response.headers['Content-Type'] == 'application/json'
assert 'user' in response.jdata['response']
assert len(recorded_instructions_sent) == 1
# Test ask for instructions with invalid email
response = client.post('/confirm', data=dict(email='bogus@bogus.com'))
assert get_message('USER_DOES_NOT_EXIST') in response.data
# Test resend instructions
response = client.post('/confirm', data=dict(email=email))
assert get_message('CONFIRMATION_REQUEST', email=email) in response.data
assert len(recorded_instructions_sent) == 2
# Test confirm
token = registrations[0]['confirm_token']
response = client.get('/confirm/' + token, follow_redirects=True)
assert get_message('EMAIL_CONFIRMED') in response.data
assert len(recorded_confirms) == 1
# Test already confirmed
response = client.get('/confirm/' + token, follow_redirects=True)
assert get_message('ALREADY_CONFIRMED') in response.data
# Test already confirmed when asking for confirmation instructions
logout(client)
response = client.get('/confirm')
assert response.status_code == 200
response = client.post('/confirm', data=dict(email=email))
assert get_message('ALREADY_CONFIRMED') in response.data
# Test user was deleted before confirmation
with capture_registrations() as registrations:
data = dict(email='mary@lp.com', password='password', next='')
client.post('/register', data=data)
user = registrations[0]['user']
token = registrations[0]['confirm_token']
with app.app_context():
sqlalchemy_datastore.delete(user)
sqlalchemy_datastore.commit()
response = client.get('/confirm/' + token, follow_redirects=True)
assert get_message('INVALID_CONFIRMATION_TOKEN') in response.data
@pytest.mark.registerable()
@pytest.mark.settings(confirm_email_within='1 milliseconds')
def test_expired_confirmation_token(client, get_message):
with capture_registrations() as registrations:
data = dict(email='mary@lp.com', password='password', next='')
client.post('/register', data=data, follow_redirects=True)
user = registrations[0]['user']
token = registrations[0]['confirm_token']
time.sleep(1)
response = client.get('/confirm/' + token, follow_redirects=True)
msg = get_message('CONFIRMATION_EXPIRED', within='1 milliseconds', email=user.email)
assert msg in response.data
@pytest.mark.registerable()
@pytest.mark.settings(login_without_confirmation=True)
def test_login_when_unconfirmed(client, get_message):
data = dict(email='mary@lp.com', password='password', next='')
response = client.post('/register', data=data, follow_redirects=True)
assert b'mary@lp.com' in response.data
@pytest.mark.registerable()
@pytest.mark.settings(login_without_confirmation=True)
def test_confirmation_different_user_when_logged_in(client, get_message):
e1 = 'dude@lp.com'
e2 = 'lady@lp.com'
with capture_registrations() as registrations:
for e in e1, e2:
data = dict(email=e, password='password', next='')
client.post('/register', data=data)
logout(client)
token1 = registrations[0]['confirm_token']
token2 = registrations[1]['confirm_token']
client.get('/confirm/' + token1, follow_redirects=True)
logout(client)
authenticate(client, email=e1)
response = client.get('/confirm/' + token2, follow_redirects=True)
assert get_message('EMAIL_CONFIRMED') in response.data
assert b'Hello lady@lp.com' in response.data
@pytest.mark.registerable()
@pytest.mark.settings(recoverable=True)
def test_cannot_reset_password_when_email_is_not_confirmed(client, get_message):
email = 'dude@lp.com'
data = dict(email=email, password='password', next='')
response = client.post('/register', data=data, follow_redirects=True)
response = client.post('/reset', data=dict(email=email), follow_redirects=True)
assert get_message('CONFIRMATION_REQUIRED') in response.data
+100
View File
@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
"""
test_context_processors
~~~~~~~~~~~~~~~~~~~~~~~
Context processor tests
"""
import pytest
from utils import authenticate
@pytest.mark.recoverable()
@pytest.mark.registerable()
@pytest.mark.confirmable()
@pytest.mark.changeable()
@pytest.mark.settings(
login_without_confirmation=True,
change_password_template='custom_security/change_password.html',
login_user_template='custom_security/login_user.html',
reset_password_template='custom_security/reset_password.html',
forgot_password_template='custom_security/forgot_password.html',
send_confirmation_template='custom_security/send_confirmation.html',
register_user_template='custom_security/register_user.html')
def test_context_processors(client, app):
@app.security.context_processor
def default_ctx_processor():
return {'global': 'global'}
@app.security.forgot_password_context_processor
def forgot_password():
return {'foo': 'bar'}
response = client.get('/reset')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.login_context_processor
def login():
return {'foo': 'bar'}
response = client.get('/login')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.register_context_processor
def register():
return {'foo': 'bar'}
response = client.get('/register')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.reset_password_context_processor
def reset_password():
return {'foo': 'bar'}
response = client.get('/reset')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.change_password_context_processor
def change_password():
return {'foo': 'bar'}
authenticate(client)
response = client.get('/change')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.send_confirmation_context_processor
def send_confirmation():
return {'foo': 'bar'}
response = client.get('/confirm')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.mail_context_processor
def mail():
return {'foo': 'bar'}
with app.mail.record_messages() as outbox:
client.post('/reset', data=dict(email='matt@lp.com'))
email = outbox[0]
assert 'global' in email.html
assert 'bar' in email.html
@pytest.mark.passwordless()
@pytest.mark.settings(send_login_template='custom_security/send_login.html')
def test_passwordless_login_context_processor(app, client):
@app.security.send_login_context_processor
def send_login():
return {'foo': 'bar'}
response = client.get('/login')
assert b'bar' in response.data
+153
View File
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
"""
test_datastore
~~~~~~~~~~~~~~
Datastore tests
"""
from pytest import raises
from flask_security import UserMixin, RoleMixin
from flask_security.datastore import Datastore, UserDatastore
from utils import init_app_with_options
class User(UserMixin):
pass
class Role(RoleMixin):
pass
def test_unimplemented_datastore_methods():
datastore = Datastore(None)
assert datastore.db is None
with raises(NotImplementedError):
datastore.put(None)
with raises(NotImplementedError):
datastore.delete(None)
assert not datastore.commit()
def test_unimplemented_user_datastore_methods():
datastore = UserDatastore(None, None)
with raises(NotImplementedError):
datastore.find_user(None)
with raises(NotImplementedError):
datastore.find_role(None)
with raises(NotImplementedError):
datastore.get_user(None)
def test_toggle_active():
datastore = UserDatastore(None, None)
user = User()
user.active = True
assert datastore.toggle_active(user) is True
assert not user.active
assert datastore.toggle_active(user) is True
assert user.active is True
def test_deactivate_user():
datastore = UserDatastore(None, None)
user = User()
user.active = True
assert datastore.deactivate_user(user) is True
assert not user.active
def test_activate_user():
datastore = UserDatastore(None, None)
user = User()
user.active = False
assert datastore.activate_user(user) is True
assert user.active is True
def test_deactivate_returns_false_if_already_false():
datastore = UserDatastore(None, None)
user = User()
user.active = False
assert not datastore.deactivate_user(user)
def test_activate_returns_false_if_already_true():
datastore = UserDatastore(None, None)
user = User()
user.active = True
assert not datastore.activate_user(user)
def test_get_user(app, datastore):
init_app_with_options(app, datastore, **{
'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username')
})
with app.app_context():
user_id = datastore.find_user(email='matt@lp.com').id
user = datastore.get_user(user_id)
assert user is not None
user = datastore.get_user('matt@lp.com')
assert user is not None
user = datastore.get_user('matt')
assert user is not None
def test_find_role(app, datastore):
init_app_with_options(app, datastore)
role = datastore.find_role('admin')
assert role is not None
role = datastore.find_role('bogus')
assert role is None
def test_add_role_to_user(app, datastore):
init_app_with_options(app, datastore)
# Test with user object
user = datastore.find_user(email='matt@lp.com')
assert user.has_role('editor') is False
assert datastore.add_role_to_user(user, 'editor') is True
assert datastore.add_role_to_user(user, 'editor') is False
assert user.has_role('editor') is True
# Test with email
assert datastore.add_role_to_user('jill@lp.com', 'editor') is True
user = datastore.find_user(email='jill@lp.com')
assert user.has_role('editor') is True
# Test remove role
assert datastore.remove_role_from_user(user, 'editor') is True
assert datastore.remove_role_from_user(user, 'editor') is False
def test_create_user_with_roles(app, datastore):
init_app_with_options(app, datastore)
role = datastore.find_role('admin')
datastore.commit()
user = datastore.create_user(email='dude@lp.com', username='dude',
password='password', roles=[role])
datastore.commit()
user = datastore.find_user(email='dude@lp.com')
assert user.has_role('admin') is True
def test_delete_user(app, datastore):
init_app_with_options(app, datastore)
user = datastore.find_user(email='matt@lp.com')
datastore.delete_user(user)
datastore.commit()
user = datastore.find_user(email='matt@lp.com')
assert user is None
+46
View File
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
"""
test_entities
~~~~~~~~~~~~~
Entity tests
"""
from flask_security import RoleMixin, UserMixin, AnonymousUser
class Role(RoleMixin):
def __init__(self, name):
self.name = name
class User(UserMixin):
def __init__(self, roles):
self.roles = roles
def test_role_mixin_equal():
admin1 = Role('admin')
admin2 = Role('admin')
assert admin1 == admin2
def test_role_mixin_not_equal():
admin = Role('admin')
editor = Role('editor')
assert admin != editor
def test_user_mixin_has_role_with_string():
admin = Role('admin')
editor = Role('editor')
user = User([admin, editor])
assert user.has_role('admin') is True
assert user.has_role('editor') is True
assert user.has_role(admin) is True
assert user.has_role(editor) is True
def test_anonymous_user_has_no_roles():
user = AnonymousUser()
assert not user.has_role('admin')
+38
View File
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""
test_hashing
~~~~~~~~~~~~
hashing tests
"""
from pytest import raises
from flask_security.utils import verify_password, encrypt_password
from utils import authenticate, init_app_with_options
def test_verify_password_bcrypt(app, sqlalchemy_datastore):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SALT': 'salty'
})
with app.app_context():
assert verify_password('pass', encrypt_password('pass'))
def test_login_with_bcrypt_enabled(app, sqlalchemy_datastore):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SALT': 'salty'
})
response = authenticate(app.test_client(), follow_redirects=True)
assert b'Home Page' in response.data
def test_missing_hash_salt_option(app, sqlalchemy_datastore):
with raises(RuntimeError):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bcrypt',
})
+193
View File
@@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-
"""
test_emails
~~~~~~~~~~~
Email functionality tests
"""
import pytest
from flask_security import Security
from flask_security.forms import LoginForm, RegisterForm, ConfirmRegisterForm, \
SendConfirmationForm, PasswordlessLoginForm, ForgotPasswordForm, ResetPasswordForm, \
ChangePasswordForm, TextField, PasswordField, email_required, email_validator, valid_user_email
from flask_security.utils import capture_reset_password_requests, md5, string_types
from utils import authenticate, init_app_with_options, populate_data
@pytest.mark.recoverable()
def test_async_email_task(app, client):
app.mail_sent = False
@app.security.send_mail_task
def send_email(msg):
app.mail_sent = True
client.post('/reset', data=dict(email='matt@lp.com'))
assert app.mail_sent is True
def test_register_blueprint_flag(app, sqlalchemy_datastore):
app.security = Security(app, datastore=Security, register_blueprint=False)
client = app.test_client()
response = client.get('/login')
assert response.status_code == 404
@pytest.mark.registerable()
@pytest.mark.recoverable()
@pytest.mark.changeable()
def test_basic_custom_forms(app, sqlalchemy_datastore):
class MyLoginForm(LoginForm):
email = TextField('My Login Email Address Field')
class MyRegisterForm(RegisterForm):
email = TextField('My Register Email Address Field')
class MyForgotPasswordForm(ForgotPasswordForm):
email = TextField('My Forgot Email Address Field',
validators=[email_required, email_validator, valid_user_email])
class MyResetPasswordForm(ResetPasswordForm):
password = TextField('My Reset Password Field')
class MyChangePasswordForm(ChangePasswordForm):
password = PasswordField('My Change Password Field')
app.security = Security(app,
datastore=sqlalchemy_datastore,
login_form=MyLoginForm,
register_form=MyRegisterForm,
forgot_password_form=MyForgotPasswordForm,
reset_password_form=MyResetPasswordForm,
change_password_form=MyChangePasswordForm)
populate_data(app)
client = app.test_client()
response = client.get('/login')
assert b'My Login Email Address Field' in response.data
response = client.get('/register')
assert b'My Register Email Address Field' in response.data
response = client.get('/reset')
assert b'My Forgot Email Address Field' in response.data
with capture_reset_password_requests() as requests:
response = client.post('/reset', data=dict(email='matt@lp.com'))
token = requests[0]['token']
response = client.get('/reset/' + token)
assert b'My Reset Password Field' in response.data
authenticate(client)
response = client.get('/change')
assert b'My Change Password Field' in response.data
@pytest.mark.registerable()
@pytest.mark.confirmable()
def test_confirmable_custom_form(app, sqlalchemy_datastore):
app.config['SECURITY_REGISTERABLE'] = True
app.config['SECURITY_CONFIRMABLE'] = True
class MyRegisterForm(ConfirmRegisterForm):
email = TextField('My Register Email Address Field')
class MySendConfirmationForm(SendConfirmationForm):
email = TextField('My Send Confirmation Email Address Field')
app.security = Security(app,
datastore=sqlalchemy_datastore,
send_confirmation_form=MySendConfirmationForm,
confirm_register_form=MyRegisterForm)
client = app.test_client()
response = client.get('/register')
assert b'My Register Email Address Field' in response.data
response = client.get('/confirm')
assert b'My Send Confirmation Email Address Field' in response.data
def test_passwordless_custom_form(app, sqlalchemy_datastore):
app.config['SECURITY_PASSWORDLESS'] = True
class MyPasswordlessLoginForm(PasswordlessLoginForm):
email = TextField('My Passwordless Email Address Field')
app.security = Security(app,
datastore=sqlalchemy_datastore,
passwordless_login_form=MyPasswordlessLoginForm)
client = app.test_client()
response = client.get('/login')
assert b'My Passwordless Email Address Field' in response.data
def test_addition_identity_attributes(app, sqlalchemy_datastore):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username')
})
client = app.test_client()
response = authenticate(client, email='matt', follow_redirects=True)
assert b'Hello matt@lp.com' in response.data
def test_flash_messages_off(app, sqlalchemy_datastore, get_message):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_FLASH_MESSAGES': False
})
client = app.test_client()
response = client.get('/profile')
assert get_message('LOGIN') not in response.data
def test_invalid_hash_scheme(app, sqlalchemy_datastore, get_message):
with pytest.raises(ValueError):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bogus'
})
def test_change_hash_type(app, sqlalchemy_datastore):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_SCHEMES': ['bcrypt', 'plaintext']
})
app.config['SECURITY_PASSWORD_HASH'] = 'bcrypt'
app.config['SECURITY_PASSWORD_SALT'] = 'salty'
app.security = Security(app, datastore=sqlalchemy_datastore, register_blueprint=False)
client = app.test_client()
response = client.post('/login', data=dict(email='matt@lp.com', password='password'))
assert response.status_code == 302
response = client.get('/logout')
response = client.post('/login', data=dict(email='matt@lp.com', password='password'))
assert response.status_code == 302
def test_md5():
data = md5(b'hello')
assert isinstance(data, string_types)
data = md5(u'hellö')
assert isinstance(data, string_types)
@pytest.mark.settings(password_salt=u'öööööööööööööööööööööööööööööööööö',
password_hash='bcrypt')
def test_password_unicode_password_salt(client):
response = authenticate(client)
assert response.status_code == 302
response = authenticate(client, follow_redirects=True)
assert b'Hello matt@lp.com' in response.data
+89
View File
@@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
"""
test_passwordless
~~~~~~~~~~~~~~~~~
Passwordless tests
"""
import time
import pytest
from flask_security.signals import login_instructions_sent
from flask_security.utils import capture_passwordless_login_requests
from utils import logout
pytestmark = pytest.mark.passwordless()
def test_trackable_flag(app, client, get_message):
recorded = []
@login_instructions_sent.connect_via(app)
def on_instructions_sent(app, user, login_token):
recorded.append(user)
# Test disabled account
response = client.post('/login', data=dict(email='tiya@lp.com'), follow_redirects=True)
assert get_message('DISABLED_ACCOUNT') in response.data
# Test login with json and valid email
data = '{"email": "matt@lp.com", "password": "password"}'
response = client.post('/login', data=data, headers={'Content-Type': 'application/json'})
assert response.status_code == 200
assert len(recorded) == 1
# Test login with json and invalid email
data = '{"email": "nobody@lp.com", "password": "password"}'
response = client.post('/login', data=data, headers={'Content-Type': 'application/json'})
assert b'errors' in response.data
# Test sends email and shows appropriate response
with capture_passwordless_login_requests() as requests:
with app.mail.record_messages() as outbox:
response = client.post('/login', data=dict(email='matt@lp.com'), follow_redirects=True)
assert len(recorded) == 2
assert len(requests) == 1
assert len(outbox) == 1
assert 'user' in requests[0]
assert 'login_token' in requests[0]
user = requests[0]['user']
assert get_message('LOGIN_EMAIL_SENT', email=user.email) in response.data
token = requests[0]['login_token']
response = client.get('/login/' + token, follow_redirects=True)
assert get_message('PASSWORDLESS_LOGIN_SUCCESSFUL') in response.data
# Test already authenticated
response = client.get('/login/' + token, follow_redirects=True)
assert get_message('PASSWORDLESS_LOGIN_SUCCESSFUL') not in response.data
logout(client)
# Test invalid token
response = client.get('/login/bogus', follow_redirects=True)
assert get_message('INVALID_LOGIN_TOKEN') in response.data
# Test login request with invalid email
response = client.post('/login', data=dict(email='bogus@bogus.com'))
assert get_message('USER_DOES_NOT_EXIST') in response.data
@pytest.mark.settings(login_within='1 milliseconds')
def test_expired_login_token(client, app, get_message):
e = 'matt@lp.com'
with capture_passwordless_login_requests() as requests:
client.post('/login', data=dict(email=e), follow_redirects=True)
token = requests[0]['login_token']
user = requests[0]['user']
time.sleep(1)
response = client.get('/login/' + token, follow_redirects=True)
assert get_message('LOGIN_EXPIRED', within='1 milliseconds', email=user.email) in response.data
+137
View File
@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
"""
test_recoverable
~~~~~~~~~~~~~~~~
Recoverable functionality tests
"""
import time
import pytest
from flask_security.signals import reset_password_instructions_sent, password_reset
from flask_security.utils import capture_reset_password_requests
from utils import authenticate, logout
pytestmark = pytest.mark.recoverable()
def test_recoverable_flag(app, client, get_message):
recorded_resets = []
recorded_instructions_sent = []
@password_reset.connect_via(app)
def on_password_reset(app, user):
recorded_resets.append(user)
@reset_password_instructions_sent.connect_via(app)
def on_instructions_sent(app, user, token):
recorded_instructions_sent.append(user)
# Test the reset view
response = client.get('/reset')
assert b'<h1>Send password reset instructions</h1>' in response.data
# Test submitting email to reset password creates a token and sends email
with capture_reset_password_requests() as requests:
with app.mail.record_messages() as outbox:
response = client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
assert len(recorded_instructions_sent) == 1
assert len(outbox) == 1
assert response.status_code == 200
assert get_message('PASSWORD_RESET_REQUEST', email='joe@lp.com') in response.data
token = requests[0]['token']
# Test view for reset token
response = client.get('/reset/' + token)
assert b'<h1>Reset password</h1>' in response.data
# Test submitting a new password
response = client.post('/reset/' + token, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('PASSWORD_RESET') in response.data
assert len(recorded_resets) == 1
logout(client)
# Test logging in with the new password
response = authenticate(client, 'joe@lp.com', 'newpassword', follow_redirects=True)
assert b'Hello joe@lp.com' in response.data
logout(client)
# Test submitting JSON
response = client.post('/reset', data='{"email": "joe@lp.com"}', headers={
'Content-Type': 'application/json'
})
assert response.headers['Content-Type'] == 'application/json'
assert 'user' not in response.jdata['response']
logout(client)
# Test invalid email
response = client.post('/reset', data=dict(email='bogus@lp.com'), follow_redirects=True)
assert get_message('USER_DOES_NOT_EXIST') in response.data
logout(client)
# Test invalid token
response = client.post('/reset/bogus', data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('INVALID_RESET_PASSWORD_TOKEN') in response.data
# Test mangled token
token = ("WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd.BZEw_Q.lQyo3npdPZtcJ"
"_sNHVHP103syjM&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d")
response = client.post('/reset/' + token, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('INVALID_RESET_PASSWORD_TOKEN') in response.data
@pytest.mark.settings(reset_password_within='1 milliseconds')
def test_expired_reset_token(client, get_message):
with capture_reset_password_requests() as requests:
client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
user = requests[0]['user']
token = requests[0]['token']
time.sleep(1)
response = client.post('/reset/' + token, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
msg = get_message('PASSWORD_RESET_EXPIRED', within='1 milliseconds', email=user.email)
assert msg in response.data
@pytest.mark.settings(reset_url='/custom_reset')
def test_custom_reset_url(client):
response = client.get('/custom_reset')
assert response.status_code == 200
@pytest.mark.settings(reset_password_template='custom_security/reset_password.html',
forgot_password_template='custom_security/forgot_password.html')
def test_custom_reset_templates(client):
response = client.get('/reset')
assert b'CUSTOM FORGOT PASSWORD' in response.data
with capture_reset_password_requests() as requests:
client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
token = requests[0]['token']
response = client.get('/reset/' + token)
assert b'CUSTOM RESET PASSWORD' in response.data
+120
View File
@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
"""
test_registerable
~~~~~~~~~~~~~~~~~
Registerable tests
"""
import pytest
from flask_security.signals import user_registered
from utils import authenticate, logout
pytestmark = pytest.mark.registerable()
@pytest.mark.settings(post_register_view='/post_register')
def test_registerable_flag(client, app, get_message):
recorded = []
# Test the register view
response = client.get('/register')
assert b"<h1>Register</h1>" in response.data
# Test registering is successful, sends email, and fires signal
@user_registered.connect_via(app)
def on_user_registerd(app, user, confirm_token):
recorded.append(user)
data = dict(
email='dude@lp.com', password='password', password_confirm='password',
next=''
)
with app.mail.record_messages() as outbox:
response = client.post('/register', data=data, follow_redirects=True)
assert len(recorded) == 1
assert len(outbox) == 1
assert b'Post Register' in response.data
logout(client)
# Test user can login after registering
response = authenticate(client, email='dude@lp.com', password='password')
assert response.status_code == 302
logout(client)
# Test registering with an existing email
data = dict(
email='dude@lp.com', password='password', password_confirm='password',
next=''
)
response = client.post('/register', data=data, follow_redirects=True)
assert get_message('EMAIL_ALREADY_ASSOCIATED', email='dude@lp.com') in response.data
# Test registering with an existing email but case insensitive
data = dict(
email='Dude@lp.com', password='password', password_confirm='password',
next=''
)
response = client.post('/register', data=data, follow_redirects=True)
assert get_message('EMAIL_ALREADY_ASSOCIATED', email='Dude@lp.com') in response.data
# Test registering with JSON
data = '{ "email": "dude2@lp.com", "password": "password"}'
response = client.post('/register', data=data, headers={'Content-Type': 'application/json'})
assert response.headers['content-type'] == 'application/json'
assert response.jdata['meta']['code'] == 200
logout(client)
# Test registering with invalid JSON
data = '{ "email": "bogus", "password": "password"}'
response = client.post('/register', data=data, headers={'Content-Type': 'application/json'})
assert response.headers['content-type'] == 'application/json'
assert response.jdata['meta']['code'] == 400
logout(client)
# Test ?next param
data = dict(email='dude3@lp.com',
password='password',
password_confirm='password',
next='')
response = client.post('/register?next=/page1', data=data, follow_redirects=True)
assert b'Page 1' in response.data
@pytest.mark.settings(register_url='/custom_register', post_register_view='/post_register')
def test_custom_register_url(client):
response = client.get('/custom_register')
assert b"<h1>Register</h1>" in response.data
data = dict(email='dude@lp.com',
password='password',
password_confirm='password',
next='')
response = client.post('/custom_register', data=data, follow_redirects=True)
assert b'Post Register' in response.data
@pytest.mark.settings(register_user_template='custom_security/register_user.html')
def test_custom_register_tempalate(client):
response = client.get('/register')
assert b'CUSTOM REGISTER USER' in response.data
@pytest.mark.settings(send_register_email=False)
def test_disable_register_emails(client, app):
data = dict(
email='dude@lp.com', password='password', password_confirm='password',
next=''
)
with app.mail.record_messages() as outbox:
client.post('/register', data=data, follow_redirects=True)
assert len(outbox) == 0
+28
View File
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
"""
test_trackable
~~~~~~~~~~~~~~
Trackable tests
"""
import pytest
from utils import authenticate, logout
pytestmark = pytest.mark.trackable()
def test_trackable_flag(app, client):
e = 'matt@lp.com'
authenticate(client, email=e)
logout(client)
authenticate(client, email=e, headers={'X-Forwarded-For': '127.0.0.1'})
with app.app_context():
user = app.security.datastore.find_user(email=e)
assert user.last_login_at is not None
assert user.current_login_at is not None
assert user.last_login_ip == 'untrackable'
assert user.current_login_ip == '127.0.0.1'
assert user.login_count == 2
-88
View File
@@ -1,88 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
from flask_security import RoleMixin, UserMixin, AnonymousUser
from flask_security.datastore import Datastore, UserDatastore
class Role(RoleMixin):
def __init__(self, name):
self.name = name
class User(UserMixin):
def __init__(self, email, roles):
self.email = email
self.roles = roles
admin = Role('admin')
admin2 = Role('admin')
editor = Role('editor')
user = User('matt@lp.com', [admin, editor])
class SecurityEntityTests(unittest.TestCase):
def test_role_mixin_equal(self):
self.assertEqual(admin, admin2)
def test_role_mixin_not_equal(self):
self.assertNotEqual(admin, editor)
def test_user_mixin_has_role_with_string(self):
self.assertTrue(user.has_role('admin'))
def test_user_mixin_has_role_with_role_obj(self):
self.assertTrue(user.has_role(Role('admin')))
def test_anonymous_user_has_no_roles(self):
au = AnonymousUser()
self.assertEqual(0, len(au.roles))
self.assertFalse(au.has_role('admin'))
class DatastoreTests(unittest.TestCase):
def setUp(self):
super(DatastoreTests, self).setUp()
self.ds = UserDatastore(None, None)
def test_unimplemented_datastore_methods(self):
ds = Datastore(None)
self.assertRaises(NotImplementedError, ds.put, None)
self.assertRaises(NotImplementedError, ds.delete, None)
def test_unimplemented_user_datastore_methods(self):
self.assertRaises(NotImplementedError, self.ds.find_user)
self.assertRaises(NotImplementedError, self.ds.find_role)
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))
+84
View File
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
"""
utils
~~~~~
Test utils
"""
from flask import Response as BaseResponse, json
from flask_security import Security
from flask_security.utils import encrypt_password
_missing = object
def authenticate(client, email="matt@lp.com", password="password", endpoint=None, **kwargs):
data = dict(email=email, password=password, remember='y')
return client.post(endpoint or '/login', data=data, **kwargs)
def json_authenticate(client, email="matt@lp.com", password="password", endpoint=None):
data = '{"email": "%s", "password": "%s"}' % (email, password)
return client.post(endpoint or '/login', content_type="application/json", data=data)
def logout(client, endpoint=None, **kwargs):
return client.get(endpoint or '/logout', **kwargs)
def create_roles(ds):
for role in ('admin', 'editor', 'author'):
ds.create_role(name=role)
ds.commit()
def create_users(ds, count=None):
users = [('matt@lp.com', 'matt', 'password', ['admin'], True),
('joe@lp.com', 'joe', 'password', ['editor'], True),
('dave@lp.com', 'dave', 'password', ['admin', 'editor'], True),
('jill@lp.com', 'jill', 'password', ['author'], True),
('tiya@lp.com', 'tiya', 'password', [], False),
('jess@lp.com', 'jess', None, [], True)]
count = count or len(users)
for u in users[:count]:
pw = u[2]
if pw is not None:
pw = encrypt_password(pw)
roles = [ds.find_or_create_role(rn) for rn in u[3]]
ds.commit()
user = ds.create_user(email=u[0], username=u[1], password=pw, active=u[4])
ds.commit()
for role in roles:
ds.add_role_to_user(user, role)
ds.commit()
def populate_data(app, user_count=None):
ds = app.security.datastore
with app.app_context():
create_roles(ds)
create_users(ds, user_count)
class Response(BaseResponse): # pragma: no cover
@property
def jdata(self):
rv = getattr(self, '_cached_jdata', _missing)
if rv is not _missing:
return rv
try:
self._cached_jdata = json.loads(self.data)
except ValueError:
raise Exception('Invalid JSON response')
return self._cached_jdata
def init_app_with_options(app, datastore, **options):
security_args = options.pop('security_args', {})
app.config.update(**options)
app.security = Security(app, datastore=datastore, **security_args)
populate_data(app)
+10
View File
@@ -0,0 +1,10 @@
[tox]
envlist = py26, py27, py33, py34, pypy
[testenv]
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements-dev.txt
commands =
python setup.py test