Compare commits

...
408 Commits
Author SHA1 Message Date
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
Matt Wright 3a5a1b4f52 Bump version number to 1.5.2 2012-12-11 15:15:01 -05:00
Matt Wright 0724bd12a5 Remove stray file 2012-12-11 15:13:35 -05:00
Matt Wright c7e3e642fa Update CHANGES 2012-12-11 15:11:51 -05:00
Matt Wright ee4c8f2a3f Fix login_user method to actually return a True or False value as mentioned in mattupstate/flask-social-example#8 2012-12-11 15:10:26 -05:00
Matt Wright 1092ffc9ea Fix #45 2012-11-26 16:21:05 -05:00
Matt Wright f4b6eb9869 Fix table 2012-11-26 15:03:18 -05:00
Matt Wright 5e49a4e05b Bump version number to 1.5.1 2012-11-26 15:01:54 -05:00
Matt Wright adc50f07eb Update changelog 2012-11-26 14:59:12 -05:00
Matt Wright 874c758340 Add a test to test email subject configuration 2012-11-26 14:58:39 -05:00
Matt Wright ed031f01ba Concluding merge 2012-11-26 14:53:01 -05:00
Matt Wright 4cbae25ac0 Merge pull request #47 from doobeh/ISSUE43
Ability to manage email subjects from configuration.
2012-11-26 11:52:09 -08:00
Matt Wright 7f002ac612 Fixes #40 2012-11-26 14:41:51 -05:00
Matt Wright 9e8fcbc879 Merge pull request #48 from joshpurvis/nextbug
Pass 'next' url to form context during login
2012-11-26 11:17:40 -08:00
Josh Purvis c8a24a8647 Wrapped next args with get_url() 2012-11-24 17:31:05 -05:00
Josh Purvis e83a970b3a Pass 'next' url to form context variable 2012-11-24 17:02:04 -05:00
Anthony Plunkett 514d27fd66 Ability to manage email subjects from configuration. 2012-11-19 21:13:52 -05:00
Matt Wright 16976fce4e Merge pull request #46 from doobeh/ISSUE42
Flask-Mail documentation for basic functionality.
2012-11-16 14:50:39 -08:00
Anthony Plunkett 3dc2304d6e Flask-Mail documentation for basic functionality. 2012-11-16 17:30:12 -05:00
Matt Wright 7a04a857f2 Merge branch 'master' into develop 2012-10-11 22:17:00 -04:00
Matt Wright a107cd98db Fix setup.cfg 2012-10-11 22:14:32 -04:00
Matt Wright d7f52af371 Bump version number 2012-10-11 17:39:05 -04:00
Matt Wright f96feaffa1 Bump version number to 1.5.0 2012-10-11 17:34:32 -04:00
Matt Wright 14b3d80360 Fix merge conflicts and fix release.py 2012-10-11 17:34:28 -04:00
Matt Wright aad042d7ad Update changelog and install dependencies 2012-10-11 17:11:48 -04:00
Matt Wright 0154cce46c Only give out auth token on the login endpoint 2012-10-11 16:58:53 -04:00
Matt Wright a269930ec3 Update docs and tests 2012-10-10 13:36:59 -04:00
Matt Wright 6b80aae7d1 Fix error 2012-09-26 16:25:22 -04:00
Matt Wright e1dbed816c Simplify login form a bit 2012-09-19 01:22:09 -04:00
Matt Wright e423390050 Simplify login form to only include one relevant error message 2012-09-18 23:49:44 -04:00
Matt Wright 338f04b2f2 polish 2012-09-16 23:01:35 -04:00
Matt Wright fca7120210 Get rid of unnecessary anonymous_user_required decorators 2012-09-16 23:00:10 -04:00
Matt Wright 826cc0d685 Change services 2012-09-16 22:27:21 -04:00
Matt Wright 90b4c58457 Fix build, hopefully 2012-09-16 22:23:04 -04:00
Matt Wright 857f135748 Add mongodb service to Travis 2012-09-16 22:15:25 -04:00
Matt Wright a0ed846a59 Remove print statement 2012-09-16 22:11:49 -04:00
Matt Wright 6c189f331f Allow users to be registered with JSON/ajax calls 2012-09-16 22:01:40 -04:00
Matt Wright 1f8fb48727 a bit of code polish and an attempt to speed up the tests 2012-09-16 16:43:28 -04:00
Matt Wright 96f2be056d Move anonymous_user_required to decorators 2012-09-16 16:05:24 -04:00
Matt Wright 8bdd464239 Add form to namespace 2012-09-16 16:05:06 -04:00
Matt Wright c1141b57fa Add ability to not register blueprint on app. Useful if combining apps such as an API layer and a frontend where the API is not concerned with rendering templates or handling traditional auth 2012-09-11 17:51:20 -04:00
Matt Wright 364646fc6b Fix register_user.html 2012-09-10 18:14:27 -04:00
Matt Wright fdcce53823 Update build since Flask-Mail moved 2012-09-10 17:54:31 -04:00
Matt Wright d8e6ae41f1 Dropping 2.5 support for now due to WTForms 2012-08-28 16:55:43 -04:00
Matt Wright 2042b8aa4c Fixes #34 2012-08-28 16:25:20 -04:00
Matt Wright f928db298d Refactor datastore implementation 2012-08-24 11:38:25 -04:00
Matt Wright da9f683c22 Update docs a bit 2012-08-24 00:48:14 -04:00
Matt Wright 4e41f4ec5e Polish up tests 2012-08-24 00:47:41 -04:00
Matt Wright 7ddc132af5 Update script commands a bit 2012-08-24 00:47:07 -04:00
Matt Wright 23cc774f96 Add error for bad configuration 2012-08-24 00:27:22 -04:00
Matt Wright bac04a0f3c remove more unnecessary code 2012-08-23 23:48:55 -04:00
Matt Wright f1c52d01aa Even more polish 2012-08-23 20:56:13 -04:00
Matt Wright b052e09cd6 Polish 2012-08-23 20:47:48 -04:00
Matt Wright bdf61b146c Remove unnecessary exceptions file and more clean up 2012-08-23 20:39:43 -04:00
Matt Wright 6322b4cbe1 Clean up 2012-08-23 20:37:27 -04:00
Matt Wright 6e754ed356 Major refactoring. Got rid of exceptions/errors in favor of using simple return values. Update tests to ensure full coverage according to nose coverage plugin 2012-08-23 17:58:33 -04:00
Matt Wright 5a4fb94be3 Make confirm endpoint anonymous only and get rid of invalid test 2012-08-23 15:03:13 -04:00
Matt Wright 57595bbab4 Refactor forms and views a bit. Add more validation to forms 2012-08-23 14:56:35 -04:00
Matt Wright b0b09aea49 Add ability to define a send_mail_task which could be used to send mails instead of the default flask-mail plugin. Could also be used to send mail asynchronously. Make flask-mail required as well. 2012-08-23 13:01:11 -04:00
Matt Wright 1af774dcb7 Remove unused exceptions 2012-08-23 12:54:46 -04:00
Matt Wright b65b717fbc Polish 2012-08-22 18:01:31 -04:00
Matt Wright c55993fe88 Update docs 2012-08-22 17:25:40 -04:00
Matt Wright 7361114ccb Update documentation a bit 2012-08-22 17:12:00 -04:00
Matt Wright 68648c299f Change some names 2012-08-22 16:55:31 -04:00
Matt Wright 8465fc4818 Add mail context processory 2012-08-22 16:42:32 -04:00
Matt Wright dc39eb58c7 Fix test 2012-08-22 16:37:17 -04:00
Matt Wright 5964a99e57 Clean up 2012-08-22 16:37:07 -04:00
Matt Wright 53257c17a9 Update send_mail api and add welcome email for user registration. Also add security state to template context for emails for more complex template rendering. 2012-08-22 15:15:39 -04:00
Matt Wright 86adcf0653 Fix build 2012-08-22 12:06:21 -04:00
Matt Wright 2471ba0db8 Move example app, which was a bad example, to the tests namespace. Its what it was used for anyway. A better example will be provided later 2012-08-22 12:00:46 -04:00
Matt Wright 17416cb535 Always encrypt password when creating a user 2012-08-21 18:55:42 -04:00
Matt Wright eec0e23620 Remove old test 2012-08-21 17:35:19 -04:00
Matt Wright d3c23c1994 Polish 2012-08-21 17:34:38 -04:00
Matt Wright 58685f2bb4 Decent clean up. Get rid of AuthProvider class in favor of keeping it simple 2012-08-21 17:04:41 -04:00
Matt Wright 25e9d02a8a clean up 2012-08-21 11:52:49 -04:00
Matt Wright 66c565a72f Register mail as extension, for now 2012-08-21 11:38:24 -04:00
Matt Wright 24f02a76f5 Update docs 2012-08-21 01:51:17 -04:00
Matt Wright 828a973339 Add already confirmed scenario. Let datastore work without a request context 2012-08-21 01:50:40 -04:00
Matt Wright f2d5028d7c Prefer form error messages in some instances 2012-08-21 00:59:46 -04:00
Matt Wright 705b73afc1 Form refactoring 2012-08-20 23:40:20 -04:00
Matt Wright 8c533ff12c docs work 2012-08-20 23:35:28 -04:00
Matt Wright bebaac49e3 Forgo redirecting authentication endpoint so that login form errors can be displayed 2012-08-20 23:35:17 -04:00
Matt Wright ebe34005a1 Update docs 2012-08-20 18:25:03 -04:00
Matt Wright 101fa42e55 Only use password salt if using hmac 2012-08-20 18:17:29 -04:00
Matt Wright 332575e53b Fix up 2012-08-20 18:12:46 -04:00
Matt Wright fa4668aa3f Use default values for encrypt_password and verify_password 2012-08-20 17:44:20 -04:00
Matt Wright 3a07970216 Fix up salts and get rid of token serializer as its not used 2012-08-20 17:32:13 -04:00
Matt Wright a3f350f905 Start work on documentation 2012-08-20 17:07:51 -04:00
Matt Wright 8919129c95 Fiddle with templates again 2012-08-17 15:31:37 -04:00
Matt Wright f339018a5e Update template 2012-08-17 15:30:24 -04:00
Matt Wright 433b88e579 Update templates 2012-08-17 15:18:05 -04:00
Matt Wright a4356d786e More clean up 2012-08-17 15:06:54 -04:00
Matt Wright c36fee7fda Clean up 2012-08-17 15:05:22 -04:00
Matt Wright 9c189f9083 Clean up 2012-08-17 14:50:49 -04:00
Matt Wright 2bd19f999d Code cleanup 2012-08-17 14:46:30 -04:00
Matt Wright a39f46854e Significant design change: commit data after some requests to avoid multiple database hits when using SQLALchemy 2012-08-17 13:19:40 -04:00
Matt Wright b7d71f8c59 Remove old commit param 2012-08-17 12:52:05 -04:00
Matt Wright beff7a246d Add context processors for security blueprint 2012-08-17 11:50:23 -04:00
Matt Wright adb550a9f2 Improve RegisterUserForm 2012-08-16 19:05:42 -04:00
Matt Wright 1c31728a26 Update message 2012-08-16 18:54:45 -04:00
Matt Wright f00f2eff21 Update message 2012-08-16 18:54:38 -04:00
Matt Wright dec858dae9 Add registration message 2012-08-16 18:53:27 -04:00
Matt Wright d87676027e Fix some redirect rules 2012-08-16 18:42:37 -04:00
Matt Wright 704af1011a Fix up forms to grab values in certain cases 2012-08-16 18:31:32 -04:00
Matt Wright 96e11916af Always redirect to login view and not referrer 2012-08-16 18:23:24 -04:00
Matt Wright 009671090f Clean up and bug improvements 2012-08-16 18:20:42 -04:00
Matt Wright 592003ecb6 Fix error in template 2012-08-16 18:04:34 -04:00
Matt Wright eeace79ef9 Update default login template and add security state to app context processory 2012-08-16 18:03:47 -04:00
Matt Wright 8b139890b4 Add useful decorator for ensuring anonymous users on particular endpoints 2012-08-16 17:53:41 -04:00
Matt Wright df08355b1d Fix template 2012-08-16 17:28:50 -04:00
Matt Wright 81ba459fc3 Change function name 2012-08-16 17:27:04 -04:00
Matt Wright 1d8b2f8342 Change urls/views to be (subjectively) simpler 2012-08-16 17:25:24 -04:00
Matt Wright 1d378a6827 Add login to security blueprint 2012-08-16 15:18:49 -04:00
Matt Wright 2fcfb80e8e Refactor templates and added url_for_security utility. Can also configure the blueprint name. 2012-08-16 13:41:13 -04:00
Matt Wright ac674ecd86 Fix some messaging 2012-08-15 19:33:23 -04:00
Matt Wright bb91c4a81a Fix flash messages and fix password reset 2012-08-15 19:22:24 -04:00
Matt Wright 1b505d0f39 Update default templates 2012-08-15 19:22:06 -04:00
Matt Wright ec05b30106 Get rid of silly print statement 2012-08-15 17:23:42 -04:00
Matt Wright 80144c85a1 Fix bug with an invalid remember_token cookie value 2012-08-15 17:06:07 -04:00
Matt Wright f4d305cc03 Update CHANGES 2012-08-15 16:07:51 -04:00
Matt Wright 72ecb56250 Update release script 2012-08-15 16:05:27 -04:00
Matt Wright b712be346c Add release script 2012-08-15 15:38:04 -04:00
Matt Wright bee5f1cbe9 Update version number 2012-08-15 15:36:47 -04:00
Matt Wright 8c107dcb7d Add setup.cfg 2012-08-15 13:30:14 -04:00
Matt Wright 7554a52732 Cleanup and some more messaging additions 2012-08-15 11:56:26 -04:00
Matt Wright 022836c43c fix tests, hopefully 2012-08-15 10:25:07 -04:00
Matt Wright 64467b0338 Try and fix build 2012-08-15 10:14:06 -04:00
Matt Wright 318cb3dc6e First commit of passwordless login 2012-08-14 19:01:49 -04:00
Matt Wright ce6c5dcf31 Clean up 2012-08-14 16:59:05 -04:00
Matt Wright 0524271573 Clean up 2012-08-14 16:38:31 -04:00
Matt Wright f3dff9e496 Clean up 2012-08-14 16:37:56 -04:00
Matt Wright 4daefcf1f7 Change name 2012-08-14 16:21:58 -04:00
Matt Wright 4b31de057c Remove reference to 2012-08-14 16:21:50 -04:00
Matt Wright 68b0410d1b No need to keep authentication token in DB 2012-08-14 16:21:31 -04:00
Matt Wright 6f7d5378e7 Remove reference to remember_token in docs 2012-08-14 16:03:38 -04:00
Matt Wright 05bd2a5aae Full test coverage! 2012-08-14 15:57:07 -04:00
Matt Wright e9adf91a27 More and more test coverage 2012-08-14 14:27:58 -04:00
Matt Wright 404e797cd9 Add more test coverage 2012-08-14 14:18:20 -04:00
Matt Wright 58e2856612 No need to store remember_token in DB 2012-08-14 13:52:30 -04:00
Matt Wright f6bb01545d Add more test coverage 2012-08-14 13:47:16 -04:00
Matt Wright 49bebcdd7b Code cleanup 2012-08-14 12:14:19 -04:00
Matt Wright 4238a7f704 Update docs a bit 2012-08-14 12:14:08 -04:00
Matt Wright 782d6d7e89 PEP 8... 2012-07-23 18:35:37 -04:00
Matt Wright 7355a741b3 Fix build 2012-07-23 18:34:36 -04:00
Matt Wright a9727ceaa6 Refactor login_user and logout_user to be a utility method, thus it can be reused if necessary 2012-07-23 18:28:13 -04:00
Matt Wright 39670ac84e Fix state a bit 2012-07-19 17:39:55 -04:00
Matt Wright 78b2eb8b18 Remove stray print statement 2012-07-19 17:39:47 -04:00
Matt Wright a89f43a1b7 Fix build 2012-07-19 17:08:06 -04:00
Matt Wright 8829692295 Refactor state management 2012-07-19 17:05:13 -04:00
Matt Wright eb388ad04f Add RuntimeError to send_mail function 2012-07-19 17:05:06 -04:00
Matt Wright aee3d30e50 Remove required flask-mail dependency. Let users install it themselves if features require it. Also need to wait till newer version is released 2012-07-19 17:04:48 -04:00
Matt Wright dbd0b7b23b House keeping 2012-07-19 12:20:04 -04:00
Matt Wright 68dd972bfa Add more secure password storage via salt value and hmac 2012-07-18 13:27:30 -04:00
Matt Wright 1c606a242a Adjust state 2012-07-16 19:40:34 -04:00
Matt Wright e23581e260 Polish 2012-07-16 19:34:33 -04:00
Matt Wright d5c6e4eb58 Update example/test app 2012-07-16 19:11:18 -04:00
Matt Wright 3899980b2b Not using these 2012-07-16 19:08:22 -04:00
Matt Wright f170cb434c Use a stateful object instead of arbitrary assignment of extension on app object 2012-07-16 19:07:19 -04:00
Matt Wright 54da59f003 Clean up 2012-07-13 16:11:25 -04:00
Matt Wright 507de82aba Update docs 2012-07-13 16:06:10 -04:00
Matt Wright fcab270f25 Include templates 2012-07-13 16:06:02 -04:00
Matt Wright 42bffc4234 Fix templates 2012-07-13 16:05:40 -04:00
Matt Wright 7db68da8c6 Remove unnecessary string formatting 2012-07-13 16:05:25 -04:00
Matt Wright 0a0b5ecade Get rid of login_required decorator from roles_required and roles_accepted in order to be able to pair http_auth_required with roles_required or roles_accepted. Just be sure to put http_auth_required first. 2012-07-13 13:50:36 -04:00
Matt Wright 7212e20a76 no pypy 2012-07-13 13:42:34 -04:00
Matt Wright e570997ea0 Remove flag, its meant for testing 2012-07-13 12:26:58 -04:00
Matt Wright 4337534437 Script usage additions 2012-07-13 12:24:51 -04:00
Matt Wright 7b624b8622 Bring back scripts 2012-07-13 12:23:48 -04:00
Matt Wright 3cd20f0e33 Clean up 2012-07-13 12:02:43 -04:00
Matt Wright cc9832653a See if pypy passes 2012-07-13 10:40:07 -04:00
Matt Wright 1d86d33b0b Add category for messages 2012-07-12 15:39:35 -04:00
Matt Wright 18c7a838b0 Make most messages configurable 2012-07-12 15:24:57 -04:00
Matt Wright 3df4c4d14a Clean up 2012-07-12 14:41:54 -04:00
Matt Wright ed8082868f Simplify exceptions 2012-07-12 14:40:20 -04:00
Matt Wright aba98a3a03 clean up 2012-07-12 14:25:44 -04:00
Matt Wright e9b49b8e9e clean up 2012-07-12 14:25:10 -04:00
Matt Wright dcdfb4d3e7 Add configurable http auth realm and optional realm specification in http_auth_required decorator 2012-07-12 14:16:54 -04:00
Matt Wright a2d31d1d8d Add configurable default http auth header 2012-07-12 13:24:59 -04:00
Matt Wright 2e9c62b4f8 Refactor decorators a bit 2012-07-12 13:15:58 -04:00
Matt Wright dfcb3cdcc6 Add customizable unauthorized URL. Fixes #23 2012-07-12 12:47:21 -04:00
Matt Wright 8d1bdca1de Change password reset view to use packaged template 2012-07-12 12:18:08 -04:00
Matt Wright da031b8d15 Simplify routing a tiny bit 2012-07-12 11:29:42 -04:00
Matt Wright 0befa34dc8 Trying to fix build, I don't think Travis likes the quickness of the token expiration tests 2012-07-11 18:26:10 -04:00
Matt Wright ddc503d296 Mising comma 2012-07-11 17:02:44 -04:00
Matt Wright e7bd1533c4 Move it 2012-07-11 16:38:26 -04:00
Matt Wright f35fa7f4d0 Update dependency 2012-07-11 16:34:00 -04:00
Matt Wright 5e1d18c9e8 Changed token auth a bit, including the use of itsdangerous. Also added JSON authentication feature 2012-07-11 16:31:21 -04:00
Matt Wright b3de4a76d5 refactor a bit 2012-07-11 15:14:20 -04:00
Matt Wright b9a6a9c5a8 Use itsdangerous for activation and password reset tokens so they do not need to be stored in the database 2012-07-11 15:06:54 -04:00
Matt Wright 703487e608 Move things around for aesthetic reasons 2012-07-10 17:29:43 -04:00
Matt Wright 338a9cd717 Use same convention as other methods 2012-07-10 17:28:14 -04:00
Matt Wright 815c8695fc Code clean upr 2012-07-10 17:24:39 -04:00
Matt Wright 00860c29d0 Remove print statement 2012-07-10 16:23:59 -04:00
Matt Wright 49d3789f98 Fix up remember token 2012-07-10 16:23:20 -04:00
Matt Wright ab97e736d9 Remove forms from config, pretty sure there's a better way to do this or its not necessary 2012-07-10 15:57:16 -04:00
Matt Wright 4d5fa05711 Remove flask-principal session storage in favor of useing flask-login current_user since it does session persistence for us 2012-07-10 15:39:10 -04:00
Matt Wright 4f557dac4c Update template a bit 2012-07-10 14:39:43 -04:00
Matt Wright 86447ab0c9 Tiny refactor 2012-07-09 19:10:12 -04:00
Matt Wright 9bba330f2c Fix README.rst typo 2012-07-09 18:58:31 -04:00
Matt Wright 2ed0ea48e6 Add trackable option and make extra features default to off/False to minimize about of application setup to get started 2012-07-09 18:57:43 -04:00
Matt Wright 74e94b2628 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2012-06-29 13:18:14 -04:00
Matt Wright 2ea835ec9f Add a bunch of doc strings and add some more configuration values 2012-06-29 12:37:22 -04:00
Matt Wright 53dd4f0b1b Add doc strings 2012-06-29 11:29:33 -04:00
Matt Wright d7dd454044 Merge pull request #22 from deignacio/develop
correct roles_* decorator signature expectations
2012-06-22 07:14:08 -07:00
David Ignacio 24cd4938a5 correct roles_* decorator signature expectations
Having multiple RoleNeed objects in a Permission does not require
all to be satisfied in order to .can(), but will return True if
any are present.  This makes the previous roles_required logic more
elegant for roles_accepted.  roles_required decorator needs to check
all permissions individually and return only if all permissions exist
2012-06-22 00:15:43 -05:00
Matt Wright e67cbee1d5 Oops, 2.5 not 2.6 2012-06-20 17:12:07 -04:00
Matt Wright 0a8ee87319 Try using an older version of mongoengine during build to get python 2.5 tests to pass 2012-06-20 16:51:09 -04:00
Matt Wright 7f2a05c364 Try and fix an issue with python 2.5 2012-06-20 16:35:36 -04:00
Matt Wright 5034e7b4f6 Add test methods that are missing for Python versions < 2.7 2012-06-20 16:21:35 -04:00
Matt Wright 10322602da Update build 2012-06-20 15:53:44 -04:00
Matt Wright a7016c1cfa Update build and dependencies 2012-06-20 15:36:44 -04:00
Matt Wright 442afd07f6 Update travis build 2012-06-20 10:44:17 -04:00
Matt Wright 022a6e5159 Update travis config 2012-06-19 16:29:23 -04:00
Matt Wright a4622002d9 Update README.rst 2012-06-19 15:21:13 -04:00
Matt Wright babcb6e987 Rename to rst 2012-06-19 15:17:40 -04:00
Matt Wright e61b4e47f5 More travis stuff 2012-06-19 15:02:28 -04:00
Matt Wright 3044d8a5ba Add travis 2012-06-19 14:51:06 -04:00
Matt Wright a902530773 Refactor forms and import other commonly used functions into package 2012-06-19 11:55:23 -04:00
Matt Wright 1f7c2625f7 Merge pull request #19 from tescalada/develop
Adding back lost role/active functions
2012-06-19 07:34:59 -07:00
Tristan Escalada 1731891113 Re-adding current_user
passing current_user from flask-login to __init__.py
2012-06-18 21:30:33 -04:00
Tristan Escalada efc93979d1 Adding back lost role/active functions
some functions must have gotten lost. Added them back.
_do_add_role, _do_remove_role, _do_toggle_active,
_do_deactive_user, _do_active_user, _prepare_role_modify_args
2012-06-18 20:02:35 -04:00
Matt Wright 421cf410a2 Specify dependency versions in setup.py 2012-06-18 16:58:56 -04:00
Matt Wright f8cb2a413f Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2012-06-18 16:52:00 -04:00
Matt Wright c20f244d66 Big code cleanup 2012-06-18 16:51:43 -04:00
Matt Wright cd35384ae8 Merge pull request #16 from tescalada/develop
bug fixes
2012-06-15 13:07:09 -07:00
Tristan Escalada 2655d67f52 Bugfix for generate_reset_password_token
reset_password_token was being mixed with reset_password_sent_at
2012-06-15 16:01:49 -04:00
Tristan Escalada b3a9364a97 Switch from setup_app to init_app for flask_login
new versions of flask_login have setup_app depricated
2012-06-15 16:01:49 -04:00
Matt Wright 5fdfa544db Polish up some code and add some signals 2012-06-15 12:13:49 -04:00
Matt Wright c123e32ddc decorators for basic http auth and token auth 2012-06-14 18:04:14 -04:00
Matt Wright 5b7160c492 Merge branch 'feature/token-authentication' into feature/decorators 2012-06-14 16:37:07 -04:00
Matt Wright 0141b240bc Better... 2012-06-12 12:32:59 -04:00
Matt Wright 0ddbcdca06 PEP 8 2012-06-12 12:28:59 -04:00
Matt Wright 727c19b9c0 Fix #13 2012-06-12 12:11:19 -04:00
Matt Wright edf41096d7 Fix #13 2012-06-12 12:03:54 -04:00
Matt Wright 90d52c6561 token auth methods 2012-06-11 18:06:47 -04:00
Matt Wright c4ada0d998 add query string and header config 2012-06-08 14:28:38 -04:00
Matt Wright ca25c253d5 Start work on token authentication 2012-06-08 14:17:33 -04:00
Matt Wright 7263388b7c refactor tests a tiny bit 2012-06-07 11:15:42 -04:00
Matt Wright e0c7a3deb2 Fix #10 2012-06-06 17:51:14 -04:00
Matt Wright d3202c03ba Fix #11 2012-06-06 17:46:06 -04:00
Matt Wright 2d0c49f4b4 More cleanup 2012-05-24 21:44:50 -04:00
Matt Wright 97d49395ba More cleanup 2012-05-24 21:40:44 -04:00
Matt Wright 112c419403 A bit more polish 2012-05-24 21:15:31 -04:00
Matt Wright a47c6b9d55 Polish 2012-05-24 21:07:29 -04:00
Matt Wright 08582b7f82 Remove old Flask-MongoEngine dependency since it is now officially on pypi 2012-05-22 18:42:31 -04:00
Matt Wright 20abde5542 Add Flask-Mail dependency 2012-05-22 18:34:42 -04:00
Matt Wright 2a9e91dfe8 Added reset password error handling and associated test 2012-05-22 18:13:29 -04:00
Matt Wright 09aa7e113c Heavy work on confirmation and reset 2012-05-22 18:08:38 -04:00
Matt Wright 67b806860e Fixes #9 2012-05-16 12:27:43 -04:00
Matt Wright 19cc30a3c1 Fix up form classes and add ResetPasswordFOrm 2012-05-15 18:43:06 -04:00
Matt Wright 0f914d1ea6 Attempting more clean up 2012-05-15 18:14:28 -04:00
Matt Wright 17ea120032 Some more polish. Fix a test 2012-05-15 17:17:38 -04:00
Matt Wright 903b07bfbb Polish 2012-05-15 17:04:42 -04:00
Matt Wright 4f61f58b0d Polish 2012-05-15 17:02:04 -04:00
Matt Wright 4e7663079b Refactor some methods 2012-05-14 18:58:43 -04:00
Matt Wright 4c1a16e2ee Added register signal, some testing utils and basic confirmation 2012-05-11 18:15:46 -04:00
Matt Wright 1a8e6f5cb6 Make datastore find_user method accept different params so a user can be retrieved by other such things such as a confirmation token 2012-05-11 13:57:35 -04:00
Matt Wright 2b587f7047 Starting a large refactor and adding confirmation abilities 2012-05-11 13:23:42 -04:00
Matt Wright 1ae711279e Refactor modules 2012-05-10 12:45:43 -04:00
Matt Wright 5e8b53ef46 Refactor views a bit to keep things cleaner and fix up tests 2012-05-10 12:31:37 -04:00
Matt Wright d6beaa43e3 Fix possible form object scope issue 2012-05-03 18:02:02 -04:00
Matt Wright 3ce907db79 Add requirements.txt 2012-05-03 17:34:46 -04:00
Matt Wright 201d3bbffd Merge branch 'feature/user-model-improvements' into develop 2012-05-01 15:43:19 -04:00
Matt Wright 4625e9bdf3 Bump version 2012-05-01 15:43:14 -04:00
Matt Wright d285764592 Make active be true by default 2012-05-01 12:05:28 -04:00
Matt Wright 9b513ce4ab Polish 2012-05-01 11:19:46 -04:00
Matt Wright ed7beb529a First commit of refactored code and removed default models. Now meant to provide models from app level, not within Flask-Security 2012-04-30 19:34:04 -04:00
Matt Wright 44fb7cfe38 Update dev version 2012-04-27 14:04:08 -04:00
Matt Wright 5c9bc541f9 1.2.2 release, includes a minor bug fix 2012-04-27 13:48:03 -04:00
Matt Wright f7147648ba Add change note 2012-04-27 13:44:14 -04:00
Matt Wright 30b72cb7f4 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2012-04-27 13:39:15 -04:00
Matt Wright ab08abcaf9 Fix #4 2012-04-27 13:37:50 -04:00
Matt Wright 0227d987fe Fix #3 2012-04-26 11:35:24 -03:00
Matt Wright 5df01af720 Fix bad code example 2012-04-26 11:20:34 -03:00
Matt Wright 0acf4bca0d remove __version__ because it was causing problems with installation from pypi 2012-04-04 12:16:00 -04:00
Matt Wright 2e37af8c01 Adjust path so version can get imported properly 2012-03-28 11:51:18 -04:00
Matt Wright 245914cc9a Doc polish and such 2012-03-28 11:44:48 -04:00
Matt Wright 201ee5b708 Merged user-account-mixin branch 2012-03-28 11:08:41 -04:00
Matt Wright 744ded7f1b Consolidate version number in one place 2012-03-27 18:49:01 -04:00
Matt Wright 5a7f4dff47 Initial idea for specifying a user account mixin for user model 2012-03-27 14:27:12 -04:00
Matt Wright dd6ba0995d Random fixes 2012-03-27 14:26:45 -04:00
Matt Wright 5697f7c953 Update install instructions since its on pypi 2012-03-13 20:28:50 -04:00
Matt Wright 144b22a3b9 Minimal README. No need to maintain a README when online docs do the trick 2012-03-13 20:23:07 -04:00
Matt Wright 3c57e169d1 Update version number 2012-03-12 22:21:00 -04:00
Matt Wright d8f760092a Version 1.2.0 2012-03-12 22:20:15 -04:00
Matt Wright d11ac8e977 Added documentation for models 2012-03-12 21:57:09 -04:00
Matt Wright 0cdb43e167 More documentation 2012-03-12 21:15:22 -04:00
Matt Wright 6c44625945 Add another template example 2012-03-12 19:33:00 -04:00
Matt Wright 5e59ee5c0c Fix doc string 2012-03-12 19:31:38 -04:00
Matt Wright 5f5b140db2 Initial documentation files 2012-03-12 19:26:57 -04:00
Matt Wright 86f072628a Added new config value "SECURITY_FLASH_MESSAGES" 2012-03-12 19:26:42 -04:00
Matt Wright 3515eb0762 Starting to add Sphinx docs 2012-03-12 17:07:36 -04:00
Matt Wright 65eac687b7 Added a bunch of code documentation 2012-03-12 17:07:21 -04:00
Matt Wright e8d41ad4b5 Update version number 2012-03-12 11:00:09 -04:00
93 changed files with 6401 additions and 1078 deletions
+32 -5
View File
@@ -1,6 +1,33 @@
.DS_Store *.py[co]
*.pyc
# Packages
*.egg *.egg
.project *.egg-info
.pydevproject dist
.settings *build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
#Virtualenv
env/
#Editor temporaries
*~
+3
View File
@@ -0,0 +1,3 @@
[submodule "docs/_themes"]
path = docs/_themes
url = git://github.com/mitsuhiko/flask-sphinx-themes.git
+23
View File
@@ -0,0 +1,23 @@
language: python
python:
- "2.6"
- "2.7"
- "pypy"
install:
- pip install . --quiet --use-mirrors
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib --quiet --use-mirrors; fi"
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors
before_script:
- mysql -e 'create database flask_security_test;'
services:
- mongodb
script: nosetests
branches:
only:
- develop
+117
View File
@@ -0,0 +1,117 @@
Flask-Security Changelog
========================
Here you can see the full list of changes between each Flask-Security release.
Version 1.6.2
-------------
Released April 4th 2013
- Fixed bug with http basic auth
Version 1.6.1
-------------
Released April 3rd 2013
- Fixed bug with signals
Version 1.6.0
-------------
Released March 13th 2013
- Added Flask-Pewee support
- Password hashing is now more flexible and can be changed to a different type at will
- Flask-Login messages are configurable
- AJAX requests must now send a CSRF token for security reasons
- Form messages are now configurable
- Forms can now be extended with more fields
- Added change password endpoint
- Added the user to the request context when successfully authenticated via http basic and token auth
- The Flask-Security blueprint subdomain is now configurable
- Redirects to other domains are now not allowed during requests that may redirect
- Template paths can be configured
- The welcome/register email can now optionally be sent to the user
- Passwords can now contain non-latin characters
- Fixed a bug when confirming an account but the account has been deleted
Version 1.5.4
-------------
Released January 6th 2013
- Fix bug in forms with `csrf_enabled` parameter not accounting attempts to login using JSON data
Version 1.5.3
-------------
Released December 23rd 2012
- Change dependency requirement
Version 1.5.2
-------------
Released December 11th 2012
- Fix a small bug in `flask_security.utils.login_user` method
Version 1.5.1
-------------
Released November 26th 2012
- Fixed bug with `next` form variable
- Added better documentation regarding Flask-Mail configuration
- Added ability to configure email subjects
Version 1.5.0
-------------
Released October 11th 2012
- Major release. Upgrading from previous versions will require a bit of work to
accomodate API changes. See documentation for a list of new features and for
help on how to upgrade.
Version 1.2.3
-------------
Released June 12th 2012
- Fixed a bug in the RoleMixin eq/ne functions
Version 1.2.2
-------------
Released April 27th 2012
- Fixed bug where `roles_required` and `roles_accepted` did not pass the next
argument to the login view
Version 1.2.1
-------------
Released March 28th 2012
- Added optional user model mixin parameter for datastores
- Added CreateRoleCommand to available Flask-Script commands
Version 1.2.0
-------------
Released March 12th 2012
- 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
+15 -4
View File
@@ -1,9 +1,20 @@
MIT License MIT License
Copyright (C) 2012 by Matt Wright Copyright (C) 2012 by Matthew Wright
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+2 -1
View File
@@ -1 +1,2 @@
include tests/*.py recursive-include tests *.py
recursive-include flask_security/templates *.*
-120
View File
@@ -1,120 +0,0 @@
# Flask-Security
Simple security for Flask applications combining [Flask-Login](http://packages.python.org/Flask-Login/), [Flask-Principal](http://packages.python.org/Flask-Principal/), [Flask-WTF](http://packages.python.org/Flask-WTF/), [passlib](http://packages.python.org/passlib/), and your choice of datastore. Currently [SQLAlchemy](http://www.sqlalchemy.org) via [Flask-SQLAlchemy](http://packages.python.org/Flask-SQLAlchemy/) and [MongoEngine](http://www.mongoengine.org) via [Flask-MongoEngine](https://github.com/sbook/flask-mongoengine) are supported out of the box. You will need to install the necessary Flask extensions that you'll be using. Additionally, you may need to install an encryption library such as [py-bcrypt](http://www.mindrot.org/projects/py-bcrypt/) to support bcrypt passwords.
## Overview
Flask-Security does a few things that Flask-Login and Flask-Principal don't provide out of the box. They are:
1. Setting up login and logout endpoints
2. Authenticating users based on username or email
3. Limiting access based on user 'roles'
4. User and role creation
5. Password encryption
That being said, you can still hook into things such as the Flask-Login and Flask-Principal signals if need be.
## Getting Started
First, install Flask-Security:
$ mkvirtualenv app-name
$ pip install https://github.com/mattupstate/flask-security/tarball/master
Then install your datastore requirement.
SQLAlchemy:
$ pip install Flask-SQLAlchemy
MongoEngine:
$ pip install https://github.com/sbook/flask-mongoengine/tarball/master
Beyond this, the best place to get started at the moment is to look at the example application(s) and corresponding tests. The example apps are currently used to test Flask-Security as well so they are solid examples of most, if not all, features. Configuration options are illustrated in the tests as well. To run the example run do the following:
$ mkvirtualenv flask-security
$ git clone git://github.com/mattupstate/flask-security.git
$ cd flask-security
$ pip install Flask Flask-Login Flask-Principal Flask-SQLALchemy passlib
$ pip install https://github.com/sbook/flask-mongoengine/tarball/master
$ python example/app.py
## Code Examples
If you don't want to checkout the example quite yet, here are some hypothetical examples to give you a sense of how Flask-Security works:
### Setup SQLAlchemy
from flask import Flask
from flask.ext.security import Security
from flask.ext.security.datastore.sqlalchemy import SQLAlchemyDatastore
from flask.ext.sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SECRET_KEY'] = 'something'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
db = SQLALchemy(app)
Security(app, SQLAlchemyDatastore(db))
### Require a logged in user:
from flask import render_template
from flask.ext.security import login_required
… application setup …
@app.route('/profile')
@login_required
def profile():
return render_template('profile.html')
### Require an admin:
from flask import render_template
from flask.ext.security import roles_required
… application setup …
@app.route('/admin')
@roles_required('admin')
def admin():
return render_template('admin/index.html')
### Require any of the specified roles:
from flask import render_template
from flask.ext.security import roles_accepted
… application setup …
@app.route('/admin')
@roles_accepted('admin', 'editor', 'author')
def admin():
return render_template('admin/index.html')
### Showing a link in a template only for an admin:
{% if current_user.has_role('admin') %}
<a href="{{ url_for('admin.index') }}">Admin Panel</a>
{$ endif %}
## Flask-Script Commands
Flask-Security comes packed with a few Flask-Script commands. They are:
* `flask.ext.security.script.CreateUserCommand`
* `flask.ext.security.script.AddRoleCommand`
* `flask.ext.security.script.RemoveRoleCommand`
* `flask.ext.security.script.DeactivateUserCommand`
* `flask.ext.security.script.ActivateUserCommand`
Register these on your script manager for pure convenience.
## Contributing
Feel free to fork and contribute. If you decided to do so, just be sure to include relevant tests that you feel are necessary. To run the tests, please provide instructions for any requirements. For instance, if you write a new datastore implementation, please provide instructions on how best to setup a connection when testing.
If you plan on running all the provided tests you'll need a local installation of MongoDB running on the standard port 27017 without username/password protection.
+15
View File
@@ -0,0 +1,15 @@
Flask-Security
==============
.. image:: https://secure.travis-ci.org/mattupstate/flask-security.png?branch=develop
Flask-Security quickly adds security features to your Flask application.
Resources
---------
- `Documentation <http://packages.python.org/Flask-Security/>`_
- `Issue Tracker <http://github.com/mattupstate/flask-security/issues>`_
- `Code <http://github.com/mattupstate/flask-security/>`_
- `Development Version
<http://github.com/mattupstate/flask-security/zipball/develop#egg=Flask-Security-dev>`_
+297
View File
@@ -0,0 +1,297 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="184.621px" height="267.873px" viewBox="0 0 184.621 267.873" style="enable-background:new 0 0 184.621 267.873;"
xml:space="preserve">
<path d="M42.3,6.712c0.374-3.417,6.743-3.01,6.24,0C48.174,8.905,43.253,8.469,42.3,6.712z M46.14,7.432
c0.102-0.539,1.273-0.006,1.44-0.48c-0.011-0.331,0.113-0.527,0.24-0.72c-0.386-1.446-4.505-1.467-4.32,0.479
c0.463,0.287,1.217,0.915,1.68,0.48c0.409-0.099-0.051-0.31,0-0.72c0.689-0.053,1.668-1.192,1.92-0.24
C47.161,6.872,45.517,7.023,46.14,7.432z"/>
<path d="M61.02,8.872c-1.514,1.428-5.308,0.636-6.239-0.72C54.424,4.301,62.875,4.691,61.02,8.872z M55.98,8.152
c0.547,0.174,1.227,0.214,1.681,0.48c-0.31-1.429,1.293-0.947,2.16-1.2c0.022,0.377-0.032,0.832,0.479,0.72
C60.766,5.738,55.542,5.836,55.98,8.152z"/>
<path d="M75.18,11.272c-2.44,1.066-5.342-0.024-6.479-1.68C69,5.478,77.3,6.38,75.18,11.272z M70.14,9.352
c0.348,0.453,0.536,1.064,1.44,0.96c-0.42-1.412,1.778-1.107,2.16-0.96c-0.09,0.79-1.276,0.484-1.2,1.44
c1.447,0.248,1.821-0.578,2.16-1.44C74.528,7.553,70.227,7.19,70.14,9.352z"/>
<path d="M38.701,9.352c-0.632,0.249-1.473,0.287-2.4,0.24C36.181,8.614,38.749,8.071,38.701,9.352z"/>
<path d="M44.94,9.352c-0.336,1.452-3.19,0.276-4.319,0.24C41.392,8.351,43.547,9.639,44.94,9.352z"/>
<path d="M51.661,10.792c-2.162,0.242-3.118-0.722-5.28-0.48C46.883,9.482,51.405,9.765,51.661,10.792z"/>
<path d="M86.22,16.072c-2.685,0.164-5.312-0.956-5.28-2.88C81.011,8.895,91.422,11.543,86.22,16.072z M82.14,13.192
c0.572,1.569,2.509,2.415,4.32,1.44c0.099-1.059-0.065-1.855-0.48-2.4C84.701,11.79,82.343,11.814,82.14,13.192z"/>
<path d="M60.3,11.992c-1.742,0.618-4.228-0.089-5.76-0.48C55.288,10.927,59.201,11.604,60.3,11.992z"/>
<path d="M66.781,13.672c-1.624-0.216-3.132-0.548-4.561-0.96C63.243,11.997,65.987,12.678,66.781,13.672z"/>
<path d="M85.98,13.432c0.126,1.215-1.898,1.554-2.399,0.72C83.832,13.513,85.72,13.266,85.98,13.432z"/>
<path d="M70.14,14.392c3.491-0.13,7.434,1.367,11.04,1.92c-0.615,0.954-2.605-0.277-3.6-0.48
C74.54,15.913,72.676,14.816,70.14,14.392z"/>
<path d="M84.3,16.792c1.247,0.273,2.713,0.327,3.84,0.72C87.493,17.593,84.601,17.856,84.3,16.792z"/>
<path d="M43.5,20.632c-0.992,0.528-1.736,1.304-1.68,2.88c-0.07,1.03,1.131,0.789,1.199,1.68
C40.576,24.951,40.535,20.329,43.5,20.632z"/>
<path d="M43.98,20.872c2.421-0.874,2.685,4.746-0.479,4.08c0.299-0.661,1.226-0.694,1.68-1.2
C45.374,22.199,45.004,21.208,43.98,20.872z"/>
<path d="M32.46,29.752c-0.48,0.342,0.184,1.561,0.72,1.68c-1.511,0.933-1.827-2.226-0.96-3.12c3.597-2.01,5.532,3.849,1.44,3.6
C33.964,30.488,32.242,31.091,32.46,29.752z"/>
<path d="M25.741,33.112c1.15,0.084-0.513,0.719-0.48,1.2c0.018,1.183-0.203,2.604,0.96,2.64c-0.592-0.841,0.243-2.458,0.72-3.6
c2.731,0.658,1.781,4.199-0.239,4.32C24.472,37.806,23.329,34.219,25.741,33.112z"/>
<path d="M38.22,42.712c-0.253-1.106-1.293-1.427-0.96-3.12c-0.61,0.029-0.931,0.349-0.96,0.96
C35.887,35.461,44.143,41.79,38.22,42.712z"/>
<path d="M47.1,46.072c-1.321-2.109-1.041-5.914,2.88-4.8C52.478,43.065,50.231,46.405,47.1,46.072z M46.861,42.472
c0.049,0.751-0.162,1.762,0.239,2.16c0.739,0.179,0.462-0.659,1.2-0.48c-0.267,0.753-0.112,0.634,0.24,1.2
C52.363,44.767,48.946,39.561,46.861,42.472z"/>
<path d="M37.5,42.232C36.934,43.004,36.53,41.388,37.5,42.232L37.5,42.232z"/>
<path d="M30.781,42.952c0.56,0,1.12,0,1.68,0c1.986,1.451,0.51,4.935-2.4,3.6c-0.156-0.835-0.882-2.003-0.239-2.88
c0.47,0.97-0.006,2.885,1.68,2.64C31.005,45.534,30.001,44.231,30.781,42.952z"/>
<path d="M4.861,47.512c0.233,0.006,0.186,0.293,0.239,0.48c-0.204,1.471-1.334,2.713-1.92,4.08c-0.557,1.3-0.519,3.166-1.92,3.84
C1.854,52.506,3.317,49.968,4.861,47.512z"/>
<path d="M54.3,62.632c2.747,3.82-5.6,6.558-4.08,0.72C50.871,62.01,52.984,62.126,54.3,62.632z M53.821,63.592
c-1.709-2.567-5,1.958-2.16,1.92c-0.536-0.474,1.761-1.205,1.68,0C53.614,64.986,53.959,64.531,53.821,63.592z"/>
<path d="M53.821,85.192c0.891-1.578,4.269-2.012,5.279-0.24C60.584,90.521,52.591,90.472,53.821,85.192z M57.901,84.952
c-0.284-0.116-0.545-0.256-0.721-0.48c-2.651-0.412-3.296,2.625-1.68,3.84C57.95,88.96,58.699,86.766,57.901,84.952z"/>
<path d="M56.701,86.152c0.16,0,0.319,0,0.479,0c0,0.16,0,0.32,0,0.48c-0.16,0-0.319,0-0.479,0
C56.701,86.472,56.701,86.312,56.701,86.152z"/>
<path d="M55.5,87.352c0.621-0.306,1.539-0.306,2.16,0C57.558,88.531,55.73,88.054,55.5,87.352z"/>
<path d="M47.821,95.272c0.654,3.768-3.346,3.171-3.601,1.2C43.964,94.484,46.394,93.683,47.821,95.272z M45.901,97.432
c0.159-0.334-0.05-1.905-0.721-2.16C44.746,95.882,44.954,97.453,45.901,97.432z"/>
<path d="M40.861,98.632c0.105,1.306-0.608,1.792-0.96,2.64c-0.801,0-1.601,0-2.4,0C35.321,99.4,38.75,95.462,40.861,98.632z
M38.94,101.032c-0.208-0.752-0.594-1.325-1.199-1.68C37.674,100.378,38.437,100.576,38.94,101.032z"/>
<path d="M61.5,108.232c0.545,1.2,0.064,2.764-0.24,3.84C55.917,115.484,55.839,104.695,61.5,108.232z M57.661,110.632
c0.652,3.805,5.875-1.417,1.439-2.4C58.46,108.886,57.509,109.749,57.661,110.632z"/>
<path d="M40.621,113.512c-0.878-0.801-1.726-1.134-1.44-2.64c0.344-1.811,3.449-2.23,3.84-0.24
C43.258,111.84,42.221,112.906,40.621,113.512z M40.861,112.552c-0.223-0.578-0.438-1.162-0.721-1.68
c-0.075,0.325-0.342,0.458-0.239,0.96C40.671,111.621,40.229,112.623,40.861,112.552z"/>
<path d="M59.581,109.672c0.295,0.105,0.493,0.308,0.479,0.72c-0.388,0.068-0.413-0.227-0.72-0.24
C59.386,109.958,59.582,109.913,59.581,109.672z"/>
<path d="M58.621,110.632c0.744-0.024,1.146,0.293,1.68,0.48C60.05,111.75,58.254,111.922,58.621,110.632z"/>
<path d="M64.621,135.353c0.086-1.606-0.272-2.769-0.96-3.601c-3.562-0.15-7.859,0.044-11.28,0.72c0.133,0.987,0.688,1.552,0.72,2.64
c-0.504,0.299-1.229,1.1-1.92,0.479c-0.299-1.02,0.799-0.642,0.96-1.2c0.019-1.458-0.993-1.886-0.72-3.6
c1.929,0.088,2.346-0.335,3.36,0.24c0.645,0.165,0.464-0.496,0.72-0.72c0.646-0.165,0.464,0.496,0.72,0.72
c0.461-0.957,1.359-0.047,1.681-0.96c0.645-0.166,0.463,0.496,0.72,0.72c0.422-0.058,0.547-0.413,0.479-0.96
c0.505,0.528,0.937,0.528,1.44,0c0.523-0.043,0.764,0.196,0.72,0.72c0.946-0.028,0.734-1.043,1.68-0.479
c0.631-0.161,0.505,0.614,0.721,0.72c0.215,0.105,0.423-0.477,0.479-0.48c0.373-0.021,0.549,1.291,1.92,0.72
c0.171,1.864,0.423,2.749,0.721,4.08c3.811,0.014,0.37-4.601,0-6.24c-5.258-0.676-12.689,0.141-17.521,1.44
c0.228,1.728,1.729,3.344,1.2,5.041c-1.065-1.496-2.048-3.073-2.4-5.28c0.233-0.007,0.187-0.294,0.24-0.48
c4.899-1.854,12.457-2.218,18.48-2.16c0.611,0.645,1.104,1.897,1.68,3.36c0.457,1.163,1.41,2.914,1.2,3.84
C69.249,136.444,66.248,137.033,64.621,135.353z"/>
<path d="M44.22,138.231c0.101,2.525-3.294,3.207-5.04,2.4C39.008,138.113,42.058,137.625,44.22,138.231z M40.14,140.394
c1.207,0.246,1.316-0.604,1.92-0.961c0.345,0.366,1.251,0.233,1.2-0.479C42.089,138.349,39.939,138.825,40.14,140.394z"/>
<path d="M64.38,138.474c3.496-0.535,7.481,0.518,10.08,1.68c-0.366,1.195-1.776,0.125-2.4,0
C69.596,139.659,66.998,138.842,64.38,138.474z"/>
<path d="M95.581,143.271c-2.942,0.644-5.514-1.002-6-3.6C91.774,136.823,96.583,138.774,95.581,143.271z M91.98,139.192
c-0.631,0.644-1.162,2.258,0.24,2.398c-0.14-0.859,0.711-0.729,1.44-0.72c0.058,0.343,0.395,0.405,0.479,0.72
c-0.166,0.475-1.339-0.059-1.438,0.48c0.669,0.305,1.187,0.592,1.92,0.24C94.852,140.16,93.672,139.42,91.98,139.192z"/>
<path d="M32.22,145.192c-0.032,2.367-1.528,3.271-3.84,3.359C27.594,146.242,29.819,143.58,32.22,145.192z M29.1,147.832
c1.404-0.115,1.685-1.354,2.4-2.16C30.131,144.871,28.847,146.213,29.1,147.832z"/>
<path d="M86.701,146.151c1.792,0.688,3.44,1.52,4.56,2.881C89.319,148.494,87.486,147.847,86.701,146.151z"/>
<path d="M114.061,170.151c-0.939,0.369-1.713-1.262-2.16-2.159c-3-4.039-7.217-6.863-10.319-10.8
c-3.623-1.817-5.706-5.176-9.36-6.961c0.085-0.274,0.423-0.607,0.48-0.239c3.505,1.541,6.238,4.271,9.119,6.72
c1.355,1.153,2.771,2.489,4.08,3.841C108.777,163.521,112.146,165.974,114.061,170.151z"/>
<path d="M114.541,155.271c-2.948-0.092-4.854-1.227-4.8-4.318C112.334,147.631,117.725,152.144,114.541,155.271z M110.939,151.433
c0.082,1.918,1.41,2.592,3.121,2.881c0.135-0.347,0.375-0.586,0.721-0.722C115.197,151.412,112.15,149.688,110.939,151.433z"/>
<path d="M111.9,152.151c0.914,0.046,1.58,0.342,1.681,1.199C112.934,153.824,111.773,153.204,111.9,152.151z"/>
<path d="M130.621,163.912c-2.326,0.858-4.162-0.562-4.32-2.642C127.866,158.646,132.686,160.926,130.621,163.912z M127.741,161.992
c1.332-0.946,1.198,0.989,1.68,1.2c0.062-0.338,0.526-0.274,0.72-0.48c-0.092-0.788-0.701-1.059-0.959-1.68
C128.372,160.879,127.077,161.467,127.741,161.992z"/>
<path d="M146.939,179.992c-3.486,2.58-6.062-3.995-3.358-5.04C146.005,174.017,147.825,177.625,146.939,179.992z M145.741,179.512
c1.25-1.596-0.574-3.557-1.921-4.079C143.225,177.119,143.824,179.551,145.741,179.512z"/>
<path d="M144.061,177.353c0.805-0.164,0.877,0.402,0.959,0.961C144.27,178.424,143.949,178.104,144.061,177.353z"/>
<path d="M153.42,206.151c0.619,0.422,0.499,1.582,0.721,2.4C153.458,208.192,153.473,207.14,153.42,206.151z"/>
<path d="M170.701,263.271c1.24,0.039,1.096,1.465,2.64,1.201c0.423-0.059,0.548-0.414,0.479-0.961c0.088-1.207-2.1-0.139-1.68-1.68
c1.793-0.824,3.146,0.895,3.119,1.92C175.193,266.483,169.829,266.367,170.701,263.271z"/>
<path d="M166.621,263.271c0.523,0.037,0.516,0.605,0.479,1.201C166.387,264.522,166.254,263.616,166.621,263.271z"/>
<path d="M29.581,13.432c-1.118,1.762-2.535,3.225-4.561,4.08c-0.978,1.865-3.021,2.788-4.319,4.32
c-0.634,0.748-1.043,1.827-1.681,2.64c-0.617,0.788-1.547,1.365-2.159,2.16c-1.095,1.42-1.889,3.176-2.881,4.8
c-2.798,4.579-5.413,9.826-8.159,14.88c-0.323-1.007,0.498-2.206,0.96-3.12c1.375-2.724,2.946-5.975,4.56-8.64
c1.747-4.172,4.961-6.879,6.96-10.8c2.189-1.661,3.682-4.135,5.76-6c0.666-0.597,1.669-0.798,2.4-1.44
C27.522,15.38,27.938,13.633,29.581,13.432z"/>
<path d="M150.061,192.231c1.83,1.771,1.967,5.232,2.399,8.4C151.748,197.745,150.889,195.005,150.061,192.231z"/>
<path d="M152.46,200.872c0.647,1.271,0.92,2.92,0.96,4.8C152.854,204.317,152.702,202.551,152.46,200.872z"/>
<path d="M48.541,110.872c-3.429-3.012,2.593-5.527,2.88-2.16C51.547,110.201,50.337,110.811,48.541,110.872z M48.3,109.672
c0.668,0.385,0.664-1.036,0.24-0.96C48.465,109.037,48.199,109.17,48.3,109.672z"/>
<path d="M21.42,160.792c1.934-1.906,2.754-4.927,5.28-6.239c-2.298,3.544-5.058,6.622-6.48,11.039
c3.271-4.889,6.15-10.17,10.561-13.92c0.353-0.912-1.073-0.047-0.721-0.96c2.43-1.57,3.76-4.24,6.96-5.04
c-0.101,0.779-0.997,0.763-1.199,1.44c2.234-1.125,3.954-2.766,6.479-3.602c0.562-0.078,0.233-1.047,0.72-1.199
c3.039-1.44,6.383-2.578,10.561-2.881c0.624,0.633-0.388,0.189-0.24,0.961c0.72,0,1.44,0,2.16,0c-1.44-1.789-1.267-3.211,0.24-5.76
c0.573-0.972,1.111-2.216,1.92-2.4c0.204,2.628-2.214,3.833-1.92,5.76c1.9,0.694,4.79-0.966,6.96,0
c-0.317,1.442-3.182,0.339-4.32,0.96c6.704,0.176,12.813,0.945,17.521,3.12c3.945-1.339,7.001,2.024,9.6,3.6
c0.481,0.488,0.062,0.64-0.24,0.961c4.866,3.396,9.953,6.079,14.4,10.079c2.279,2.051,4.136,3.671,6.24,6
c0.603,0.668,1.541,1.234,2.158,1.921c0.623,0.689,0.996,1.632,1.682,2.399c1.046,1.172,2.52,1.681,2.88,3.601
c3.112,1.927,6.022,4.056,10.08,5.039c-2.345-1.815-5.804-2.518-8.16-4.319c1.046-0.662,1.854,0.629,2.64,0.961
c0.989,0.416,2.129,0.528,3.12,0.959c1.798,0.779,3.322,1.97,5.04,2.881c4.528,2.402,9.151,4.309,14.16,6.479
c-0.441,1-0.904,0.837,0.24,1.439c0.377-0.503,0.219-2.104,0-3.12c-0.719-3.334-2.582-6.436-4.08-9.602
c-1.439-3.043-3.422-6.012-3.84-9.6c0.725-1.034,2-1.52,3.359-1.92c-1.265-7.012-1.57-15.238,0.239-22.32
c0.979-3.827,2.716-7.71,4.08-11.76c1.356-4.028,2.086-8.338,3.36-12.48c1.813-5.897,4.24-11.626,6-17.76
c0.576-2.014,1.359-3.847,1.92-5.76c0.439-1.503,0.457-3.101,0.96-4.56c2.241-6.504,4.963-13.056,7.92-19.2
c1.229-2.554,2.055-5.326,3.358-7.44c0.334-0.541,1.066-0.881,1.44-1.44c0.547-0.818,0.683-1.998,1.2-2.88
c0.468-0.798,1.158-1.408,1.68-2.16c1.143-1.652,2.04-3.624,3.12-4.8c2.771-3.019,6.896-4.626,9.12-6.96
c-1.417-0.79-3.008-0.504-4.561-0.72c-15.819-2.198-31.188-5.902-45.119-10.08c-6.028-1.02-12.305-2.654-18-4.32
c-4.121-1.205-7.994-2.168-12-3.36c-1.084-0.322-2.811-0.442-3.36-1.68c-1.199,0.555-2.575-0.564-3.84-0.72
c-1.944-2.305-1.838-4.593-3.36-6.96c-2.579-4.011-10.535-4.885-16.8-6c-5.283-0.94-11.398-1.612-17.28-1.92
c-2.405-0.126-4.836,0.255-6.72,0c-0.96-0.13-1.744-0.689-2.64-0.72c-4.317-0.151-7.787,3.14-10.801,4.08
c1.681,8.378,5.883,16.13,8.16,24.72c0.181,0.679,0.077,1.48,0.24,2.16c1.192,4.97,2.028,11.915,2.88,18.24
c0.325,2.417,0.567,4.839,0.96,6.96c0.995,5.377,1.769,10.711,2.64,16.08c0.621,3.823,1.868,7.249,1.921,11.28
c0.023,1.867,0.833,3.842,1.199,5.76c1.145,5.99,1.941,11.323,3.12,17.76c0.664,3.625,1.143,6.605,1.92,10.56
c0.213,1.082,0.807,3.903,0.48,5.76c-1.362-0.676-0.612-2.755-0.72-3.84c-0.321-3.231-1.526-6.321-2.16-9.6
c-0.812-4.2-1.122-8.545-1.92-12.96c-0.743-4.109-1.776-8.241-2.4-12.48c-0.624-4.238-1.288-8.525-2.16-12.72
c-0.891-4.285-1.224-8.516-1.92-12.72c-1.09-6.581-2.213-13.001-2.88-19.44c-1.357-13.099-6.619-24.24-10.8-34.56
c-1.124,0.297-2.572,1.345-3.601,0.96c1.537-1.967,4.458-2.744,7.2-4.08c3.129-1.523,6.393-3.895,10.32-2.64
c9.006-3.378,18.614-3.365,29.04-2.88c3.564,0.166,6.843,0.097,10.08,0.72c2.062,0.397,4.294,0.803,6.479,1.44
c0.57,0.166,1.107,0.608,1.681,0.72c0.588,0.115,1.326-0.156,1.92,0c3.017,0.792,6.084,2.503,9.121,3.6
c9.119,3.295,17.252,8.793,24,14.16c13.316,2.752,27.009,5.719,39.84,9.12c3.088,0.819,6.115,2.127,9.119,3.12
c2.354,0.778,4.222,0.81,5.761,2.4c0,1.12,0,2.24,0,3.36c-1.306,1.416-2.587,2.854-4.8,3.36c-1.762,1.354-3.555,2.489-5.28,3.84
c-4.033,3.158-6.948,7.572-9.601,12.24c-2.646,4.656-5.75,9.431-6.961,14.88c-3.103,4.097-3.572,10.826-6,15.6
c-0.846,4.674-2.132,8.907-3.84,12.72c-3.023,12.093-7.494,23.404-10.8,36c-1.067,4.07-2.05,8.055-1.68,12.721
c0.192,2.447,1.304,4.592,0.96,7.92c2.38,2.41,5.91,1.207,6.479,4.32c0.149,0.815-0.469,1.732-0.238,2.641
c0.31,1.228,1.4,1.582,2.158,2.64c0.367,0.513,0.401,1.11,0.721,1.681c0.6,1.071,1.973,1.904,1.921,3.12
c-1.71-1.116-5.939-0.12-6.722-2.641c-0.326-1.056-0.051-2.962,0.961-3.84c-0.551-0.81-1.742-0.979-2.399-1.681
c-0.479,0.642-0.043,2.196-0.96,2.399c-1.686,0.006-2.42-0.94-3.36-1.681c2.378,6.023,5.962,10.84,7.2,18
c-0.499,0.521-0.775-0.028-1.2-0.239c0.568,1.512,1.103,3.06,2.642,3.601c0.24-0.32,0.396-0.725,0.72-0.961
c-0.04-1.24-2.19-0.369-1.92-1.92c2.896,1.111,8.071,0.932,6.72,5.521c1.604-0.086,1.508-1.227,1.439-2.16
c-0.116-1.637-1.732-1.745-2.159-2.641c2.247-0.848,2.809,1.961,3.6,3.359c1.896-1.439-0.931-3.092-0.72-4.08
c2.275-0.436,1.814,1.866,2.88,2.641c0.519-1.938-1.698-2.217-0.96-3.6c-1.312-0.848-1.551-2.77-2.16-4.32
c-0.871-0.131-1.943,0.078-2.88-0.479c-0.745-1.229-1.812-3.401-0.72-5.04c1.875-0.539,3.267,0.706,3.84,1.92
c0.294,0.623,0.039,1.396,0.24,2.16c0.467,1.771,1.71,3.521,1.92,5.04c1.044,0.637,2.349,1.011,2.88,2.16
c-0.806,3.459-0.021,6.384,0.96,10.319c0.884,3.547,1.48,6.951,2.4,10.319c0.781,2.868,1.963,7.187,2.64,10.56
c0.11,0.556-0.071,1.119,0,1.682c0.508,3.995,2.243,8.225,3.36,12.479c1.8,6.861,2.722,13.881,4.318,21.121
c0.688,3.112,2.145,5.914,2.4,8.879c6.355-0.617,5.865,10.313-1.44,8.16c-0.606-0.666-2.256-1.324-1.438-2.64
c1.234,1.05,1.816,1.95,3.6,1.681c1.979-0.301,3.092-2.835,1.92-4.801c-1.634-1.834-3.564,0.15-5.279-0.24
c-0.351-1.471,1.483-0.756,1.439-1.92c0.104-1.543-0.771-2.107-0.96-3.359c-0.397,0.242-0.261,1.021-0.479,1.439
c-1.474-0.113-1.64,1.081-2.881,1.2c-0.815-1.983-1.211-4.39-1.68-6.72c0.596-0.604,2.02-0.381,2.16-1.44
c0.73-0.011,0.934,0.507,1.439,0.72c0.184-1.303-0.297-1.943-0.24-3.119c-0.743,0.317-1.233,0.744-1.92,1.199
c-0.512,0.342-0.961,1.149-1.92,0.961c-0.873-1.607-0.926-4.035-1.199-6.24c1.027-0.469,2.085-1.998,3.6-1.199
c-0.049-2.191-0.803-3.678-0.96-5.762c-0.661,0.756,0.343,1.889,0,3.121c-0.632,0.172-0.909,1.42-1.68,0.721
c-0.278,0.762-0.327,1.752-1.44,1.68c-0.63-1.93-1.523-3.596-1.439-6.239c1.488-0.493,2.322-2.764,4.08-1.2
c-0.291-2.67-1.105-4.814-1.681-7.199c-0.632,0.961,0.533,2.18,0.24,3.6c-0.776,1.223-2.252,1.748-2.88,3.12
c-1.539-1.251-1.508-4.423-1.92-6.72c0.602-0.373,0.804-0.975,1.68-0.48c0.105-0.693,0.506-1.094,1.2-1.199
c0.582,0.297,1.116,0.643,1.438,1.199c-0.136-1.464-0.621-2.578-0.959-3.84c-1.062,0.138-0.719,1.682-1.92,1.68
c-0.482,0.398-0.183,1.578-0.961,1.68c-2.101-0.4-2.123-4.084-2.16-5.76c0.31-1.292,1.816-1.384,2.642-2.16
c1.34,0.42,1.255,2.267,1.92,3.36c-0.037-1.646-0.894-4.545-1.2-6.72c-1.124,0.044-3.02,1.266-3.6,2.641
c-0.644,0.646-0.913-0.307-1.44-0.48c0.653,3.693,1.729,7.789,2.64,11.761c0.703,3.063,1.023,6.019,2.4,7.92
c0.721,8.088,4.225,15.449,5.28,24c-1.283-1.156-1.265-3.088-1.681-4.562c-2.156-7.615-4.125-15.852-5.761-24.238
c-1.569-8.059-4.251-15.672-5.04-23.762c1.043,0.799,0.825,2.855,1.2,4.32c0.996-0.684,2.788-0.572,3.12-1.92
c0.891-0.17,1.029,0.41,1.92,0.24c-1.221-6.25-2.62-12.502-4.08-18.721c-0.438-1.863-0.727-4.033-1.92-5.521
c-1.1,0.021-1.098,1.144-1.92,1.44c-2.129-0.365-2.719,1.443-5.28,1.68c1.589,9.702,4.788,19.036,6.479,28.8
c0.302,1.738,0.367,3.38,0.721,5.041c0.557,2.615,1.475,5.364,2.16,8.64c2.146,10.254,6.004,21.727,8.16,32.399
c2.027,0.351,2.928-0.432,4.318-0.72c0.418,2.652-3.131,2.361-5.039,1.92c-6.931-24.35-12.082-50.479-18-75.84
c-2.002-1.438-2.99-3.891-4.801-5.521c-0.678-0.197-0.621,0.34-1.199,0.24c-4.386-2.818-9.349-4.729-14.16-7.199
c-4.744-2.436-9.889-4.223-14.159-7.201c-1.263-3.646-4.685-6.091-7.681-9.119c-2.334-2.357-4.869-4.594-7.439-6.721
c-2.421-2.001-5.087-3.658-8.4-5.76c-2.435-1.543-6.654-4.709-9.12-4.8c-1.204-0.044-2.162,0.894-3.359,1.44
c-1.265,0.575-2.666,0.76-3.84,1.438c-2.599,1.504-6.572,4.528-8.641,6.721c-0.7,0.742-1.223,2.046-1.92,3.119
c-1.686,2.597-2.355,3.652-3.6,6.961c-1.01,2.685-1.321,5.697-2.16,9.119c-0.304,1.24-0.88,3.11-0.96,5.28
c-0.161,4.354-0.081,9.812,0.479,14.16c0.47,3.643,1.634,5.873-1.439,7.199c1.128,5.896-2.103,8.493-7.2,7.92
c-1.755-0.196-3.433-1.703-4.8-1.439c-0.772,0.148-1.158,1.165-1.68,1.199c-0.012,2.816,3.443,8.568,3.119,12
c-0.345,3.664-5.084,4.781-8.159,2.4c-2.018-5.104-3.257-10.984-5.04-16.32c-1.104-1.088-2.422-0.971-4.32-1.68
c-0.448-0.168-0.741-0.642-1.2-0.721c-2.181-0.375-4.735,0.471-6.479-0.24c-1.671-0.68-1.605-2.477-2.881-3.359
c-2.689-13.207-0.238-23.942,3.36-34.08c0.355-1,1.099-1.922,1.44-2.879c0.316-0.893,0.148-1.949,0.479-2.881
c0.616-1.729,1.61-3.226,2.16-5.041c1.552-5.117,3.843-11.199,4.8-17.278c1.435-9.108,0.173-19.464-1.439-27.84
c-0.581-3.016-0.713-5.869-1.44-8.64c-0.86-3.279-2.544-6.25-3.36-9.36c-0.188-0.718,0.001-1.275-0.239-1.92
c-0.484-1.298-1.316-2.454-1.92-3.84c-0.254-0.583-0.681-1.277-0.96-1.92c-0.551-1.265-1.039-2.307-1.44-4.08
c-1-1.933-1.815-3.799-2.88-5.52c-3.057-4.945-6.939-9.452-10.561-14.16c-1.982-1.733-3.234-2.961-2.88-6.96
c0.488-5.49,3.643-10.933,6-16.32c2.413-5.511,5.669-10.261,8.16-13.92c-0.681-3.26,0.508-5.816,4.32-5.28
c1.648-1.083,0.856-4.006,2.88-4.8c0.818-0.321,1.811,0.003,2.4-0.24c1.817-0.749,1.167-2.514,2.399-3.36
c1.08-0.742,2.506-0.357,3.601,0c-0.578,2.316-2.125,4.626-5.04,3.84c0.284,2.28-1.472,3.962-3.12,5.04
c-0.904,0.519-1.017-0.376-1.92-0.479c-0.671,0.613,1.168,1.048,0.239,1.92c-0.883,2.076-2.336,3.584-4.56,4.32
c-3.07,3.984-6.422,8.912-8.64,14.16c-0.396,0.937-0.553,2.007-0.96,2.88c-0.777,1.662-1.557,3.044-2.4,5.28
c-0.337,0.892-0.643,1.788-0.96,2.88c-0.519,1.786-1.834,4.843-1.68,6.24c0.31,2.806,5.646,6.404,7.439,8.4
c1.528,1.702,3.668,5.2,5.28,7.68c1.484,2.283,3.561,5.287,4.8,8.16c0.76,1.76,1.024,3.335,1.68,4.8
c0.614,1.371,1.233,2.71,2.16,4.32c1.305,6.888,4.467,14.05,5.761,21.84c0.322,1.945,0.454,4.176,0.72,6.24
C30.615,132.855,26.544,148.64,21.42,160.792z M127.5,20.152c-5.646-3.555-10.348-8.052-17.521-10.08
c-0.875-1.489-2.273-2.128-4.078-2.88c-3.35-1.396-7.562-3.075-10.801-2.64c-1.82-1.175-4.289-0.83-6.721-1.2
c-2.28-0.347-4.444-1.439-6.72-1.68c-1.344-0.142-2.785,0.07-4.32,0c-7.497-0.34-13.854-0.484-19.68,0.48
c-1.943,0.321-3.91,0.105-4.8,1.2c12.866,0.814,25.084,2.275,35.04,6c2.174,1.906,3.231,4.928,4.56,7.68
c3.756,0.281,7.337,0.812,11.04,1.44c3.771,0.638,7.745,0.647,11.28,1.44c1.232,0.276,2.434,0.909,3.6,1.2
c1.477,0.369,2.948,0.386,4.32,0.72c3.857,0.938,7.084,3.071,10.8,3.6C132.611,22.562,128.557,22.856,127.5,20.152z M26.701,10.552
c-0.081,0.799-0.695,1.064-0.48,2.16c1.457-0.143,2.016-1.185,2.4-2.4C28.04,9.76,27.455,10.557,26.701,10.552z M21.18,17.752
c1.42-0.58,2.396-1.604,2.641-3.36C21.856,12.896,19.951,16.054,21.18,17.752z M15.901,19.192c-0.65,1.087-1.234,3.719,0.479,4.08
c1.305-0.855,2.313-2.007,2.64-3.84C18.334,18.765,16.911,18.93,15.901,19.192z M100.38,20.872c-0.043-0.69-1.745-0.956-2.159-0.48
C99.227,20.265,99.561,20.812,100.38,20.872z M119.581,26.152c8.754,2.136,16.763,4.572,25.2,6.72
c3.148,0.801,6.432,1.065,9.84,1.68c2.46,0.443,5.003,1.364,6.96,1.68c4.544,0.733,8.42,1.37,12.72,1.92
c2.854,0.364,6.668,1.791,9.12-0.72c-0.155-1.847-2.051-2.091-3.36-2.64c-9.778-4.101-21.683-7.286-32.64-8.88
c1.938,0.772,3.869,1.362,6,1.92c2.259,0.591,4.64,0.699,6.96,1.2c0.835,0.18,1.508,0.71,2.4,0.96
c0.902,0.253,1.931,0.237,2.88,0.48c3.551,0.907,6.789,2.409,9.84,3.6c1.567,0.612,3.974,0.669,4.319,2.64
c-1.635,2.107-4.569,0.608-6.959,0.24c-3.174-0.488-5.125-0.856-7.44-1.44c-4.142-1.043-8.385-1.535-12.72-2.4
c-5.418-1.082-10.871-2.907-16.32-4.08c-5.503-1.185-10.977-2.667-16.32-4.08c-2.738-0.725-5.652-0.984-8.398-1.68
c-3.6-0.912-7.153-2.42-10.561-2.16C106.85,22.779,113.795,24.741,119.581,26.152z M135.42,23.032c-1.168,0.12-2.3-1.049-3.12-0.48
C132.919,23.471,134.123,23.59,135.42,23.032z M137.581,23.512c-0.429,0.001-1.396-0.646-1.681,0
C136.659,23.182,137.35,24.106,137.581,23.512z M138.781,23.752C137.244,23.475,138.253,24.485,138.781,23.752L138.781,23.752z
M140.701,24.232c-0.418,0.018-0.747-0.054-0.96-0.24C139.355,24.661,140.777,24.656,140.701,24.232z M146.939,25.672
c-0.42,0.066-1.196-0.675-1.438,0C145.921,25.606,146.698,26.348,146.939,25.672z M50.46,140.872
c0.413,0.013,0.615-0.186,0.72-0.479C50.767,140.379,50.565,140.576,50.46,140.872z M83.34,145.192
c-0.599,0.171-0.986-1.058-1.439-0.24C82.156,145.124,83.005,145.89,83.34,145.192z M43.741,158.151
c-0.613,1.31-1.417,2.559-1.92,3.841c-1.446,3.681-2.29,8.405-3.36,12.239c-0.197,0.707-0.124,1.976-0.96,2.4
c0.58-5.143,1.586-9.746,3.12-13.921c1.579-4.298,3.745-8.028,4.8-12c-1.188,2.661-2.916,5.207-4.08,8.16
c-0.661,1.677-0.845,3.556-1.439,5.28c-0.392,1.133-1.079,2.006-1.44,3.119c-0.397,1.227-0.381,2.485-0.72,3.84
c-0.347,1.387-1.107,2.686-1.44,4.08c-0.313,1.313,0.229,3.015-1.2,3.841c0.68-4.237,1.386-8.304,2.4-12.24
c0.995-3.864,2.999-7.331,3.6-10.8c-2.037,3.979-3.683,9.367-4.8,13.68c-0.562,2.166-1.047,4.053-1.68,5.76
c-0.333,0.898-0.376,2.038-1.2,2.642c0.995-3.695,1.656-7.688,2.64-11.521c0.853-3.32,2.179-6.689,3.36-9.601
c0.623-1.534,1.54-2.929,1.68-4.56c-4.529,7.229-6.201,17.317-8.88,26.398c0.265-7.02,3.061-14.855,4.561-20.641
c-3.25,7.15-5.026,15.774-6.721,24.48c-0.278-3.48,0.833-6.847,0.721-9.361c-0.837,2.524-0.938,5.783-1.92,8.16
c0.249-8.07,2.284-14.354,4.079-20.879c-1.099,0.604-1.108,2.125-1.439,3.359c-0.33,1.229-0.892,2.562-1.2,3.84
c-0.954,3.941-1.397,8.85-2.399,12.24c-0.175,0.59,0.17,1.729-0.721,1.92c0.333-4.309,1.486-9.072,1.681-12.721
c-1.289,3.992-1.331,9.229-2.641,13.201c-0.339-2.184,0.607-4.351,0-6c0,0.56,0,1.118,0,1.68c-0.873,1.207-0.36,3.8-1.2,5.039
c-0.27-0.689-0.048-1.871-0.479-2.399c0.245,0.824-0.396,1.979-0.72,1.44c-0.22-1.982,1.028-3.769,0.239-5.521
c-0.602,3.717-0.384,8.256-1.92,11.039c-0.146-0.574,0.302-1.741-0.239-1.92c-0.188,0.625-0.413,2.451-0.961,1.92
c0.091-1.16,0.98-3.078,0-3.84c0.321,1.521-0.976,3.624-1.68,1.92c-0.085,1.595-0.108,3.252-0.479,4.562
c-0.516-0.525-0.113-1.969-0.721-2.4c-0.175,0.545,0.433,1.873-0.479,1.68c-1.05-2.633,0.628-7.181-0.48-9.84
c-1.418,6.688-0.179,14.127,0,21.6c2.076,1.42,3.499,0.545,5.761,0.721c1.369,0.106,3.233,0.787,5.04,1.439
c1.765,0.639,3.736,0.938,5.039,1.44c2.034,0.786,3.52,2.353,5.28,2.159c1.335-0.146,2.019-1.488,3.12-1.92
c2.678,1.703,7.881,2.992,10.32,0.239c1.113-1.255,1.084-3.026,0.479-5.278c0.331-1.271,1.749-1.451,2.4-2.4
c-2.405-9.068-1.622-20.285,0.479-28.561c0.488-1.918,0.75-4.174,1.44-6c0.134-0.353,0.766-0.771,0.96-1.199
c0.886-1.948,1.236-2.836,2.16-4.32c1.059-1.702,2.208-3.387,3.12-4.8c-2.922,1.388-4.375,5.03-5.521,7.681
c-0.801,1.852-1.863,3.79-2.399,5.279c-0.763,2.117-0.429,4.688-2.16,6.24c0.534-2.24,0.688-4.629,1.439-6.721
c0.263-0.728,0.917-1.197,1.2-1.921c0.23-0.586,0.055-1.355,0.24-1.92c0.108-0.33,0.553-0.344,0.72-0.72
c0.202-0.454,0.059-1.048,0.24-1.439c1.094-2.361,2.931-4.074,3.36-6.24c-0.611,0.59-1.02,1.382-1.92,1.681
c-0.104,1.799-1.573,2.836-2.4,4.319c-1.735,3.113-3.22,6.397-4.561,9.36c-0.674,1.49-1.021,3.412-2.399,4.56
c1.534-4.945,3.68-9.279,5.76-13.68c0.425-0.216,0.701-0.578,1.2-0.72c0.619-2.661,2.062-4.499,3.36-6.479
c-3.013,0.856-3.713,4.425-5.04,7.199c-0.45,0.94-1.206,1.77-1.681,2.641c-0.541,0.994-0.717,2.013-1.2,3.119
c-0.875,2.007-2.194,3.858-2.88,5.761c-0.434,1.204-0.337,3.796-1.92,3.84c1.629-3.503,2.437-7.214,3.84-10.8
c1.331-3.398,3.999-6.447,4.08-10.08c-3.672,5.197-6.271,12.139-8.399,18.48c-0.297,0.883-0.301,2.074-1.2,2.64
c0.978-7.423,4.625-13.454,6.479-19.2c-1.604,2.814-3.279,5.785-4.56,9.12c-1.296,3.375-1.818,7.194-3.36,10.56
c0.077-3.477,1.145-7.088,2.4-10.56c0.404-1.118,0.97-2.237,1.44-3.36c0.494-1.182,0.69-2.555,1.199-3.6
c0.804-1.65,2.572-2.846,2.641-4.801c-0.997,0.047-1.504,1.209-1.92,1.92c-3.448,5.897-4.921,13.352-7.44,19.681
c0.259-2.94,0.81-5.591,1.92-7.681c0.018-3.902,2.939-6.178,2.641-9.601c-1.471,2.734-2.318,5.974-3.36,9.121
c-1.07,3.234-1.825,6.58-3.12,9.601c0.832-8.607,3.912-14.969,6.48-21.84c0.947-1.293,2.015-2.466,2.159-4.562
c0.559-0.647,1.829-1.842,1.44-2.64C47.226,150.467,45.48,154.437,43.741,158.151z M61.26,149.032
c0.413,0.013,0.615-0.186,0.72-0.479C61.568,148.539,61.365,148.735,61.26,149.032z M32.46,149.752
c0.601,0.041,0.751-0.369,0.96-0.72C32.82,148.991,32.669,149.399,32.46,149.752z M49.5,150.712
C48.405,152.59,50.104,150.498,49.5,150.712L49.5,150.712z M52.14,154.553c0.787-1.134,1.448-2.393,2.16-3.601
C52.851,151.267,52.217,153.665,52.14,154.553z M135.18,164.633c-0.4-0.397-0.189-1.409-0.24-2.159
c0.188-0.054,0.279-0.201,0.24-0.48c0.703,0.062,0.737-0.543,1.44-0.48c0.905-0.104,1.127,0.475,1.68,0.721
c0.244-1.283-0.329-1.75-0.96-2.159c-1.496,0.184-2.255,1.104-3.6,1.438C133.689,162.928,134.73,164.515,135.18,164.633z
M139.5,161.992c0.439,0.462,0.812-0.205,1.2-0.479c-0.343-0.058-0.405-0.396-0.722-0.479
C139.977,161.509,139.326,161.338,139.5,161.992z M136.141,163.672c1.604-0.376,1.142,0.785,0.959,1.681
c0.455-0.054,0.529,0.271,0.961,0.239c-0.129-1.552-0.433-2.928-1.92-3.118C136.141,162.872,136.141,163.271,136.141,163.672z
M140.46,163.672c0.478,0.004,0.306,0.655,0.96,0.479c0.075-0.323,0.342-0.459,0.24-0.959
C141.232,163.324,140.664,163.315,140.46,163.672z M30.06,165.353c0.139-0.146,0.562-1.139,0-1.199
C30.373,164.748,29.356,165.147,30.06,165.353z M136.141,169.433c-0.297-1.623-1.065-2.773-1.92-3.841
C134.659,167.074,135.413,168.24,136.141,169.433z M29.821,166.553c0.064-0.327-0.149-0.938-0.24-0.479
C29.515,166.397,29.729,167.009,29.821,166.553z M27.661,168.474c0.73-0.082,0.964-1.9,0.479-2.4
C27.971,166.863,27.754,167.606,27.661,168.474z M141.9,167.512c0,0.24,0,0.48,0,0.722c0.324,0.326,1.676,0.457,1.681-0.24
C143.09,167.731,142.6,166.842,141.9,167.512z M19.02,168.712c0.07-0.146,0.598-0.924,0-0.96
C18.951,167.899,18.423,168.676,19.02,168.712z M31.26,172.553c-0.155-1.84,1.534-3.102,0.72-4.801
C31.852,168.927,30.532,171.974,31.26,172.553z M143.34,170.633c0.68,0.197,0.622-0.339,1.2-0.239
c0.076-0.558-0.039-0.922-0.24-1.201C143.473,169.164,143.184,169.676,143.34,170.633z M27.18,172.072
c0.136-0.677,0.835-2.029,0.24-2.642C27.301,170.125,26.843,171.719,27.18,172.072z M137.82,172.792
c-0.106-0.613-0.336-1.104-0.721-1.439C137.223,171.95,137.092,172.8,137.82,172.792z M25.5,173.271
c-0.171,4.112,0.753-0.395,0.24-0.719C25.703,172.834,25.782,173.233,25.5,173.271z M148.38,175.192
c1.005,0.115,1.526,0.713,1.44,1.92c1.74-0.732-0.082-3.19-1.2-3.36C148.344,174.035,148.35,174.601,148.38,175.192z M21.42,175.433
c0.038-0.166-0.084-1.396-0.24-0.721C20.969,175.626,21.309,175.914,21.42,175.433z M16.14,178.553c0.035-0.284,0.428-0.545,0-0.721
C16.106,178.115,15.713,178.376,16.14,178.553z M128.46,178.312c-0.315-0.244-0.885-0.235-1.2-0.479
C126.975,178.545,128.271,178.863,128.46,178.312z M22.861,179.032c0.064-0.327-0.149-0.938-0.24-0.479
C22.555,178.879,22.769,179.489,22.861,179.032z M130.861,179.992c0.285-0.714-1.012-1.031-1.2-0.479
C130.4,179.332,129.953,180.34,130.861,179.992z M22.621,181.433c0.038-0.166-0.084-1.396-0.24-0.721
C22.169,181.626,22.509,181.914,22.621,181.433z M138.3,182.872c-0.282-0.034-0.544-0.428-0.72,0
C137.863,182.906,138.125,183.299,138.3,182.872z M148.38,188.633c0.229-1.432-0.444-1.955-0.96-2.641
C141.995,184.959,145.773,191.787,148.38,188.633z M32.22,206.633c-0.457-0.104-0.603-0.518-1.2-0.479
C30.852,206.748,32.046,207.168,32.22,206.633z M32.94,208.072c0.286-0.714-1.011-1.031-1.199-0.48
C32.298,207.697,32.383,208.651,32.94,208.072z M35.821,214.553c0.402,1.761,1.146,5.343,2.64,5.039
c0.277-3.799-2.039-7.08-2.4-11.039c-0.57-0.086-1.38-1.157-1.92-0.479C35.256,209.742,35.267,212.132,35.821,214.553z
M33.18,209.512c-0.148,1.339,1.039,2.537,0.24,4.08c0.282,0.357,0.603,0.679,0.96,0.961c-1.081,0.765,1.131,2.709,0,3.601
c0.576,0.223,0.636,0.965,1.68,0.72c-0.979-3.261-1.55-6.931-2.64-10.08c-0.85-0.21-0.59-0.03-1.44-0.239
C31.747,209.506,33.322,208.649,33.18,209.512z M39.18,210.231c0.174-0.733-0.414-0.705-0.24-1.439c-0.399,0-0.8,0-1.199,0
C38.161,209.332,38.641,209.812,39.18,210.231z M38.22,211.433c0.68,0.041,1,0.439,1.681,0.479
C39.902,210.8,38.848,210.937,38.22,211.433z M38.701,213.112c1.781-0.084-0.676,0.596,0.239,0.96
c0.066-0.566,0.895,0.021,0.961,0.239c0.459,1.181-0.798,0.644-0.721,1.439c0.596-0.035,1.165-0.045,1.2,0.48
c0.206,0.766-0.378,0.742-0.479,1.199c0.728-0.089,0.498,0.781,0.72,1.2c-0.617,1.144-1.351,2.169-3.12,2.159
c-0.871-0.169-0.853-1.229-1.68-1.439c-0.346,0.135-0.586,0.375-0.721,0.721c1.11-0.18,0.98,0.271,0.721,0.96
c2.038,0.269,3.538,1.712,5.52,0.72c1.29-3.207-0.678-6.458-1.439-9.119C39.305,212.597,38.736,212.588,38.701,213.112z"/>
</svg>

After

Width:  |  Height:  |  Size: 30 KiB

+1
View File
@@ -0,0 +1 @@
_build
+153
View File
@@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Security.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Security.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Security"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Security"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

+17
View File
@@ -0,0 +1,17 @@
<h3>About</h3>
<p>
Flask-Security is an opinionated Flask extension which adds basic
security and authentication features to your Flask apps quickly
and easily. Flask-Social can also be used to add "social" or OAuth
login and connection management.
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
<li><a href="http://github.com/jfinkels/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>
+3
View File
@@ -0,0 +1,3 @@
<p class="logo"><a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/logo-helmet.png', 1) }}" alt="Logo"/>
</a></p>
Submodule
+1
Submodule docs/_themes added at 0269f3d188
+107
View File
@@ -0,0 +1,107 @@
API
===
Core
----
.. autoclass:: flask_security.core.Security
:members:
.. data:: flask_security.core.current_user
A proxy for the current user.
Protecting Views
----------------
.. autofunction:: flask_security.decorators.login_required
.. autofunction:: flask_security.decorators.roles_required
.. autofunction:: flask_security.decorators.roles_accepted
.. autofunction:: flask_security.decorators.http_auth_required
.. autofunction:: flask_security.decorators.auth_token_required
User Object Helpers
-------------------
.. autoclass:: flask_security.core.UserMixin
:members:
.. autoclass:: flask_security.core.RoleMixin
:members:
.. autoclass:: flask_security.core.AnonymousUser
:members:
Datastores
----------
.. autoclass:: flask_security.datastore.UserDatastore
:members:
.. autoclass:: flask_security.datastore.SQLAlchemyUserDatastore
:members:
:inherited-members:
.. autoclass:: flask_security.datastore.MongoEngineUserDatastore
:members:
:inherited-members:
.. autoclass:: flask_security.datastore.PeeweeUserDatastore
:members:
:inherited-members:
Signals
-------
See the `Flask documentation on signals`_ for information on how to use these
signals in your code.
See the documentation for the signals provided by the Flask-Login and
Flask-Principal extensions. In addition to those signals, Flask-Security
sends the following signals.
.. data:: user_registered
Sent when a user registers on the site. It is passed a dict with
the `user` and `confirm_token`, the user being logged in and the
(if so configured) the confirmation token issued.
.. data:: user_confirmed
Sent when a user is confirmed. It is passed `user`, which is the
user being confirmed.
.. data:: confirm_instructions_sent
Sent when a user requests confirmation instructions. It is passed
the `user`.
.. data:: login_instructions_sent
Sent when passwordless login is used and user logs in. It is passed
a dict with the `user` and `login_token`, the user being logged in
and the (if so configured) the login token issued.
.. data:: password_reset
Sent when a user completes a password reset. It is passed the
`user`.
.. data:: password_changed
Sent when a user completes a password change. It is passed the
`user`.
.. data:: reset_password_instructions_sent
Sent when a user requests a password reset. It is passed a dict
with the `user` and `token`, the user being logged in and
the (if so configured) the reset token issued.
All signals are also passed a `app` keyword argument, which is the
current application.
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
+1
View File
@@ -0,0 +1 @@
.. include:: ../CHANGES
+316
View File
@@ -0,0 +1,316 @@
# -*- coding: utf-8 -*-
#
# Flask-Security documentation build configuration file, created by
# sphinx-quickstart on Mon Mar 12 15:35:21 2012.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..'))
sys.path.append(os.path.abspath('_themes'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Flask-Security'
copyright = u'2012, Matt Wright'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.6.2'
# The full version, including alpha/beta/rc tags.
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'flask'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
#'github_fork': 'mattupstate/flask-security',
#'index_logo': False
'touch_icon': 'touch-icon.png',
'index_logo': 'logo-full.png'
}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['_themes']
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
'**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
'sourcelink.html', 'searchbox.html']
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Flask-Securitydoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'Flask-Security.tex', u'Flask-Security Documentation',
u'Matt Wright', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'flask-security', u'Flask-Security Documentation',
[u'Matt Wright'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Flask-Security', u'Flask-Security Documentation',
u'Matt Wright', 'Flask-Security', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# -- Options for Epub output ---------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = u'Flask-Security'
epub_author = u'Matt Wright'
epub_publisher = u'Matt Wright'
epub_copyright = u'2012, Matt Wright'
# The language of the text. It defaults to the language option
# or en if the language is not set.
#epub_language = ''
# The scheme of the identifier. Typical schemes are ISBN or URL.
#epub_scheme = ''
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#epub_identifier = ''
# A unique identification for the text.
#epub_uid = ''
# A tuple containing the cover image and cover page html template filenames.
#epub_cover = ()
# HTML files that should be inserted before the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#epub_pre_files = []
# HTML files shat should be inserted after the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#epub_post_files = []
# A list of files that should not be packed into the epub file.
#epub_exclude_files = []
# The depth of the table of contents in toc.ncx.
#epub_tocdepth = 3
# Allow duplicate toc entries.
#epub_tocdup = True
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}
pygments_style = 'flask_theme_support.FlaskyStyle'
# fall back if theme is not there
try:
__import__('flask_theme_support')
except ImportError, e:
print '-' * 74
print 'Warning: Flask themes unavailable. Building with default theme'
print 'If you want the Flask themes, run this command and build again:'
print
print ' git submodule update --init'
print '-' * 74
pygments_style = 'tango'
html_theme = 'default'
html_theme_options = {}
+223
View File
@@ -0,0 +1,223 @@
Configuration
=============
The following configuration values are used by Flask-Security:
Core
--------------
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
======================================== =======================================
``SECURITY_BLUEPRINT_NAME`` Specifies the name for the
Flask-Security blueprint. Defaults to
``security``.
``SECURITY_URL_PREFIX`` Specifies the URL prefix for the
Flask-Security blueprint. Defaults to
``None``.
``SECURITY_FLASH_MESSAGES`` Specifies wether or not to flash
messages during security procedures.
Defaults to ``True``.
``SECURITY_PASSWORD_HASH`` Specifies the password hash algorith to
use when encrypting and decrypting
passwords. Recommended values for
production systems are ``bcrypt``,
``sha512_crypt``, or ``pbkdf2_sha512``.
Defaults to ``plaintext``.
``SECURITY_PASSWORD_SALT`` Specifies the HMAC salt. This is only
used if the password hash type is set
to something other than plain text.
Defaults to ``None``.
``SECURITY_EMAIL_SENDER`` Specifies the email address to send
emails as. Defaults to
``no-reply@localhost``.
``SECURITY_TOKEN_AUTHENTICATION_KEY`` Specifies the query sting parameter to
read when using token authentication.
Defaults to ``auth_token``.
``SECURITY_TOKEN_AUTHENTICATION_HEADER`` Specifies the HTTP header to read when
using token authentication. Defaults to
``Authentication-Token``.
``SECURITY_DEFAULT_HTTP_AUTH_REALM`` Specifies the default authentication
realm when using basic HTTP auth.
Defaults to ``Login Required``
======================================== =======================================
URLs and Views
--------------
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
=============================== ================================================
``SECURITY_LOGIN_URL`` Specifies the login URL. Defaults to ``/login``.
``SECURITY_LOGOUT_URL`` Specifies the logout URL. Defaults to
``/logout``.
``SECURITY_REGISTER_URL`` Specifies the register URL. Defaults to
``/register``.
``SECURITY_RESET_URL`` Specifies the password reset URL. Defaults to
``/reset``.
``SECURITY_CONFIRM_URL`` Specifies the email confirmation URL. Defaults
to ``/confirm``.
``SECURITY_POST_LOGIN_VIEW`` Specifies the default view to redirect to after
a user logs in. This value can be set to a URL
or an endpoint name. Defaults to ``/``.
``SECURITY_POST_LOGOUT_VIEW`` Specifies the default view to redirect to after
a user logs out. This value can be set to a URL
or an endpoint name. Defaults to ``/``.
``SECURITY_CONFIRM_ERROR_VIEW`` Specifies the view to redirect to if a
confirmation error occurs. This value can be set
to a URL or an endpoint name. If this value is
``None`` the user is presented the default view
to resend a confirmation link. Defaults to
``None``.
``SECURITY_POST_REGISTER_VIEW`` Specifies the view to redirect to after a user
successfully registers. This value can be set to
a URL or an endpoint name. If this value is
``None`` the user is redirected to the value of
``SECURITY_POST_LOGIN_VIEW``. Defaults to
``None``.
``SECURITY_POST_CONFIRM_VIEW`` Specifies the view to redirect to after a user
successfully confirms their email. This value
can be set to a URL or an endpoint name. If this
value is ``None`` the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``.
``SECURITY_POST_RESET_VIEW`` Specifies the view to redirect to after a user
successfully resets their password. This value
can be set to a URL or an endpoint name. If this
value is ``None`` the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults to
``None``.
``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user
attempts to access a URL/endpoint that they do
not have permission to access. If this value is
``None`` the user is presented with a default
HTTP 403 response. Defaults to ``None``.
=============================== ================================================
Template Paths
--------------
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
======================================== =======================================
``SECURITY_FORGOT_PASSWORD_TEMPLATE`` Specifies the path to the template for
the forgot password page. Defaults to
``security/forgot_password.html``.
``SECURITY_LOGIN_USER_TEMPLATE`` Specifies the path to the template for
the user login page. Defaults to
``security/login_user.html``.
``SECURITY_REGISTER_USER_TEMPLATE`` Specifies the path to the template for
the user registration page. Defaults to
``security/register_user.html``.
``SECURITY_RESET_PASSWORD_TEMPLATE`` Specifies the path to the template for
the reset password page. Defaults to
``security/reset_password.html``.
``SECURITY_SEND_CONFIRMATION_TEMPLATE`` Specifies the path to the template for
the resend confirmation instructions
page. Defaults to
``security/send_confirmation.html``.
``SECURITY_SEND_LOGIN_TEMPLATE`` Specifies the path to the template for
the send login instructions page for
passwordless logins. Defaults to
``security/send_login.html``.
======================================== =======================================
Feature Flags
-------------
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
========================= ======================================================
``SECURITY_CONFIRMABLE`` Specifies if users are required to confirm their email
address when registering a new account. If this value
is `True` Flask-Security creates an endpoint to handle
confirmations and requests to resend confirmation
instructions. The URL for this endpoint is specified
by the ``SECURITY_CONFIRM_URL`` configuration option.
Defaults to ``False``.
``SECURITY_REGISTERABLE`` Specifies if Flask-Security should create a user
registration endpoint. The URL for this endpoint is
specified by the ``SECURITY_REGISTER_URL``
configuration option. Defaults to ``False``.
``SECURITY_RECOVERABLE`` Specifies if Flask-Security should create a password
reset/recover endpoint. The URL for this endpoint is
specified by the ``SECURITY_RESET_URL`` configuration
option. Defaults to ``False``.
``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user
login statistics. If set to ``True`` ensure your
models have the required fields/attribues. Defaults to
``False``
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the
passwordless login feature. If set to ``True`` users
are not required to enter a password to login but are
sent an email with a login link. This feature is
experimental and should be used with caution. Defaults
to ``False``.
========================= ======================================================
Email
----------
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
=========================================== ====================================
``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the
confirmation email. Defaults to
``Welcome``
``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the
passwordless feature. Defaults to
``Login instructions``
``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password
notice. Defaults to
``Your password has been reset``
``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the password
reset. Defaults to
``Password reset instructions``
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
confirmation message. Defaults to
``Please confirm your email``
=========================================== ====================================
Miscellaneous
-------------
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
======================================= ========================================
``SECURITY_SEND_REGISTER_EMAIL`` Specifies whether registration email is
sent. Defaults to ``True``.
``SECURITY_CONFIRM_EMAIL_WITHIN`` Specifies the amount of time a user has
before their confirmation link expires.
Always pluralized the time unit for this
value. Defaults to ``5 days``.
``SECURITY_RESET_PASSWORD_WITHIN`` Specifies the amount of time a user has
before their password reset link
expires. Always pluralized the time unit
for this value. Defaults to ``5 days``.
``SECURITY_LOGIN_WITHIN`` Specifies the amount of time a user has
before a login link expires. This is
only used when the passwordless login
feature is enabled. Always pluralized
the time unit for this value. Defaults
to ``1 days``.
``SECURITY_LOGIN_WITHOUT_CONFIRMATION`` Specifies if a user may login before
confirming their email when the value
of ``SECURITY_CONFIRMABLE`` is set to
``True``. Defaults to ``False``.
``SECURITY_CONFIRM_SALT`` Specifies the salt value when generating
confirmation links/tokens. Defaults to
``confirm-salt``.
``SECURITY_RESET_SALT`` Specifies the salt value when generating
password reset links/tokens. Defaults to
``reset-salt``.
``SECURITY_LOGIN_SALT`` Specifies the salt value when generating
login links/tokens. Defaults to
``login-salt``.
``SECURITY_REMEMBER_SALT`` Specifies the salt value when generating
remember tokens. Remember tokens are
used instead of user ID's as it is more
secure. Defaults to ``remember-salt``.
======================================= ========================================
+13
View File
@@ -0,0 +1,13 @@
Contents
--------
.. toctree::
:maxdepth: 1
features
configuration
quickstart
models
customizing
api
changelog
+142
View File
@@ -0,0 +1,142 @@
Customizing Views
=================
Flask-Security bootstraps your application with various views for handling its
configured features to get you up and running as quick as possible. However,
you'll probably want to change the way these views look to be more in line with
your application's visual design.
Views
-----
Flask-Security is packaged with a default template for each view it presents to
a user. Templates are located within a subfolder named ``security``. The
following is a list of view templates:
* `security/forgot_password.html`
* `security/login_user.html`
* `security/register_user.html`
* `security/reset_password.html`
* `security/change_password.html`
* `security/send_confirmation.html`
* `security/send_login.html`
Overriding these templates is simple:
1. Create a folder named ``security`` within your application's templates folder
2. Create a template with the same name for the template you wish to override
You can also specify custom template file paths in the :doc:`configuration <configuration>`.
Each template is passed a template context object that includes the following,
including the objects/values that are passed to the template by the main
Flask application context processor:
* ``<template_name>_form``: A form object for the view
* ``security``: The Flask-Security extension object
To add more values to the template context you can specify a context processor
for all views or a specific view. For example::
security = Security(app, user_datastore)
# This processor is added to all templates
@security.context_processor
def security_context_processor():
return dict(hello="world")
# This processor is added to only the register view
@security.register_context_processor
def security_register_processor():
return dict(something="else")
The following is a list of all the available context processor decorators:
* ``context_processor``: All views
* ``forgot_password_context_processor``: Forgot password view
* ``login_context_processor``: Login view
* ``register_context_processor``: Register view
* ``reset_password_context_processor``: Reset password view
* ``change_password_context_processor``: Reset password view
* ``send_confirmation_context_processor``: Send confirmation view
* ``send_login_context_processor``: Send login view
Forms
-----
All forms can be overridden. For each form used, you can specify a
replacement class. This allows you to add extra fields to the
register form or override validators::
from flask_security.forms import RegisterForm
class ExtendedRegisterForm(RegisterForm):
first_name = TextField('First Name', [Required()])
last_name = TextField('Last Name', [Required()])
security = Security(app, user_datastore,
register_form=ExtendedRegisterForm)
For the ``register_form`` and ``confirm_register_form``, each field is
passed to the user model (as kwargs) when a user is created. In the
above case, the ``first_name`` and ``last_name`` fields are passed
directly to the model, so the model should look like::
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
first_name = db.Column(db.String(255))
last_name = db.Column(db.String(255))
The following is a list of all the available form overrides:
* ``login_form``: Login form
* ``confirm_register_form``: Confirmable register form
* ``register_form``: Register form
* ``forgot_password_form``: Forgot password form
* ``reset_password_form``: Reset password form
* ``change_password_form``: Reset password form
* ``send_confirmation_form``: Send confirmation form
* ``passwordless_login_form``: Passwordless login form
Emails
------
Flask-Security is also packaged with a default tempalte for each email that it
may send. Templates are located within the subfolder named ``security/mail``.
The following is a list of email templates:
* `security/mail/confirmation_instructions.html`
* `security/mail/confirmation_instructions.txt`
* `security/mail/login_instructions.html`
* `security/mail/login_instructions.txt`
* `security/mail/reset_instructions.html`
* `security/mail/reset_instructions.txt`
* `security/mail/reset_notice.html`
* `security/mail/change_notice.txt`
* `security/mail/change_notice.html`
* `security/mail/reset_notice.txt`
* `security/mail/welcome.html`
* `security/mail/welcome.txt`
Overriding these templates is simple:
1. Create a folder named ``security`` within your application's templates folder
2. Create a folder named ``email`` within the ``security`` folder
3. Create a template with the same name for the template you wish to override
Each template is passed a template context object that includes values for any
links that are required in the email. If you require more values in the
templates you can specify an email context processor with the
``email_context_processor`` decorator. For example::
security = Security(app, user_datastore)
# This processor is added to all emails
@security.email_context_processor
def security_mail_processor():
return dict(hello="world")
+112
View File
@@ -0,0 +1,112 @@
Features
========
Flask-Security allows you to quickly add common security mechanisms to your
Flask application. They include:
Session Based Authentication
----------------------------
Session based authentication is fulfilled entirely by the `Flask-Login`_
extension. Flask-Security handles the configuration of Flask-Login automatically
based on a few of its own configuration values and uses Flask-Login's
`alternative token`_ feature for remembering users when their session has
expired.
Role/Identity Based Access
--------------------------
Flask-Security implements very basic role management out of the box. This means
that you can associate a high level role or multiple roles to any user. For
instance, you may assign roles such as `Admin`, `Editor`, `SuperUser`, or a
combination of said roles to a user. Access control is based on the role name
and all roles should be uniquely named. This feature is implemented using the
`Flask-Principal`_ extension. If you'd like to implement more granular access
control you can refer to the Flask-Princpal `documentation on this topic`_.
Password Encryption
-------------------
Password encryption is enabled with `passlib`_. Passwords are stored in plain
text by default but you can easily configure the encryption algorithm. You
should **always use an encryption algorithm** in your production environment.
You may also specify to use HMAC with a configured salt value in addition to the
algorithm chosen. Bear in mind passlib does not assume which algorithm you will choose and may require additional libraries to be installed.
Basic HTTP Authentication
-------------------------
Basic HTTP authentication is achievable using a simple view method decorator.
This feature expects the incoming authentication information to identify a user
in the system. This means that the username must be equal to their email address.
Token Authentication
--------------------
Token based authentication is enabled by retrieving the user auth token by
performing an HTTP POST with the authentication details as JSON data against the
authentication endpoint. A successful call to this endpoint will return the
user's ID and their authentication token. This token can be used in subsequent
requests to protected resources. The auth token is supplied in the request
through an HTTP header or query string parameter. By default the HTTP header
name is `X-Auth-Token` and the default query string parameter name is
`auth_token`. Authentication tokens are generated using the user's password.
Thus if the user changes his or her password their existing authentication token
will become invalid. A new token will need to be retrieved using the user's new
password.
Email Confirmation
------------------
If desired you can require that new users confirm their email address.
Flask-Security will send an email message to any new users with an confirmation
link. Upon navigating to the confirmation link, the user will be automatically
logged in. There is also view for resending a confirmation link to a given email
if the user happens to try to use an expired token or has lost the previous
email. Confirmation links can be configured to expire after a specified amount
of time.
Password Reset/Recovery
-----------------------
Password reset and recovery is available for when a user forgets his or her
password. Flask-Security sends an email to the user with a link to a view which
they can reset their password. Once the password is reset they are automatically
logged in and can use the new password from then on. Password reset links can
be configured to expire after a specified amount of time.
User Registration
-----------------
Flask-Security comes packaged with a basic user registration view. This view is
very simple and new users need only supply an email address and their password.
This view can be overrided if your registration process requires more fields.
Login Tracking
--------------
Flask-Security can, if configured, keep track of basic login events and
statistics. They include:
* Last login date
* Current login date
* Last login IP address
* Current login IP address
* Total login count
.. _Flask-Login: http://packages.python.org/Flask-Login/
.. _alternative token: http://packages.python.org/Flask-Login/#alternative-tokens
.. _Flask-Principal: http://packages.python.org/Flask-Principal/
.. _documentation on this topic: http://packages.python.org/Flask-Principal/#granular-resource-protection
.. _passlib: http://packages.python.org/passlib/
+37
View File
@@ -0,0 +1,37 @@
Flask-Security
==============
Flask-Security allows you to quickly add common security mechanisms to your
Flask application. They include:
1. Session based authentication
2. Role management
3. Password encryption
4. Basic HTTP authentication
5. Token based authentication
6. Token based account activation (optional)
7. Token based password recovery / resetting (optional)
8. User registration (optional)
9. Login tracking (optional)
Many of these features are made possible by integrating various Flask extensions
and libraries. They include:
1. `Flask-Login <http://packages.python.org/Flask-Login/>`_
2. `Flask-Mail <http://packages.python.org/Flask-Mail/>`_
3. `Flask-Principal <http://packages.python.org/Flask-Principal/>`_
4. `Flask-Script <http://packages.python.org/Flask-Script/>`_
5. `Flask-WTF <http://packages.python.org/Flask-WTF/>`_
6. `itsdangerous <http://packages.python.org/itsdangerous/>`_
7. `passlib <http://packages.python.org/passlib/>`_
Additionally, it assumes you'll be using a common library for your database
connections and model definitions. Flask-Security supports the following Flask
extensions out of the box for data persistence:
1. `Flask-SQLAlchemy <http://pypi.python.org/pypi/flask-sqlalchemy/>`_
2. `Flask-MongoEngine <http://pypi.python.org/pypi/flask-mongoengine/>`_
3. `Flask-Peewee <http://pypi.python.org/pypi/flask-peewee/>`_
.. include:: contents.rst.inc
+51
View File
@@ -0,0 +1,51 @@
Models
======
Flask-Security assumes you'll be using libraries such as SQLAlchemy,
MongoEngine or Peewee to define a data model that includes a `User` and
`Role` model. The fields on your models must follow a particular convention
depending on the functionality your app requires. Aside from this, you're
free to add any additional fields to your model(s) if you want. At the bear
minimum your `User` and `Role` model should include the following fields:
**User**
* ``id``
* ``email``
* ``password``
* ``active``
**Role**
* ``id``
* ``name``
* ``description``
Additional Functionality
------------------------
Depending on the application's configuration, additional fields may need to be
added to your `User` model.
Confirmable
^^^^^^^^^^^
If you enable account confirmation by setting your application's
`SECURITY_CONFIRMABLE` configuration value to `True` your `User` model will
require the following additional field:
* ``confirmed_at``
Trackable
^^^^^^^^^
If you enable user tracking by setting your application's `SECURITY_TRACKABLE`
configuration value to `True` your `User` model will require the following
additional fields:
* ``last_login_at``
* ``current_login_at``
* ``last_login_ip``
* ``current_login_ip``
* ``login_count``
+252
View File
@@ -0,0 +1,252 @@
Quick Start
===========
- `Basic SQLAlchemy Application <#basic-sqlalchemy-application>`_
- `Basic MongoEngine Application <#basic-mongoengine-application>`_
- `Basic Peewee Application <#basic-peewee-application>`_
- `Mail Configuration <#mail-configuration>`_
Basic SQLAlchemy Application
=============================
SQLAlchemy Install requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
$ mkvirtualenv <your-app-name>
$ pip install flask-security flask-sqlalchemy
SQLAlchemy Application
~~~~~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as
possible using SQLAlchemy:
::
from flask import Flask, render_template
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, SQLAlchemyUserDatastore, \
UserMixin, RoleMixin, login_required
# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
# Create database connection object
db = SQLAlchemy(app)
# Define models
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)
# Create a user to test with
@app.before_first_request
def create_user():
db.create_all()
user_datastore.create_user(email='matt@nobien.net', password='password')
db.session.commit()
# Views
@app.route('/')
@login_required
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run()
Basic MongoEngine Application
==============================
MongoEngine Install requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
$ mkvirtualenv <your-app-name>
$ pip install flask-security flask-mongoengine
MongoEngine Application
~~~~~~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as
possible using MongoEngine:
::
from flask import Flask, render_template
from flask.ext.mongoengine import MongoEngine
from flask.ext.security import Security, MongoEngineUserDatastore, \
UserMixin, RoleMixin, login_required
# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
# MongoDB Config
app.config['MONGODB_DB'] = 'mydatabase'
app.config['MONGODB_HOST'] = 'localhost'
app.config['MONGODB_PORT'] = 27017
# Create database connection object
db = MongoEngine(app)
class Role(db.Document, RoleMixin):
name = db.StringField(max_length=80, unique=True)
description = db.StringField(max_length=255)
class User(db.Document, UserMixin):
email = db.StringField(max_length=255)
password = db.StringField(max_length=255)
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
# Setup Flask-Security
user_datastore = MongoEngineUserDatastore(db, User, Role)
security = Security(app, user_datastore)
# Create a user to test with
@app.before_first_request
def create_user():
user_datastore.create_user(email='matt@nobien.net', password='password')
# Views
@app.route('/')
@login_required
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run()
Basic Peewee Application
========================
Peewee Install requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
$ mkvirtualenv <your-app-name>
$ pip install flask-security flask-peewee
Peewee Application
~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as
possible using Peewee:
::
from flask import Flask, render_template
from flask_peewee.db import Database
from peewee import *
from flask.ext.security import Security, PeeweeUserDatastore, \
UserMixin, RoleMixin, login_required
# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['DATABASE'] = {
'name': 'example.db',
'engine': 'peewee.SqliteDatabase',
}
# Create database connection object
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
description = TextField(null=True)
class User(db.Model, UserMixin):
email = TextField()
password = TextField()
active = BooleanField(default=True)
confirmed_at = DateTimeField(null=True)
class UserRoles(db.Model):
# Because peewee does not come with built-in many-to-many
# relationships, we need this intermediary class to link
# user to roles.
user = ForeignKeyField(User, related_name='roles')
role = ForeignKeyField(Role, related_name='users')
name = property(lambda self: self.role.name)
description = property(lambda self: self.role.description)
# Setup Flask-Security
user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles)
security = Security(app, user_datastore)
# Create a user to test with
@app.before_first_request
def create_user():
for Model in (Role, User, UserRoles):
Model.drop_table(fail_silently=True)
Model.create_table(fail_silently=True)
user_datastore.create_user(email='matt@nobien.net', password='password')
# Views
@app.route('/')
@login_required
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run()
Mail Configuration
===================
Flask-Security integrates with Flask-Mail to handle all email
communications between user and site, so it's important to configure
Flask-Mail with your email server details so Flask-Security can talk
with Flask-Mail correctly.
The following code illustrates a basic setup, which could be added to
the basic application code in the previous section::
# At top of file
from flask_mail import Mail
# After 'Create app'
app.config['MAIL_SERVER'] = 'smtp.example.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = 'username'
app.config['MAIL_PASSWORD'] = 'password'
mail = Mail(app)
To learn more about the various Flask-Mail settings to configure it to
work with your particular email server configuration, please see the
`Flask-Mail documentation <http://packages.python.org/Flask-Mail/>`_.
View File
-118
View File
@@ -1,118 +0,0 @@
# a little trick so you can run:
# $ python example/app.py
# from the root of the security project
import sys, os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask import Flask, render_template
from flask.ext.mongoengine import MongoEngine
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import (Security, LoginForm, user_datastore,
login_required, roles_required, roles_accepted)
from flask.ext.security.datastore.sqlalchemy import SQLAlchemyUserDatastore
from flask.ext.security.datastore.mongoengine import MongoEngineUserDatastore
def create_roles():
for role in ('admin', 'editor', 'author'):
user_datastore.create_role(name=role)
def create_users():
for u in (('matt','matt@lp.com','password',['admin'],True),
('joe','joe@lp.com','password',['editor'],True),
('jill','jill@lp.com','password',['author'],True),
('tiya','tiya@lp.com','password',[],False)):
user_datastore.create_user(username=u[0], email=u[1], password=u[2],
roles=u[3], active=u[4])
def populate_data():
create_roles()
create_users()
def create_app(auth_config):
app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'secret'
if auth_config:
for key, value in auth_config.items():
app.config[key] = value
@app.route('/')
def index():
return render_template('index.html', content='Home Page')
@app.route('/login')
def login():
return render_template('login.html', content='Login Page', form=LoginForm())
@app.route('/custom_login')
def custom_login():
return render_template('login.html', content='Custom Login Page', form=LoginForm())
@app.route('/profile')
@login_required
def profile():
return render_template('index.html', content='Profile Page')
@app.route('/post_login')
@login_required
def post_login():
return render_template('index.html', content='Post Login')
@app.route('/post_logout')
def post_logout():
return render_template('index.html', content='Post Logout')
@app.route('/admin')
@roles_required('admin')
def admin():
return render_template('index.html', content='Admin Page')
@app.route('/admin_or_editor')
@roles_accepted('admin', 'editor')
def admin_or_editor():
return render_template('index.html', content='Admin or Editor Page')
return app
def create_sqlalchemy_app(auth_config=None):
app = create_app(auth_config)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/flask_security_example.sqlite'
db = SQLAlchemy(app)
Security(app, SQLAlchemyUserDatastore(db))
@app.before_first_request
def before_first_request():
db.drop_all()
db.create_all()
populate_data()
return app
def create_mongoengine_app(auth_config=None):
app = create_app(auth_config)
app.config['MONGODB_DB'] = 'flask_security_example'
app.config['MONGODB_HOST'] = 'localhost'
app.config['MONGODB_PORT'] = 27017
db = MongoEngine(app)
Security(app, MongoEngineUserDatastore(db))
@app.before_first_request
def before_first_request():
from flask.ext.security import User, Role
User.drop_collection()
Role.drop_collection()
populate_data()
return app
if __name__ == '__main__':
app = create_sqlalchemy_app()
#app = create_mongoengine_app()
app.run()
-21
View File
@@ -1,21 +0,0 @@
# a little trick so you can run:
# $ python example/app.py
# from the root of the security project
import sys, os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from example import app
from flask.ext.script import Manager
from flask.ext.security.script import (CreateUserCommand , AddRoleCommand,
RemoveRoleCommand, ActivateUserCommand, DeactivateUserCommand)
manager = Manager(app.create_sqlalchemy_app())
manager.add_command('create_user', CreateUserCommand())
manager.add_command('add_role', AddRoleCommand())
manager.add_command('remove_role', RemoveRoleCommand())
manager.add_command('deactivate_user', DeactivateUserCommand())
manager.add_command('activate_user', ActivateUserCommand())
if __name__ == "__main__":
manager.run()
-10
View File
@@ -1,10 +0,0 @@
{% include "_messages.html" %}
{% include "_nav.html" %}
<form action="{{ url_for('auth.authenticate') }}" method="POST" name="login_form">
{{ form.hidden_tag() }}
{{ form.username.label }} {{ form.username }}<br/>
{{ form.password.label }} {{ form.password }}<br/>
{{ form.remember.label }} {{ form.remember }}<br/>
{{ form.submit }}
</form>
<p>{{ content }}</p>
+13 -386
View File
@@ -1,396 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security flask.ext.security
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
Flask-Security is a Flask extension module that aims to add quick and Flask-Security is a Flask extension that aims to add quick and simple
simple security via Flask-Login and Flask-Principal. security via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
:copyright: (c) 2012 by Matt Wright. :copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
from __future__ import absolute_import
import sys __version__ = '1.6.2'
from datetime import datetime from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from types import StringType from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
from .decorators import auth_token_required, http_auth_required, \
from flask import (current_app, Blueprint, flash, redirect, request, login_required, roles_accepted, roles_required, auth_required
session, _request_ctx_stack, url_for, abort, g) from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
from flask.ext.login import (AnonymousUser as AnonymousUserBase, from .signals import confirm_instructions_sent, password_reset, \
UserMixin as BaseUserMixin, LoginManager, login_required, login_user, reset_password_instructions_sent, user_confirmed, user_registered
logout_user, current_user, user_logged_in, user_logged_out) from .utils import login_user, logout_user, url_for_security
from flask.ext.principal import (Identity, Principal, RoleNeed, UserNeed,
Permission, AnonymousIdentity, identity_changed, identity_loaded)
from flask.ext.wtf import (Form, TextField, PasswordField, SubmitField,
HiddenField, Required, ValidationError, BooleanField, Email)
from functools import wraps
from passlib.context import CryptContext
from werkzeug.utils import import_string
from werkzeug.local import LocalProxy
User, Role = None, None
URL_PREFIX_KEY = 'SECURITY_URL_PREFIX'
AUTH_PROVIDER_KEY = 'SECURITY_AUTH_PROVIDER'
PASSWORD_HASH_KEY = 'SECURITY_PASSWORD_HASH'
USER_DATASTORE_KEY = 'SECURITY_USER_DATASTORE'
LOGIN_FORM_KEY = 'SECURITY_LOGIN_FORM'
AUTH_URL_KEY = 'SECURITY_AUTH_URL'
LOGOUT_URL_KEY = 'SECURITY_LOGOUT_URL'
LOGIN_VIEW_KEY = 'SECURITY_LOGIN_VIEW'
POST_LOGIN_KEY = 'SECURITY_POST_LOGIN'
POST_LOGOUT_KEY = 'SECURITY_POST_LOGOUT'
DEBUG_LOGIN = 'User %s logged in. Redirecting to: %s'
ERROR_LOGIN = 'Unsuccessful authentication attempt: %s. Redirecting to: %s'
DEBUG_LOGOUT = 'User logged out, redirecting to: %s'
FLASH_INACTIVE = 'Inactive user'
FLASH_PERMISSIONS = 'You do not have permission to view this resource.'
default_config = {
URL_PREFIX_KEY: None,
PASSWORD_HASH_KEY: 'plaintext',
USER_DATASTORE_KEY: 'user_datastore',
AUTH_PROVIDER_KEY: 'flask.ext.security.AuthenticationProvider',
LOGIN_FORM_KEY: 'flask.ext.security.LoginForm',
AUTH_URL_KEY: '/auth',
LOGOUT_URL_KEY: '/logout',
LOGIN_VIEW_KEY: '/login',
POST_LOGIN_KEY: '/',
POST_LOGOUT_KEY: '/',
}
class BadCredentialsError(Exception):
"""Raised when an authentication attempt fails due to an error with the
provided credentials.
"""
class AuthenticationError(Exception):
"""Raised when an authentication attempt fails due to invalid configuration
or an unknown reason.
"""
class UserNotFoundError(Exception):
"""Raised by a user datastore when there is an attempt to find a user by
their identifier, often username or email, and the user is not found.
"""
class RoleNotFoundError(Exception):
"""Raised by a user datastore when there is an attempt to find a role and
the role cannot be found.
"""
class UserIdNotFoundError(Exception):
"""Raised by a user datastore when there is an attempt to find a user by
ID and the user is not found.
"""
class UserDatastoreError(Exception):
"""Raise when a user datastore experiences an unexpected error
"""
class UserCreationError(Exception):
"""Raise when an error occurs when creating a user
"""
class RoleCreationError(Exception):
"""Raise when an error occurs when creating a role
"""
#: App logger for convenience
logger = LocalProxy(lambda: current_app.logger)
#: Authentication provider
auth_provider = LocalProxy(lambda: current_app.auth_provider)
#: Login manager
login_manager = LocalProxy(lambda: current_app.login_manager)
#: Password encyption context
pwd_context = LocalProxy(lambda: current_app.pwd_context)
# User service
user_datastore = LocalProxy(lambda: getattr(current_app,
current_app.config[USER_DATASTORE_KEY]))
def roles_required(*args):
roles = args
perm = Permission(*[RoleNeed(role) for role in roles])
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated():
return redirect(current_app.config[LOGIN_VIEW_KEY])
if perm.can():
return fn(*args, **kwargs)
logger.debug('Identity does not provide all of the '
'following roles: %s' % [r for r in roles])
flash(FLASH_PERMISSIONS, 'error')
return redirect(request.referrer or '/')
return decorated_view
return wrapper
def roles_accepted(*args):
roles = args
perms = [Permission(RoleNeed(role)) for role in roles]
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated():
return redirect(current_app.config[LOGIN_VIEW_KEY])
for perm in perms:
if perm.can():
return fn(*args, **kwargs)
logger.debug('Identity does not provide at least one of '
'the following roles: %s' % [r for r in roles])
flash(FLASH_PERMISSIONS, 'error')
return redirect(request.referrer or '/')
return decorated_view
return wrapper
class RoleMixin(object):
def __eq__(self, other):
return self.name == other.name
def __ne__(self, other):
return self.name != other.name
def __str__(self):
return '<Role name=%s, description=%s>' % (self.name, self.description)
class UserMixin(BaseUserMixin):
def is_active(self):
return self.active
def has_role(self, role):
if not isinstance(role, Role):
role = Role(name=role)
return role in self.roles
def __str__(self):
ctx = (str(self.id), self.username, self.email)
return '<User id=%s, username=%s, email=%s>' % ctx
class AnonymousUser(AnonymousUserBase):
def __init__(self):
super(AnonymousUser, self).__init__()
self.roles = [] # TODO: Make this immutable?
def has_role(self, *args):
return False
class Security(object):
def __init__(self, app=None, datastore=None):
self.init_app(app, datastore)
def init_app(self, app, datastore):
"""Initialize the application
:param app: An instance of an application
:param datastore: An instance of a datastore for your users
"""
if app is None or datastore is None: return
blueprint = Blueprint('auth', __name__)
configured = {}
for key, value in default_config.items():
configured[key] = app.config.get(key, value)
app.config.update(configured)
config = app.config
#config = default_config.copy()
#config.update(app.config.get(AUTH_CONFIG_KEY, {}))
#app.config[AUTH_CONFIG_KEY] = config
# setup the login manager extension
login_manager = LoginManager()
login_manager.anonymous_user = AnonymousUser
login_manager.login_view = config[LOGIN_VIEW_KEY]
login_manager.setup_app(app)
app.login_manager = login_manager
Provider = get_class_from_config(AUTH_PROVIDER_KEY, config)
Form = get_class_from_config(LOGIN_FORM_KEY, config)
pw_hash = config[PASSWORD_HASH_KEY]
app.pwd_context = CryptContext(schemes=[pw_hash], default=pw_hash)
app.auth_provider = Provider(Form)
app.principal = Principal(app)
from flask.ext import security as s
s.User, s.Role = datastore.get_models()
setattr(app, config[USER_DATASTORE_KEY], datastore)
@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
if hasattr(current_user, 'id'):
identity.provides.add(UserNeed(current_user.id))
for role in current_user.roles:
identity.provides.add(RoleNeed(role.name))
identity.user = current_user
@login_manager.user_loader
def load_user(user_id):
try:
return datastore.with_id(user_id)
except Exception, e:
logger.error('Error getting user: %s' % e)
return None
auth_url = config[AUTH_URL_KEY]
@blueprint.route(auth_url, methods=['POST'], endpoint='authenticate')
def authenticate():
try:
form = Form()
user = auth_provider.authenticate(form)
if login_user(user, remember=form.remember.data):
redirect_url = get_post_login_redirect()
identity_changed.send(app, identity=Identity(user.id))
logger.debug(DEBUG_LOGIN % (user, redirect_url))
return redirect(redirect_url)
raise BadCredentialsError(FLASH_INACTIVE)
except BadCredentialsError, e:
message = '%s' % e
flash(message, 'error')
redirect_url = request.referrer or login_manager.login_view
logger.error(ERROR_LOGIN % (message, redirect_url))
return redirect(redirect_url)
@blueprint.route(config[LOGOUT_URL_KEY], endpoint='logout')
@login_required
def logout():
for value in ('identity.name', 'identity.auth_type'):
session.pop(value, None)
identity_changed.send(app, identity=AnonymousIdentity())
logout_user()
redirect_url = find_redirect(POST_LOGOUT_KEY)
logger.debug(DEBUG_LOGOUT % redirect_url)
return redirect(redirect_url)
app.register_blueprint(blueprint, url_prefix=config[URL_PREFIX_KEY])
class LoginForm(Form):
"""Default login form"""
username = TextField("Username or Email",
validators=[Required(message="Username not provided")])
password = PasswordField("Password",
validators=[Required(message="Password not provided")])
remember = BooleanField("Remember Me")
next = HiddenField()
submit = SubmitField("Login")
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
self.next.data = request.args.get('next', None)
class AuthenticationProvider(object):
"""Default authentication provider"""
def __init__(self, login_form_class=None):
self.login_form_class = login_form_class or LoginForm
def login_form(self, formdata=None):
return self.login_form_class(formdata)
def authenticate(self, form):
# first some basic validation
if not form.validate():
if form.username.errors:
raise BadCredentialsError(form.username.errors[0])
if form.password.errors:
raise BadCredentialsError(form.password.errors[0])
return self.do_authenticate(form.username.data, form.password.data)
def do_authenticate(self, user_identifier, password):
try:
user = user_datastore.find_user(user_identifier)
except AttributeError, e:
self.auth_error("Could not find user service: %s" % e)
except UserNotFoundError, e:
raise BadCredentialsError("Specified user does not exist")
except AttributeError, e:
self.auth_error('Invalid user service: %s' % e)
except Exception, e:
self.auth_error('Unexpected authentication error: %s' % e)
# compare passwords
if pwd_context.verify(password, user.password):
return user
# bad match
raise BadCredentialsError("Password does not match")
def auth_error(self, msg):
logger.error(msg)
raise AuthenticationError(msg)
def get_class_by_name(clazz):
parts = clazz.split('.')
module = ".".join(parts[:-1])
m = __import__( module )
for comp in parts[1:]:
m = getattr(m, comp)
return m
def get_class_from_config(key, config):
try:
return get_class_by_name(config[key])
except Exception, e:
raise AttributeError(
"Could not get class '%s' for Auth setting '%s' >> %s" %
(config[key], key, e))
def get_url(value):
# try building the url or assume its a url already
try: return url_for(value)
except: return value
def get_post_login_redirect():
return (get_url(request.args.get('next')) or
get_url(request.form.get('next')) or
find_redirect(POST_LOGIN_KEY))
def find_redirect(key):
# Look in the session first, and if not there go to the config, and
# if its not there either just go to the root url
result = (get_url(session.get(key.lower(), None)) or
get_url(current_app.config[key] or None) or '/')
# Try and delete the session value if it was used
try: del session[key.lower()]
except: pass
return result
+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, request
from werkzeug.local import LocalProxy
from .signals import password_changed
from .utils import send_mail, encrypt_password, url_for_security, \
config_value
# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
def send_password_changed_notice(user):
"""Sends the password changed notice email for the specified user.
:param user: The user to send the notice to
"""
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE'), user.email,
'change_notice', user=user)
def change_user_password(user, password):
"""Change the specified user's password
:param user: The user to change_password
:param password: The unencrypted new password
"""
user.password = encrypt_password(password)
_datastore.put(user)
send_password_changed_notice(user)
password_changed.send(user, app=app._get_current_object())
+83
View File
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.confirmable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security confirmable module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from datetime import datetime
from flask import current_app as app, request
from werkzeug.local import LocalProxy
from .utils import send_mail, md5, url_for_security, get_token_status,\
config_value
from .signals import user_confirmed, confirm_instructions_sent
# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
def generate_confirmation_link(user):
token = generate_confirmation_token(user)
url = url_for_security('confirm_email', token=token)
return request.url_root[:-1] + url, token
def send_confirmation_instructions(user):
"""Sends the confirmation instructions email for the specified user.
:param user: The user to send the instructions to
:param token: The confirmation token
"""
confirmation_link, token = generate_confirmation_link(user)
send_mail(config_value('EMAIL_SUBJECT_CONFIRM'), user.email,
'confirmation_instructions', user=user,
confirmation_link=confirmation_link)
confirm_instructions_sent.send(app._get_current_object(), user=user)
return token
def generate_confirmation_token(user):
"""Generates a unique confirmation token for the specified user.
:param user: The user to work with
"""
data = [str(user.id), md5(user.email)]
return _security.confirm_serializer.dumps(data)
def requires_confirmation(user):
"""Returns `True` if the user requires confirmation."""
return _security.confirmable and user.confirmed_at == None
def confirm_email_token_status(token):
"""Returns the expired status, invalid status, and user of a confirmation
token. For example::
expired, invalid, user = confirm_email_token_status('...')
:param token: The confirmation token
"""
return get_token_status(token, 'confirm', 'CONFIRM_EMAIL')
def confirm_user(user):
"""Confirms the specified user
:param user: The user to confirm
"""
user.confirmed_at = datetime.utcnow()
_datastore.put(user)
user_confirmed.send(app._get_current_object(), user=user)
+381
View File
@@ -0,0 +1,381 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.core
~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security core module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from flask import current_app
from flask.ext.login import AnonymousUser as AnonymousUserBase, \
UserMixin as BaseUserMixin, LoginManager, current_user
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \
identity_loaded
from itsdangerous import URLSafeTimedSerializer
from passlib.context import CryptContext
from werkzeug.datastructures import ImmutableList
from werkzeug.local import LocalProxy
from .utils import config_value as cv, get_config, md5, url_for_security
from .views import create_blueprint
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \
SendConfirmationForm, PasswordlessLoginForm
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
#: Default Flask-Security configuration
_default_config = {
'BLUEPRINT_NAME': 'security',
'URL_PREFIX': None,
'SUBDOMAIN': None,
'FLASH_MESSAGES': True,
'PASSWORD_HASH': 'plaintext',
'PASSWORD_SALT': None,
'LOGIN_URL': '/login',
'LOGOUT_URL': '/logout',
'REGISTER_URL': '/register',
'RESET_URL': '/reset',
'CHANGE_URL': '/change',
'CONFIRM_URL': '/confirm',
'POST_LOGIN_VIEW': '/',
'POST_LOGOUT_VIEW': '/',
'CONFIRM_ERROR_VIEW': None,
'POST_REGISTER_VIEW': None,
'POST_CONFIRM_VIEW': None,
'POST_RESET_VIEW': None,
'POST_CHANGE_VIEW': None,
'UNAUTHORIZED_VIEW': None,
'FORGOT_PASSWORD_TEMPLATE': 'security/forgot_password.html',
'LOGIN_USER_TEMPLATE': 'security/login_user.html',
'REGISTER_USER_TEMPLATE': 'security/register_user.html',
'RESET_PASSWORD_TEMPLATE': 'security/reset_password.html',
'SEND_CONFIRMATION_TEMPLATE': 'security/send_confirmation.html',
'SEND_LOGIN_TEMPLATE': 'security/send_login.html',
'CONFIRMABLE': False,
'REGISTERABLE': False,
'RECOVERABLE': False,
'TRACKABLE': False,
'PASSWORDLESS': False,
'CHANGEABLE': False,
'SEND_REGISTER_EMAIL': True,
'LOGIN_WITHIN': '1 days',
'CONFIRM_EMAIL_WITHIN': '5 days',
'RESET_PASSWORD_WITHIN': '5 days',
'LOGIN_WITHOUT_CONFIRMATION': False,
'EMAIL_SENDER': 'no-reply@localhost',
'TOKEN_AUTHENTICATION_KEY': 'auth_token',
'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token',
'CONFIRM_SALT': 'confirm-salt',
'RESET_SALT': 'reset-salt',
'LOGIN_SALT': 'login-salt',
'CHANGE_SALT': 'change-salt',
'REMEMBER_SALT': 'remember-salt',
'DEFAULT_HTTP_AUTH_REALM': 'Login Required',
'EMAIL_SUBJECT_REGISTER': 'Welcome',
'EMAIL_SUBJECT_CONFIRM': 'Please confirm your email',
'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions',
'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset',
'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed',
'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions'
}
#: Default Flask-Security messages
_default_messages = {
'UNAUTHORIZED': ('You do not have permission to view this resource.', 'error'),
'CONFIRM_REGISTRATION': ('Thank you. Confirmation instructions have been sent to %(email)s.', 'success'),
'EMAIL_CONFIRMED': ('Thank you. Your email has been confirmed.', 'success'),
'ALREADY_CONFIRMED': ('Your email has already been confirmed.', 'info'),
'INVALID_CONFIRMATION_TOKEN': ('Invalid confirmation token.', 'error'),
'EMAIL_ALREADY_ASSOCIATED': ('%(email)s is already associated with an account.', 'error'),
'PASSWORD_MISMATCH': ('Password does not match', 'error'),
'RETYPE_PASSWORD_MISMATCH': ('Passwords do not match', 'error'),
'INVALID_REDIRECT': ('Redirections outside the domain are forbidden', 'error'),
'PASSWORD_RESET_REQUEST': ('Instructions to reset your password have been sent to %(email)s.', 'info'),
'PASSWORD_RESET_EXPIRED': ('You did not reset your password within %(within)s. New instructions have been sent to %(email)s.', 'error'),
'INVALID_RESET_PASSWORD_TOKEN': ('Invalid reset password token.', 'error'),
'CONFIRMATION_REQUIRED': ('Email requires confirmation.', 'error'),
'CONFIRMATION_REQUEST': ('Confirmation instructions have been sent to %(email)s.', 'info'),
'CONFIRMATION_EXPIRED': ('You did not confirm your email within %(within)s. New instructions to confirm your email have been sent to %(email)s.', 'error'),
'LOGIN_EXPIRED': ('You did not login within %(within)s. New instructions to login have been sent to %(email)s.', 'error'),
'LOGIN_EMAIL_SENT': ('Instructions to login have been sent to %(email)s.', 'success'),
'INVALID_LOGIN_TOKEN': ('Invalid login token.', 'error'),
'DISABLED_ACCOUNT': ('Account is disabled.', 'error'),
'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'),
'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'),
'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'),
'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'),
'INVALID_PASSWORD': ('Invalid password', 'error'),
'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'),
'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'),
'PASSWORD_CHANGE': ('You successfully changed your password.', 'success'),
'LOGIN': ('Please log in to access this page.', 'info'),
'REFRESH': ('Please reauthenticate to access this page.', 'info'),
}
_allowed_password_hash_schemes = [
'bcrypt',
'des_crypt',
'pbkdf2_sha256',
'pbkdf2_sha512',
'sha256_crypt',
'sha512_crypt',
# And always last one...
'plaintext'
]
_default_forms = {
'login_form': LoginForm,
'confirm_register_form': ConfirmRegisterForm,
'register_form': RegisterForm,
'forgot_password_form': ForgotPasswordForm,
'reset_password_form': ResetPasswordForm,
'change_password_form': ChangePasswordForm,
'send_confirmation_form': SendConfirmationForm,
'passwordless_login_form': PasswordlessLoginForm,
}
def _user_loader(user_id):
return _security.datastore.find_user(id=user_id)
def _token_loader(token):
try:
data = _security.remember_token_serializer.loads(token)
user = _security.datastore.find_user(id=data[0])
if user and md5(user.password) == data[1]:
return user
except:
pass
return None
def _identity_loader():
if not isinstance(current_user._get_current_object(), AnonymousUser):
identity = Identity(current_user.id)
return identity
def _on_identity_loaded(sender, identity):
if hasattr(current_user, 'id'):
identity.provides.add(UserNeed(current_user.id))
for role in current_user.roles:
identity.provides.add(RoleNeed(role.name))
identity.user = current_user
def _get_login_manager(app):
lm = LoginManager()
lm.anonymous_user = AnonymousUser
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
lm.user_loader(_user_loader)
lm.token_loader(_token_loader)
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)
lm.init_app(app)
return lm
def _get_principal(app):
p = Principal(app, use_sessions=False)
p.identity_loader(_identity_loader)
return p
def _get_pwd_context(app):
pw_hash = cv('PASSWORD_HASH', app=app)
if pw_hash not in _allowed_password_hash_schemes:
allowed = ', '.join(_allowed_password_hash_schemes[:-1]) + ' and ' + _allowed_password_hash_schemes[-1]
raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed))
return CryptContext(schemes=_allowed_password_hash_schemes, default=pw_hash)
def _get_serializer(app, name):
secret_key = app.config.get('SECRET_KEY')
salt = app.config.get('SECURITY_%s_SALT' % name.upper())
return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)
def _get_state(app, datastore, **kwargs):
for key, value in get_config(app).items():
kwargs[key.lower()] = value
kwargs.update(dict(
app=app,
datastore=datastore,
login_manager=_get_login_manager(app),
principal=_get_principal(app),
pwd_context=_get_pwd_context(app),
remember_token_serializer=_get_serializer(app, 'remember'),
login_serializer=_get_serializer(app, 'login'),
reset_serializer=_get_serializer(app, 'reset'),
confirm_serializer=_get_serializer(app, 'confirm'),
_context_processors={},
_send_mail_task=None
))
for key, value in _default_forms.items():
if key not in kwargs or not kwargs[key]:
kwargs[key] = value
return _SecurityState(**kwargs)
def _context_processor():
return dict(url_for_security=url_for_security, security=_security)
class RoleMixin(object):
"""Mixin for `Role` model definitions"""
def __eq__(self, other):
return (self.name == other or \
self.name == getattr(other, 'name', None))
def __ne__(self, other):
return (self.name != other and
self.name != getattr(other, 'name', None))
class UserMixin(BaseUserMixin):
"""Mixin for `User` model definitions"""
def is_active(self):
"""Returns `True` if the user is active."""
return self.active
def get_auth_token(self):
"""Returns the user's authentication token."""
data = [str(self.id), md5(self.password)]
return _security.remember_token_serializer.dumps(data)
def has_role(self, role):
"""Returns `True` if the user identifies with the specified role.
:param role: A role name or `Role` instance"""
return role in self.roles
class AnonymousUser(AnonymousUserBase):
"""AnonymousUser definition"""
def __init__(self):
super(AnonymousUser, self).__init__()
self.roles = ImmutableList()
def has_role(self, *args):
"""Returns `False`"""
return False
class _SecurityState(object):
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key.lower(), value)
def _add_ctx_processor(self, endpoint, fn):
group = self._context_processors.setdefault(endpoint, [])
fn not in group and group.append(fn)
def _run_ctx_processor(self, endpoint):
rv, fns = {}, []
for g in [None, endpoint]:
for fn in self._context_processors.setdefault(g, []):
rv.update(fn())
return rv
def context_processor(self, fn):
self._add_ctx_processor(None, fn)
def forgot_password_context_processor(self, fn):
self._add_ctx_processor('forgot_password', fn)
def login_context_processor(self, fn):
self._add_ctx_processor('login', fn)
def register_context_processor(self, fn):
self._add_ctx_processor('register', fn)
def reset_password_context_processor(self, fn):
self._add_ctx_processor('reset_password', fn)
def change_password_context_processor(self, fn):
self._add_ctx_processor('change_password', fn)
def send_confirmation_context_processor(self, fn):
self._add_ctx_processor('send_confirmation', fn)
def send_login_context_processor(self, fn):
self._add_ctx_processor('send_login', fn)
def mail_context_processor(self, fn):
self._add_ctx_processor('mail', fn)
def send_mail_task(self, fn):
self._send_mail_task = fn
class Security(object):
"""The :class:`Security` class initializes the Flask-Security extension.
:param app: The application.
:param datastore: An instance of a user datastore.
"""
def __init__(self, app=None, datastore=None, **kwargs):
self.app = app
self.datastore = datastore
if app is not None and datastore is not None:
self._state = self.init_app(app, datastore, **kwargs)
def init_app(self, app, datastore=None, register_blueprint=True,
login_form=None, confirm_register_form=None,
register_form=None, forgot_password_form=None,
reset_password_form=None, change_password_form=None,
send_confirmation_form=None, passwordless_login_form=None):
"""Initializes the Flask-Security extension for the specified
application and datastore implentation.
:param app: The application.
:param datastore: An instance of a user datastore.
:param register_blueprint: to register the Security blueprint or not.
"""
datastore = datastore or self.datastore
for key, value in _default_config.items():
app.config.setdefault('SECURITY_' + key, value)
for key, value in _default_messages.items():
app.config.setdefault('SECURITY_MSG_' + key, value)
identity_loaded.connect_via(app)(_on_identity_loaded)
state = _get_state(app, datastore,
login_form=login_form,
confirm_register_form=confirm_register_form,
register_form=register_form,
forgot_password_form=forgot_password_form,
reset_password_form=reset_password_form,
change_password_form=change_password_form,
send_confirmation_form=send_confirmation_form,
passwordless_login_form=passwordless_login_form)
if register_blueprint:
app.register_blueprint(create_blueprint(state, __name__))
app.context_processor(_context_processor)
app.extensions['security'] = state
return state
def __getattr__(self, name):
return getattr(self._state, name, None)
+260
View File
@@ -0,0 +1,260 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.datastore
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains an user datastore classes.
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
class Datastore(object):
def __init__(self, db):
self.db = db
def commit(self):
pass
def put(self, model):
raise NotImplementedError
def delete(self, model):
raise NotImplementedError
class SQLAlchemyDatastore(Datastore):
def commit(self):
self.db.session.commit()
def put(self, model):
self.db.session.add(model)
return model
def delete(self, model):
self.db.session.delete(model)
class MongoEngineDatastore(Datastore):
def put(self, model):
model.save()
return model
def delete(self, model):
model.delete()
class PeeweeDatastore(Datastore):
def put(self, model):
model.save()
return model
def delete(self, model):
model.delete_instance()
class UserDatastore(object):
"""Abstracted user datastore.
:param user_model: A user model class definition
:param role_model: A role model class definition
"""
def __init__(self, user_model, role_model):
self.user_model = user_model
self.role_model = role_model
def _prepare_role_modify_args(self, user, role):
if isinstance(user, basestring):
user = self.find_user(email=user)
if isinstance(role, basestring):
role = self.find_role(role)
return user, role
def _prepare_create_user_args(self, **kwargs):
kwargs.setdefault('active', True)
roles = kwargs.get('roles', [])
for i, role in enumerate(roles):
rn = role.name if isinstance(role, self.role_model) else role
# see if the role exists
roles[i] = self.find_role(rn)
kwargs['roles'] = roles
return kwargs
def find_user(self, *args, **kwargs):
"""Returns a user matching the provided parameters."""
raise NotImplementedError
def find_role(self, *args, **kwargs):
"""Returns a role matching the provided name."""
raise NotImplementedError
def add_role_to_user(self, user, role):
"""Adds a role tp a user
:param user: The user to manipulate
:param role: The role to add to the user
"""
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
def remove_role_from_user(self, user, role):
"""Removes a role from a user
:param user: The user to manipulate
:param role: The role to remove from the user
"""
rv = False
user, role = self._prepare_role_modify_args(user, role)
if role in user.roles:
rv = True
user.roles.remove(role)
return rv
def toggle_active(self, user):
"""Toggles a user's active status. Always returns True."""
user.active = not user.active
return True
def deactivate_user(self, user):
"""Deactivates a specified user. Returns `True` if a change was made.
:param user: The user to deactivate
"""
if user.active:
user.active = False
return True
return False
def activate_user(self, user):
"""Activates a specified user. Returns `True` if a change was made.
:param user: The user to activate
"""
if not user.active:
user.active = True
return True
return False
def create_role(self, **kwargs):
"""Creates and returns a new role from the given parameters."""
role = self.role_model(**kwargs)
return self.put(role)
def find_or_create_role(self, name, **kwargs):
"""Returns a role matching the given name or creates it with any
additionally provided parameters
"""
kwargs["name"] = name
return self.find_role(name) or self.create_role(**kwargs)
def create_user(self, **kwargs):
"""Creates and returns a new user from the given parameters."""
user = self.user_model(**self._prepare_create_user_args(**kwargs))
return self.put(user)
def delete_user(self, user):
"""Delete the specified user
:param user: The user to delete
"""
self.delete(user)
class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
"""A SQLAlchemy datastore implementation for Flask-Security that assumes the
use of the Flask-SQLAlchemy extension.
"""
def __init__(self, db, user_model, role_model):
SQLAlchemyDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
def find_user(self, **kwargs):
return self.user_model.query.filter_by(**kwargs).first()
def find_role(self, role):
return self.role_model.query.filter_by(name=role).first()
class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
"""A MongoEngine datastore implementation for Flask-Security that assumes
the use of the Flask-MongoEngine extension.
"""
def __init__(self, db, user_model, role_model):
MongoEngineDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
def find_user(self, **kwargs):
return self.user_model.objects(**kwargs).first()
def find_role(self, role):
return self.role_model.objects(name=role).first()
class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
"""A PeeweeD datastore implementation for Flask-Security that assumes
the use of the Flask-Peewee extension.
:param user_model: A user model class definition
:param role_model: A role model class definition
:param role_link: A model implementing the many-to-many user-role relation
"""
def __init__(self, db, user_model, role_model, role_link):
PeeweeDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
self.UserRole = role_link
def find_user(self, **kwargs):
try:
return self.user_model.filter(**kwargs).get()
except self.user_model.DoesNotExist:
return None
def find_role(self, role):
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)
return user
def add_role_to_user(self, user, role):
"""Adds a role tp a user
:param user: The user to manipulate
:param role: The role to add to the user
"""
user, role = self._prepare_role_modify_args(user, role)
if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count():
return False
else:
self.UserRole.create(user=user, role=role)
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)
if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count():
self.UserRole.delete().where(self.UserRole.user==user, self.UserRole.role==role)
return True
else:
return False
-131
View File
@@ -1,131 +0,0 @@
from datetime import datetime
from flask.ext import security
from flask.ext.security import UserCreationError, RoleCreationError, pwd_context
class UserDatastore(object):
"""Abstracted user datastore. Always extend this and implement
missing methods"""
def _save_model(self, model, **kwargs):
raise NotImplementedError(
"User datastore does not implement _save_model method")
def _do_with_id(self, id):
raise NotImplementedError(
"User datastore does not implement _do_with_id method")
def _do_find_user(self):
raise NotImplementedError(
"User datastore does not implement _do_find_user method")
def _do_find_role(self):
raise NotImplementedError(
"User datastore does not implement _do_find_role method")
def _do_add_role(self, user, role):
user, role = self._prepare_role_modify_args(user, role)
if role not in user.roles:
user.roles.append(role)
return user
def _do_remove_role(self, user, role):
user, role = self._prepare_role_modify_args(user, role)
if role in user.roles:
user.roles.remove(role)
return user
def _do_toggle_active(self, user, active=None):
user = self.find_user(user)
if active is None:
user.active = not user.active
elif active != user.active:
user.active = active
return user
def _do_deactive_user(self, user):
return self._do_toggle_active(user, False)
def _do_active_user(self, user):
return self._do_toggle_active(user, True)
def _prepare_role_modify_args(self, user, role):
if isinstance(user, security.User):
user = user.username or user.email
if isinstance(role, security.Role):
role = role.name
return self.find_user(user), self.find_role(role)
def _prepare_create_role_args(self, kwargs):
for key in ('name', 'description'):
kwargs[key] = kwargs.get(key, None)
if kwargs['name'] is None:
raise RoleCreationError("Missing name argument")
return kwargs
def _prepare_create_user_args(self, kwargs):
username = kwargs.get('username', None)
email = kwargs.get('email', None)
password = kwargs.get('password', None)
if username is None and email is None:
raise UserCreationError('Missing username and/or email arguments')
if password is None:
raise UserCreationError('Missing password argument')
roles = kwargs.get('roles', [])
for i, role in enumerate(roles):
rn = role.name if isinstance(role, security.Role) else role
# see if the role exists
roles[i] = self.find_role(rn)
kwargs['roles'] = roles
now = datetime.utcnow()
kwargs['created_at'], kwargs['modified_at'] = now, now
pw = kwargs['password']
if not pwd_context.identify(pw):
kwargs['password'] = pwd_context.encrypt(pw)
return kwargs
def with_id(self, id):
user = self._do_with_id(id)
if user: return user
raise security.UserIdNotFoundError()
def find_user(self, user):
user = self._do_find_user(user)
if user: return user
raise security.UserNotFoundError()
def find_role(self, role):
role = self._do_find_role(role)
if role: return role
raise security.RoleNotFoundError()
def create_role(self, commit=True, **kwargs):
role = security.Role(**self._prepare_create_role_args(kwargs))
return self._save_model(role)
def create_user(self, commit=True, **kwargs):
user = security.User(**self._prepare_create_user_args(kwargs))
return self._save_model(user)
def add_role_to_user(self, user, role):
return self._save_model(self._do_add_role(user, role))
def remove_role_from_user(self, user, role, commit=True):
return self._save_model(self._do_remove_role(user, role))
def deactivate_user(self, user):
return self._save_model(self._do_deactive_user(user))
def activate_user(self, user, commit=True):
return self._save_model(self._do_active_user(user))
-43
View File
@@ -1,43 +0,0 @@
from flask.ext import security
from flask.ext.security import UserMixin, RoleMixin
from flask.ext.security.datastore import UserDatastore
class MongoEngineUserDatastore(UserDatastore):
"""MongoEngine datastore"""
def __init__(self, db):
self.db = db
def get_models(self):
db = self.db
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):
username = db.StringField(unique=True, max_length=255)
email = db.StringField(unique=True, max_length=255)
password = db.StringField(required=True, max_length=120)
active = db.BooleanField(default=True)
roles= db.ListField(db.ReferenceField(Role), default=[])
created_at = db.DateTimeField()
modified_at = db.DateTimeField()
return User, Role
def _save_model(self, model):
model.save()
return model
def _do_with_id(self, id):
try: return security.User.objects.get(id=id)
except: return None
def _do_find_user(self, user):
return security.User.objects(username=user).first() or \
security.User.objects(email=user).first()
def _do_find_role(self, role):
return security.Role.objects(name=role).first()
-66
View File
@@ -1,66 +0,0 @@
from flask.ext import security
from flask.ext.security import UserMixin, RoleMixin
from flask.ext.security.datastore import UserDatastore
class SQLAlchemyUserDatastore(UserDatastore):
"""SQLAlchemy datastore"""
def __init__(self, db):
self.db = db
def get_models(self):
db = self.db
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('role.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('user.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
def __init__(self, name=None, description=None):
self.name = name
self.description = description
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), unique=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(120))
active = db.Column(db.Boolean())
created_at = db.Column(db.DateTime())
modified_at = db.Column(db.DateTime())
roles= db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
def __init__(self, username=None, email=None, password=None,
active=True, roles=None,
created_at=None, modified_at=None):
self.username = username
self.email = email
self.password = password
self.active = active
self.roles = roles or []
self.created_at = created_at
self.modified_at = modified_at
return User, Role
def _save_model(self, model):
self.db.session.add(model)
self.db.session.commit()
return model
def _do_with_id(self, id):
return security.User.query.get(id)
def _do_find_user(self, user):
return security.User.query.filter_by(username=user).first() or \
security.User.query.filter_by(email=user).first()
def _do_find_role(self, role):
return security.Role.query.filter_by(name=role).first()
+210
View File
@@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.decorators
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security decorators module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from collections import namedtuple
from functools import wraps
from flask import current_app, Response, request, redirect, _request_ctx_stack
from flask.ext.login import current_user, login_required
from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed
from werkzeug.local import LocalProxy
from . import utils
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
_default_unauthorized_html = """
<h1>Unauthorized</h1>
<p>The server could not verify that you are authorized to access the URL
requested. You either supplied the wrong credentials (e.g. a bad password),
or your browser doesn't understand how to supply the credentials required.</p>
"""
BasicAuth = namedtuple('BasicAuth', 'username, password')
def _get_unauthorized_response(text=None, headers=None):
text = text or _default_unauthorized_html
headers = headers or {}
return Response(text, 401, headers)
def _get_unauthorized_view():
cv = utils.get_url(utils.config_value('UNAUTHORIZED_VIEW'))
utils.do_flash(*utils.get_message('UNAUTHORIZED'))
return redirect(cv or request.referrer or '/')
def _check_token():
header_key = _security.token_authentication_header
args_key = _security.token_authentication_key
header_token = request.headers.get(header_key, None)
token = request.args.get(args_key, header_token)
if request.json:
token = request.json.get(args_key, token)
serializer = _security.remember_token_serializer
try:
data = serializer.loads(token)
except:
return False
user = _security.datastore.find_user(id=data[0])
if utils.md5(user.password) == data[1]:
app = current_app._get_current_object()
_request_ctx_stack.top.user = user
identity_changed.send(app, identity=Identity(user.id))
return True
def _check_http_auth():
auth = request.authorization or BasicAuth(username=None, password=None)
user = _security.datastore.find_user(email=auth.username)
if user and utils.verify_and_update_password(auth.password, user):
_security.datastore.commit()
app = current_app._get_current_object()
_request_ctx_stack.top.user = user
identity_changed.send(app, identity=Identity(user.id))
return True
return False
def http_auth_required(realm):
"""Decorator that protects endpoints using Basic HTTP authentication.
The username should be set to the user's email address.
:param realm: optional realm name"""
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
if _check_http_auth():
return fn(*args, **kwargs)
r = _security.default_http_auth_realm if callable(realm) else realm
h = {'WWW-Authenticate': 'Basic realm="%s"' % r}
return _get_unauthorized_response(headers=h)
return wrapper
if callable(realm):
return decorator(realm)
return decorator
def auth_token_required(fn):
"""Decorator that protects endpoints using token authentication. The token
should be added to the request by the client by using a query string
variable with a name equal to the configuration value of
`SECURITY_TOKEN_AUTHENTICATION_KEY` or in a request header named that of
the configuration value of `SECURITY_TOKEN_AUTHENTICATION_HEADER`
"""
@wraps(fn)
def decorated(*args, **kwargs):
if _check_token():
return fn(*args, **kwargs)
return _get_unauthorized_response()
return decorated
def auth_required(*auth_methods):
"""
Decorator that protects enpoints through multiple mechanisms
Example::
@app.route('/dashboard')
@auth_required('token', 'session')
def dashboard():
return 'Dashboard'
:param auth_methods: Specified mechanisms.
"""
login_mechanisms = {
'token': lambda: _check_token(),
'basic': lambda: _check_http_auth(),
'session': lambda: current_user.is_authenticated()
}
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
mechanisms = [login_mechanisms.get(method) for method in auth_methods]
for mechanism in mechanisms:
if mechanism and mechanism():
return fn(*args, **kwargs)
return _get_unauthorized_response()
return decorated_view
return wrapper
def roles_required(*roles):
"""Decorator which specifies that a user must have all the specified roles.
Example::
@app.route('/dashboard')
@roles_required('admin', 'editor')
def dashboard():
return 'Dashboard'
The current user must have both the `admin` role and `editor` role in order
to view the page.
:param args: The required roles.
"""
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
perms = [Permission(RoleNeed(role)) for role in roles]
for perm in perms:
if not perm.can():
return _get_unauthorized_view()
return fn(*args, **kwargs)
return decorated_view
return wrapper
def roles_accepted(*roles):
"""Decorator which specifies that a user must have at least one of the
specified roles. Example::
@app.route('/create_post')
@roles_accepted('editor', 'author')
def create_post():
return 'Create Post'
The current user must have either the `editor` role or `author` role in
order to view the page.
:param args: The possible roles.
"""
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
perm = Permission(*[RoleNeed(role) for role in roles])
if perm.can():
return fn(*args, **kwargs)
return _get_unauthorized_view()
return decorated_view
return wrapper
def anonymous_user_required(f):
@wraps(f)
def wrapper(*args, **kwargs):
if current_user.is_authenticated():
return redirect(utils.get_url(_security.post_login_view))
return f(*args, **kwargs)
return wrapper
+277
View File
@@ -0,0 +1,277 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.forms
~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security forms module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
import inspect
import urlparse
import flask_wtf as wtf
from flask import request, current_app
from flask_wtf import Form as BaseForm, TextField, PasswordField, \
SubmitField, HiddenField, BooleanField, ValidationError, Field
from flask_login import current_user
from werkzeug.local import LocalProxy
from .confirmable import requires_confirmation
from .utils import verify_and_update_password, get_message
# Convenient reference
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
_default_field_labels = {
'email': 'Email Address',
'password': 'Password',
'remember_me': 'Remember Me',
'login': 'Login',
'retype_password': 'Retype Password',
'register': 'Register',
'send_confirmation': 'Resend Confirmation Instructions',
'recover_password': 'Recover Password',
'reset_password': 'Reset Password',
'retype_password': 'Retype Password',
'new_password': 'New Password',
'change_password': 'Change Password',
'send_login_link': 'Send Login Link'
}
class ValidatorMixin(object):
def __call__(self, form, field):
if self.message and self.message.isupper():
self.message = get_message(self.message)[0]
return super(ValidatorMixin, self).__call__(form, field)
class EqualTo(ValidatorMixin, wtf.EqualTo):
pass
class Required(ValidatorMixin, wtf.Required):
pass
class Email(ValidatorMixin, wtf.Email):
pass
class Length(ValidatorMixin, wtf.Length):
pass
email_required = Required(message='EMAIL_NOT_PROVIDED')
email_validator = Email(message='INVALID_EMAIL_ADDRESS')
password_required = Required(message='PASSWORD_NOT_PROVIDED')
def get_form_field_label(key):
return _default_field_labels.get(key, '')
def unique_user_email(form, field):
if _datastore.find_user(email=field.data) is not None:
msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0]
raise ValidationError(msg)
def valid_user_email(form, field):
form.user = _datastore.find_user(email=field.data)
if form.user is None:
raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0])
class Form(BaseForm):
def __init__(self, *args, **kwargs):
if current_app.testing:
self.TIME_LIMIT = None
super(Form, self).__init__(*args, **kwargs)
class EmailFormMixin():
email = TextField(get_form_field_label('email'),
validators=[email_required,
email_validator])
class UserEmailFormMixin():
user = None
email = TextField(get_form_field_label('email'),
validators=[email_required,
email_validator,
valid_user_email])
class UniqueEmailFormMixin():
email = TextField(get_form_field_label('email'),
validators=[email_required,
email_validator,
unique_user_email])
class PasswordFormMixin():
password = PasswordField(get_form_field_label('password'),
validators=[password_required])
class NewPasswordFormMixin():
password = PasswordField(get_form_field_label('password'),
validators=[password_required,
Length(min=6, max=128)])
class PasswordConfirmFormMixin():
password_confirm = PasswordField(
get_form_field_label('retype_password'),
validators=[EqualTo('password', message='RETYPE_PASSWORD_MISMATCH')])
class NextFormMixin():
next = HiddenField()
def validate_next(self, field):
url_next = urlparse.urlsplit(field.data)
url_base = urlparse.urlsplit(request.host_url)
if url_next.netloc and url_next.netloc != url_base.netloc:
field.data = ''
raise ValidationError(get_message('INVALID_REDIRECT')[0])
class RegisterFormMixin():
submit = SubmitField(get_form_field_label('register'))
def to_dict(form):
def is_field_and_user_attr(member):
return isinstance(member, Field) and \
hasattr(_datastore.user_model, member.name)
fields = inspect.getmembers(form, is_field_and_user_attr)
return dict((key, value.data) for key, value in fields)
class SendConfirmationForm(Form, UserEmailFormMixin):
"""The default forgot password form"""
submit = SubmitField(get_form_field_label('send_confirmation'))
def __init__(self, *args, **kwargs):
super(SendConfirmationForm, self).__init__(*args, **kwargs)
if request.method == 'GET':
self.email.data = request.args.get('email', None)
def validate(self):
if not super(SendConfirmationForm, self).validate():
return False
if self.user.confirmed_at is not None:
self.email.errors.append(get_message('ALREADY_CONFIRMED')[0])
return False
return True
class ForgotPasswordForm(Form, UserEmailFormMixin):
"""The default forgot password form"""
submit = SubmitField(get_form_field_label('recover_password'))
class PasswordlessLoginForm(Form, UserEmailFormMixin):
"""The passwordless login form"""
submit = SubmitField(get_form_field_label('send_login_link'))
def __init__(self, *args, **kwargs):
super(PasswordlessLoginForm, self).__init__(*args, **kwargs)
def validate(self):
if not super(PasswordlessLoginForm, self).validate():
return False
if not self.user.is_active():
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
return False
return True
class LoginForm(Form, NextFormMixin):
"""The default login form"""
email = TextField(get_form_field_label('email'))
password = PasswordField(get_form_field_label('password'))
remember = BooleanField(get_form_field_label('remember_me'))
submit = SubmitField(get_form_field_label('login'))
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
def validate(self):
if not super(LoginForm, self).validate():
return False
if self.email.data.strip() == '':
self.email.errors.append(get_message('EMAIL_NOT_PROVIDED')[0])
return False
if self.password.data.strip() == '':
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
return False
self.user = _datastore.find_user(email=self.email.data)
if self.user is None:
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
return False
if not verify_and_update_password(self.password.data, self.user):
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False
if requires_confirmation(self.user):
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
return False
if not self.user.is_active():
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
return False
return True
class ConfirmRegisterForm(Form, RegisterFormMixin,
UniqueEmailFormMixin, NewPasswordFormMixin):
pass
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
pass
class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
"""The default reset password form"""
submit = SubmitField(get_form_field_label('reset_password'))
class ChangePasswordForm(Form, PasswordFormMixin):
"""The default change password form"""
new_password = PasswordField(get_form_field_label('new_password'),
validators=[password_required,
Length(min=6, max=128)])
new_password_confirm = PasswordField(get_form_field_label('retype_password'),
validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
submit = SubmitField(get_form_field_label('change_password'))
def validate(self):
if not super(ChangePasswordForm, self).validate():
return False
if self.password.data.strip() == '':
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
return False
if not verify_and_update_password(self.password.data, current_user):
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False
return True
+59
View File
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.passwordless
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security passwordless module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from flask import request, current_app as app
from werkzeug.local import LocalProxy
from .signals import login_instructions_sent
from .utils import send_mail, url_for_security, get_token_status, \
config_value
# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
def send_login_instructions(user):
"""Sends the login instructions email for the specified user.
:param user: The user to send the instructions to
:param token: The login token
"""
token = generate_login_token(user)
url = url_for_security('token_login', token=token)
login_link = request.url_root[:-1] + url
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
'login_instructions', user=user, login_link=login_link)
login_instructions_sent.send(app._get_current_object(),
user=user, login_token=token)
def generate_login_token(user):
"""Generates a unique login token for the specified user.
:param user: The user the token belongs to
"""
return _security.login_serializer.dumps([str(user.id)])
def login_token_status(token):
"""Returns the expired status, invalid status, and user of a login token.
For example::
expired, invalid, user = login_token_status('...')
:param token: The login token
"""
return get_token_status(token, 'login', 'LOGIN')
+80
View File
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.recoverable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security recoverable module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from flask import current_app as app, request
from werkzeug.local import LocalProxy
from .signals import password_reset, reset_password_instructions_sent
from .utils import send_mail, md5, encrypt_password, url_for_security, \
get_token_status, config_value
# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
def send_reset_password_instructions(user):
"""Sends the reset password instructions email for the specified user.
:param user: The user to send the instructions to
"""
token = generate_reset_password_token(user)
url = url_for_security('reset_password', token=token)
reset_link = request.url_root[:-1] + url
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), user.email,
'reset_instructions',
user=user, reset_link=reset_link)
reset_password_instructions_sent.send(app._get_current_object(),
user=user, token=token)
def send_password_reset_notice(user):
"""Sends the password reset notice email for the specified user.
:param user: The user to send the notice to
"""
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
'reset_notice', user=user)
def generate_reset_password_token(user):
"""Generates a unique reset password token for the specified user.
:param user: The user to work with
"""
data = [str(user.id), md5(user.password)]
return _security.reset_serializer.dumps(data)
def reset_password_token_status(token):
"""Returns the expired status, invalid status, and user of a password reset
token. For example::
expired, invalid, user = reset_password_token_status('...')
:param token: The password reset token
"""
return get_token_status(token, 'reset', 'RESET_PASSWORD')
def update_password(user, password):
"""Update the specified user's password
:param user: The user to update_password
:param password: The unencrypted new password
"""
user.password = encrypt_password(password)
_datastore.put(user)
send_password_reset_notice(user)
password_reset.send(app._get_current_object(), user=user)
+43
View File
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.registerable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security registerable module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from flask import current_app as app
from werkzeug.local import LocalProxy
from .confirmable import generate_confirmation_link
from .signals import user_registered
from .utils import do_flash, get_message, send_mail, encrypt_password, \
config_value
# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
def register_user(**kwargs):
confirmation_link, token = None, None
kwargs['password'] = encrypt_password(kwargs['password'])
user = _datastore.create_user(**kwargs)
_datastore.commit()
if _security.confirmable:
confirmation_link, token = generate_confirmation_link(user)
do_flash(*get_message('CONFIRM_REGISTRATION', email=user.email))
user_registered.send(app._get_current_object(),
user=user, confirm_token=token)
if config_value('SEND_REGISTER_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
user=user, confirmation_link=confirmation_link)
return user
+82 -30
View File
@@ -1,41 +1,87 @@
import json # -*- coding: utf-8 -*-
"""
flask.ext.security.script
~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security script module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
try:
import simplejson as json
except ImportError:
import json
import re import re
from flask import current_app
from flask.ext.script import Command, Option from flask.ext.script import Command, Option
from flask.ext.security import (UserCreationError, UserNotFoundError, from werkzeug.local import LocalProxy
RoleNotFoundError, user_datastore)
from .utils import encrypt_password
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
def pprint(obj): def pprint(obj):
print json.dumps(obj, sort_keys=True, indent=4) print json.dumps(obj, sort_keys=True, indent=4)
def commit(fn):
def wrapper(*args, **kwargs):
fn(*args, **kwargs)
_datastore.commit()
return wrapper
class CreateUserCommand(Command): class CreateUserCommand(Command):
"""Create a user""" """Create a user"""
option_list = ( option_list = (
Option('-u', '--username', dest='username', default=None),
Option('-e', '--email', dest='email', default=None), Option('-e', '--email', dest='email', default=None),
Option('-p', '--password', dest='password', default=None), Option('-p', '--password', dest='password', default=None),
Option('-a', '--active', dest='active', default=''), Option('-a', '--active', dest='active', default=''),
Option('-r', '--roles', dest='roles', default=''),
) )
@commit
def run(self, **kwargs): def run(self, **kwargs):
# sanitize active input # sanitize active input
ai = re.sub(r'\s', '', str(kwargs['active'])) ai = re.sub(r'\s', '', str(kwargs['active']))
kwargs['active'] = ai.lower() in ['', 'y','yes', '1', 'active'] kwargs['active'] = ai.lower() in ['', 'y', 'yes', '1', 'active']
# sanitize role input a bit from flask_security.forms import ConfirmRegisterForm
ri = re.sub(r'\s', '', kwargs['roles']) from werkzeug.datastructures import MultiDict
kwargs['roles'] = [] if ri == '' else ri.split(',')
form = ConfirmRegisterForm(MultiDict(kwargs), csrf_enabled=False)
user_datastore.create_user(**kwargs)
if form.validate():
print 'User created successfully.' kwargs['password'] = encrypt_password(kwargs['password'])
kwargs['password'] = '****' _datastore.create_user(**kwargs)
pprint(kwargs) print 'User created successfully.'
kwargs['password'] = '****'
pprint(kwargs)
else:
print 'Error creating user'
pprint(form.errors)
class _RoleCommand(Command): class CreateRoleCommand(Command):
"""Create a role"""
option_list = (
Option('-n', '--name', dest='name', default=None),
Option('-d', '--desc', dest='description', default=None),
)
@commit
def run(self, **kwargs):
_datastore.create_role(**kwargs)
print 'Role "%(name)s" created successfully.' % kwargs
class _RoleCommand(Command):
option_list = ( option_list = (
Option('-u', '--user', dest='user_identifier'), Option('-u', '--user', dest='user_identifier'),
Option('-r', '--role', dest='role_name'), Option('-r', '--role', dest='role_name'),
@@ -44,35 +90,41 @@ class _RoleCommand(Command):
class AddRoleCommand(_RoleCommand): class AddRoleCommand(_RoleCommand):
"""Add a role to a user""" """Add a role to a user"""
@commit
def run(self, user_identifier, role_name): def run(self, user_identifier, role_name):
user_datastore.add_role_to_user(user_identifier, role_name) _datastore.add_role_to_user(user_identifier, role_name)
print "Role '%s' added to user '%s' successfully" % (role_name, user_identifier) print "Role '%s' added to user '%s' successfully" % (role_name, user_identifier)
class RemoveRoleCommand(_RoleCommand): class RemoveRoleCommand(_RoleCommand):
"""Add a role to a user""" """Add a role to a user"""
@commit
def run(self, user_identifier, role_name): def run(self, user_identifier, role_name):
user_datastore.remove_role_from_user(user_identifier, role_name) _datastore.remove_role_from_user(user_identifier, role_name)
print "Role '%s' removed from user '%s' successfully" % (role_name, user_identifier) print "Role '%s' removed from user '%s' successfully" % (role_name, user_identifier)
class _ToggleActiveCommand(Command): class _ToggleActiveCommand(Command):
option_list = ( option_list = (
Option('-u', '--user', dest='user_identifier'), Option('-u', '--user', dest='user_identifier'),
) )
class DeactivateUserCommand(_ToggleActiveCommand): class DeactivateUserCommand(_ToggleActiveCommand):
"""Deactive a user""" """Deactive a user"""
@commit
def run(self, user_identifier): def run(self, user_identifier):
user_datastore.deactivate_user(user_identifier) _datastore.deactivate_user(user_identifier)
print "User '%s' has been deactivated" % user_identifier print "User '%s' has been deactivated" % user_identifier
class ActivateUserCommand(_ToggleActiveCommand): class ActivateUserCommand(_ToggleActiveCommand):
"""Deactive a user""" """Deactive a user"""
@commit
def run(self, user_identifier): def run(self, user_identifier):
user_datastore.activate_user(user_identifier) _datastore.activate_user(user_identifier)
print "User '%s' has been activated" % user_identifier print "User '%s' has been activated" % user_identifier
+29
View File
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.signals
~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security signals module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
import blinker
signals = blinker.Namespace()
user_registered = signals.signal("user-registered")
user_confirmed = signals.signal("user-confirmed")
confirm_instructions_sent = signals.signal("confirm-instructions-sent")
login_instructions_sent = signals.signal("login-instructions-sent")
password_reset = signals.signal("password-reset")
password_changed = signals.signal("password-changed")
reset_password_instructions_sent = signals.signal("password-reset-instructions-sent")
@@ -0,0 +1,16 @@
{% macro render_field_with_errors(field) %}
<p>
{{ field.label }} {{ field(**kwargs)|safe }}
{% if field.errors %}
<ul>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</p>
{% endmacro %}
{% macro render_field(field) %}
<p>{{ field(**kwargs)|safe }}</p>
{% endmacro %}
@@ -0,0 +1,15 @@
{% if security.registerable or security.recoverable or security.confirmabled %}
<h2>Menu</h2>
<ul>
<li><a href="{{ url_for_security('login') }}">Login</a></li>
{% if security.registerable %}
<li><a href="{{ url_for_security('register') }}">Register</a><br/></li>
{% endif %}
{% if security.recoverable %}
<li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li>
{% endif %}
{% if security.confirmable %}
<li><a href="{{ url_for_security('send_confirmation') }}">Confirm account</a></li>
{% endif %}
</ul>
{% endif %}
@@ -0,0 +1,9 @@
{%- with messages = get_flashed_messages(with_categories=true) -%}
{% if messages %}
<ul class="flashes">
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{%- endwith %}
@@ -0,0 +1,11 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% include "security/_messages.html" %}
<h1>Change password</h1>
<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
{{ change_password_form.hidden_tag() }}
{{ render_field_with_errors(change_password_form.password) }}
{{ render_field_with_errors(change_password_form.new_password) }}
{{ render_field_with_errors(change_password_form.new_password_confirm) }}
{{ render_field(change_password_form.submit) }}
</form>
@@ -0,0 +1,4 @@
<p>Your password has been changed.</p>
{% if security.recoverable %}
<p>If you did not change your password, <a href="{{ url_for_security('forgot_password', _external=True) }}">click here to reset it</a>.</p>
{% endif %}
@@ -0,0 +1,5 @@
Your password has been changed
{% if security.recoverable %}
If you did not change your password, click the link below to reset it.
{{ url_for_security('forgot_password', _external=True) }}
{% endif %}
@@ -0,0 +1,3 @@
<p>Please confirm your email through the link below:</p>
<p><a href="{{ confirmation_link }}">Confirm my account</a></p>
@@ -0,0 +1,3 @@
Please confirm your email through the link below:
{{ confirmation_link }}
@@ -0,0 +1,5 @@
<p>Welcome {{ user.email }}!</p>
<p>You can log into your through the link below:</p>
<p><a href="{{ login_link }}">Login now</a></p>
@@ -0,0 +1,5 @@
Welcome {{ user.email }}!
You can log into your through the link below:
{{ login_link }}
@@ -0,0 +1 @@
<p><a href="{{ reset_link }}">Click here to reset your password</a></p>
@@ -0,0 +1,3 @@
Click the link below to reset your password:
{{ reset_link }}
@@ -0,0 +1 @@
<p>Your password has been reset</p>
@@ -0,0 +1 @@
Your password has been reset
@@ -0,0 +1,7 @@
<p>Welcome {{ user.email }}!</p>
{% if security.confirmable %}
<p>You can confirm your email through the link below:</p>
<p><a href="{{ confirmation_link }}">Confirm my account</a></p>
{% endif %}
@@ -0,0 +1,7 @@
Welcome {{ user.email }}!
{% if security.confirmable %}
You can confirm your email through the link below:
{{ confirmation_link }}
{% endif %}
@@ -0,0 +1,9 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% include "security/_messages.html" %}
<h1>Send password reset instructions</h1>
<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
{{ forgot_password_form.hidden_tag() }}
{{ render_field_with_errors(forgot_password_form.email) }}
{{ render_field(forgot_password_form.submit) }}
</form>
{% include "security/_menu.html" %}
@@ -0,0 +1,12 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% include "security/_messages.html" %}
<h1>Login</h1>
<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
{{ login_user_form.hidden_tag() }}
{{ render_field_with_errors(login_user_form.email) }}
{{ render_field_with_errors(login_user_form.password) }}
{{ render_field_with_errors(login_user_form.remember) }}
{{ render_field(login_user_form.next) }}
{{ render_field(login_user_form.submit) }}
</form>
{% include "security/_menu.html" %}
@@ -0,0 +1,13 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% include "security/_messages.html" %}
<h1>Register</h1>
<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
{{ register_user_form.hidden_tag() }}
{{ render_field_with_errors(register_user_form.email) }}
{{ render_field_with_errors(register_user_form.password) }}
{% if register_user_form.password_confirm %}
{{ render_field_with_errors(register_user_form.password_confirm) }}
{% endif %}
{{ render_field(register_user_form.submit) }}
</form>
{% include "security/_menu.html" %}
@@ -0,0 +1,10 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% include "security/_messages.html" %}
<h1>Reset password</h1>
<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form">
{{ reset_password_form.hidden_tag() }}
{{ render_field_with_errors(reset_password_form.password) }}
{{ render_field_with_errors(reset_password_form.password_confirm) }}
{{ render_field(reset_password_form.submit) }}
</form>
{% include "security/_menu.html" %}
@@ -0,0 +1,9 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% include "security/_messages.html" %}
<h1>Resend confirmation instructions</h1>
<form action="{{ url_for_security('send_confirmation') }}" method="POST" name="send_confirmation_form">
{{ send_confirmation_form.hidden_tag() }}
{{ render_field_with_errors(send_confirmation_form.email) }}
{{ render_field(send_confirmation_form.submit) }}
</form>
{% include "security/_menu.html" %}
@@ -0,0 +1,9 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% include "security/_messages.html" %}
<h1>Login</h1>
<form action="{{ url_for_security('login') }}" method="POST" name="send_login_form">
{{ send_login_form.hidden_tag() }}
{{ render_field_with_errors(send_login_form.email) }}
{{ render_field(send_login_form.submit) }}
</form>
{% include "security/_menu.html" %}
+379
View File
@@ -0,0 +1,379 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.utils
~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security utils module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
import base64
import blinker
import functools
import hashlib
import hmac
from contextlib import contextmanager
from datetime import datetime, timedelta
from flask import url_for, flash, current_app, request, session, render_template
from flask.ext.login import login_user as _login_user, \
logout_user as _logout_user
from flask.ext.mail import Message
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
from itsdangerous import BadSignature, SignatureExpired
from werkzeug.local import LocalProxy
from .signals import user_registered, user_confirmed, \
confirm_instructions_sent, login_instructions_sent, \
password_reset, password_changed, reset_password_instructions_sent
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
_pwd_context = LocalProxy(lambda: _security.pwd_context)
def login_user(user, remember=True):
"""Performs the login and sends the appropriate signal."""
if not _login_user(user, remember):
return False
if _security.trackable:
old_current_login, new_current_login = user.current_login_at, datetime.utcnow()
remote_addr = request.remote_addr or 'untrackable'
old_current_ip, new_current_ip = user.current_login_ip, remote_addr
user.last_login_at = old_current_login or new_current_login
user.current_login_at = new_current_login
user.last_login_ip = old_current_ip or new_current_ip
user.current_login_ip = new_current_ip
user.login_count = user.login_count + 1 if user.login_count else 1
_datastore.put(user)
identity_changed.send(current_app._get_current_object(),
identity=Identity(user.id))
return True
def logout_user():
for key in ('identity.name', 'identity.auth_type'):
session.pop(key, None)
identity_changed.send(current_app._get_current_object(),
identity=AnonymousIdentity())
_logout_user()
def get_hmac(password):
if _security.password_hash == 'plaintext':
return password
if _security.password_salt is None:
raise RuntimeError('The configuration value `SECURITY_PASSWORD_SALT` '
'must not be None when the value of `SECURITY_PASSWORD_HASH` is '
'set to "%s"' % _security.password_hash)
h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512)
return base64.b64encode(h.digest())
def verify_password(password, password_hash):
return _pwd_context.verify(get_hmac(password), password_hash)
def verify_and_update_password(password, user):
verified, new_password = _pwd_context.verify_and_update(get_hmac(password), user.password)
if verified and new_password:
user.password = new_password
_datastore.put(user)
return verified
def encrypt_password(password):
return _pwd_context.encrypt(get_hmac(password))
def md5(data):
return hashlib.md5(data).hexdigest()
def do_flash(message, category=None):
"""Flash a message depending on if the `FLASH_MESSAGES` configuration
value is set.
:param message: The flash message
:param category: The flash message category
"""
if config_value('FLASH_MESSAGES'):
flash(message, category)
def get_url(endpoint_or_url):
"""Returns a URL if a valid endpoint is found. Otherwise, returns the
provided value.
:param endpoint_or_url: The endpoint name or URL to default to
"""
try:
return url_for(endpoint_or_url)
except:
return endpoint_or_url
def get_security_endpoint_name(endpoint):
return '%s.%s' % (_security.blueprint_name, endpoint)
def url_for_security(endpoint, **values):
"""Return a URL for the security blueprint
:param endpoint: the endpoint of the URL (name of the function)
:param values: the variable arguments of the URL rule
:param _external: if set to `True`, an absolute URL is generated. Server
address can be changed via `SERVER_NAME` configuration variable which
defaults to `localhost`.
:param _anchor: if provided this is added as anchor to the URL.
:param _method: if provided this explicitly specifies an HTTP method.
"""
endpoint = get_security_endpoint_name(endpoint)
return url_for(endpoint, **values)
def get_post_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 find_redirect(key):
"""Returns the URL to redirect to after a user logs in successfully.
:param key: The session or application configuration key to search for
"""
rv = (get_url(session.pop(key.lower(), None)) or
get_url(current_app.config[key.upper()] or None) or '/')
return rv
def get_config(app):
"""Conveniently get the security configuration for the specified
application without the annoying 'SECURITY_' prefix.
:param app: The application to inspect
"""
items = app.config.items()
prefix = 'SECURITY_'
def strip_prefix(tup):
return (tup[0].replace('SECURITY_', ''), tup[1])
return dict([strip_prefix(i) for i in items if i[0].startswith(prefix)])
def get_message(key, **kwargs):
rv = config_value('MSG_' + key)
return rv[0] % kwargs, rv[1]
def config_value(key, app=None, default=None):
"""Get a Flask-Security configuration value.
:param key: The configuration key without the prefix `SECURITY_`
:param app: An optional specific application to inspect. Defaults to Flask's
`current_app`
:param default: An optional default value if the value is not set
"""
app = app or current_app
return get_config(app).get(key.upper(), default)
def get_max_age(key, app=None):
now = datetime.utcnow()
expires = now + get_within_delta(key + '_WITHIN', app)
td = (expires - now)
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6
def get_within_delta(key, app=None):
"""Get a timedelta object from the application configuration following
the internal convention of::
<Amount of Units> <Type of Units>
Examples of valid config values::
5 days
10 minutes
:param key: The config value key without the 'SECURITY_' prefix
:param app: Optional application to inspect. Defaults to Flask's
`current_app`
"""
txt = config_value(key, app=app)
values = txt.split()
return timedelta(**{values[1]: int(values[0])})
def send_mail(subject, recipient, template, **context):
"""Send an email via the Flask-Mail extension.
:param subject: Email subject
:param recipient: Email recipient
:param template: The name of the email template
:param context: The context to render the template with
"""
context.setdefault('security', _security)
context.update(_security._run_ctx_processor('mail'))
msg = Message(subject,
sender=_security.email_sender,
recipients=[recipient])
ctx = ('security/email', template)
msg.body = render_template('%s/%s.txt' % ctx, **context)
msg.html = render_template('%s/%s.html' % ctx, **context)
if _security._send_mail_task:
_security._send_mail_task(msg)
return
mail = current_app.extensions.get('mail')
mail.send(msg)
def get_token_status(token, serializer, max_age=None):
serializer = getattr(_security, serializer + '_serializer')
max_age = get_max_age(max_age)
user, data = None, None
expired, invalid = False, False
try:
data = serializer.loads(token, max_age=max_age)
except SignatureExpired:
d, data = serializer.loads_unsafe(token)
expired = True
except BadSignature:
invalid = True
if data:
user = _datastore.find_user(id=data[0])
expired = expired and (user is not None)
return expired, invalid, user
@contextmanager
def capture_passwordless_login_requests():
login_requests = []
def _on(app, **data):
login_requests.append(data)
login_instructions_sent.connect(_on)
try:
yield login_requests
finally:
login_instructions_sent.disconnect(_on)
@contextmanager
def capture_registrations():
"""Testing utility for capturing registrations.
:param confirmation_sent_at: An optional datetime object to set the
user's `confirmation_sent_at` to
"""
registrations = []
def _on(app, **data):
registrations.append(data)
user_registered.connect(_on)
try:
yield registrations
finally:
user_registered.disconnect(_on)
@contextmanager
def capture_reset_password_requests(reset_password_sent_at=None):
"""Testing utility for capturing password reset requests.
:param reset_password_sent_at: An optional datetime object to set the
user's `reset_password_sent_at` to
"""
reset_requests = []
def _on(app, **data):
reset_requests.append(data)
reset_password_instructions_sent.connect(_on)
try:
yield reset_requests
finally:
reset_password_instructions_sent.disconnect(_on)
class CaptureSignals(object):
"""Testing utility for capturing blinker signals.
Context manager which mocks out selected signals and registers which are `sent` on and what
arguments were sent. Instantiate with a list of blinker `NamedSignals` to patch. Each signal
has it's `send` mocked out.
"""
def __init__(self, signals):
"""Patch all given signals and make them available as attributes.
:param signals: list of signals
"""
self._records = {}
self._receivers = {}
for signal in signals:
self._records[signal] = []
self._receivers[signal] = functools.partial(self._record, signal)
def __getitem__(self, signal):
"""All captured signals are available via `ctxt[signal]`.
"""
if isinstance(signal, blinker.base.NamedSignal):
return self._records[signal]
else:
super(CaptureSignals, self).__setitem__(signal)
def _record(self, signal, *args, **kwargs):
self._records[signal].append((args, kwargs))
def __enter__(self):
for signal, receiver in self._receivers.iteritems():
signal.connect(receiver)
return self
def __exit__(self, type, value, traceback):
for signal, receiver in self._receivers.iteritems():
signal.disconnect(receiver)
def signals_sent(self):
"""Return a set of the signals sent.
:rtype: list of blinker `NamedSignals`.
"""
return set([signal for signal, _ in self._records.iteritems() if self._records[signal]])
def capture_signals():
"""Factory method that creates a `CaptureSignals` with all the flask_security signals."""
return CaptureSignals([user_registered, user_confirmed,
confirm_instructions_sent, login_instructions_sent,
password_reset, password_changed,
reset_password_instructions_sent])
+359
View File
@@ -0,0 +1,359 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.views
~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security views module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from flask import current_app, redirect, request, render_template, jsonify, \
after_this_request, Blueprint
from flask_login import current_user
from werkzeug.datastructures import MultiDict
from werkzeug.local import LocalProxy
from .confirmable import send_confirmation_instructions, \
confirm_user, confirm_email_token_status
from .decorators import login_required, anonymous_user_required
from .passwordless import send_login_instructions, \
login_token_status
from .recoverable import reset_password_token_status, \
send_reset_password_instructions, update_password
from .changeable import change_user_password
from .registerable import register_user
from .utils import get_url, get_post_login_redirect, do_flash, \
get_message, login_user, logout_user, url_for_security as url_for, \
config_value
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
def _render_json(form, include_auth_token=False):
has_errors = len(form.errors) > 0
if has_errors:
code = 400
response = dict(errors=form.errors)
else:
code = 200
response = dict(user=dict(id=str(form.user.id)))
if include_auth_token:
token = form.user.get_auth_token()
response['user']['authentication_token'] = token
return jsonify(dict(meta=dict(code=code), response=response))
def _commit(response=None):
_datastore.commit()
return response
def _ctx(endpoint):
return _security._run_ctx_processor(endpoint)
@anonymous_user_required
def login():
"""View function for login view"""
form_class = _security.login_form
if request.json:
form = form_class(MultiDict(request.json))
else:
form = form_class()
if form.validate_on_submit():
login_user(form.user, remember=form.remember.data)
after_this_request(_commit)
if not request.json:
return redirect(get_post_login_redirect())
form.next.data = get_url(request.args.get('next')) \
or get_url(request.form.get('next')) or ''
if request.json:
return _render_json(form, True)
return render_template(config_value('LOGIN_USER_TEMPLATE'),
login_user_form=form,
**_ctx('login'))
@login_required
def logout():
"""View function which handles a logout request."""
logout_user()
return redirect(request.args.get('next', None) or
get_url(_security.post_logout_view))
def register():
"""View function which handles a registration request."""
if _security.confirmable or request.json:
form_class = _security.confirm_register_form
else:
form_class = _security.register_form
if request.json:
form_data = MultiDict(request.json)
else:
form_data = request.form
form = form_class(form_data)
if form.validate_on_submit():
user = register_user(**form.to_dict())
form.user = user
if not _security.confirmable or _security.login_without_confirmation:
after_this_request(_commit)
login_user(user)
if not request.json:
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 request.json:
return _render_json(form)
return render_template(config_value('REGISTER_USER_TEMPLATE'),
register_user_form=form,
**_ctx('register'))
def send_login():
"""View function that sends login instructions for passwordless login"""
form_class = _security.passwordless_login_form
if request.json:
form = form_class(MultiDict(request.json))
else:
form = form_class()
if form.validate_on_submit():
send_login_instructions(form.user)
if request.json is None:
do_flash(*get_message('LOGIN_EMAIL_SENT', email=form.user.email))
if request.json:
return _render_json(form)
return render_template(config_value('SEND_LOGIN_TEMPLATE'),
send_login_form=form,
**_ctx('send_login'))
@anonymous_user_required
def token_login(token):
"""View function that handles passwordless login via a token"""
expired, invalid, user = login_token_status(token)
if invalid:
do_flash(*get_message('INVALID_LOGIN_TOKEN'))
if expired:
send_login_instructions(user)
do_flash(*get_message('LOGIN_EXPIRED', email=user.email,
within=_security.login_within))
if invalid or expired:
return redirect(url_for('login'))
login_user(user, True)
after_this_request(_commit)
do_flash(*get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'))
return redirect(get_post_login_redirect())
def send_confirmation():
"""View function which sends confirmation instructions."""
form_class = _security.send_confirmation_form
if request.json:
form = form_class(MultiDict(request.json))
else:
form = form_class()
if form.validate_on_submit():
send_confirmation_instructions(form.user)
if request.json is None:
do_flash(*get_message('CONFIRMATION_REQUEST', email=form.user.email))
if request.json:
return _render_json(form)
return render_template(config_value('SEND_CONFIRMATION_TEMPLATE'),
send_confirmation_form=form,
**_ctx('send_confirmation'))
@anonymous_user_required
def confirm_email(token):
"""View function which handles a email confirmation request."""
expired, invalid, user = confirm_email_token_status(token)
if not user or invalid:
invalid = True
do_flash(*get_message('INVALID_CONFIRMATION_TOKEN'))
if expired:
send_confirmation_instructions(user)
do_flash(*get_message('CONFIRMATION_EXPIRED', email=user.email,
within=_security.confirm_email_within))
if invalid or expired:
return redirect(get_url(_security.confirm_error_view) or
url_for('send_confirmation'))
confirm_user(user)
login_user(user, True)
after_this_request(_commit)
do_flash(*get_message('EMAIL_CONFIRMED'))
return redirect(get_url(_security.post_confirm_view) or
get_url(_security.post_login_view))
def forgot_password():
"""View function that handles a forgotten password request."""
form_class = _security.forgot_password_form
if request.json:
form = form_class(MultiDict(request.json))
else:
form = form_class()
if form.validate_on_submit():
send_reset_password_instructions(form.user)
if request.json is None:
do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email))
if request.json:
return _render_json(form)
return render_template(config_value('FORGOT_PASSWORD_TEMPLATE'),
forgot_password_form=form,
**_ctx('forgot_password'))
@anonymous_user_required
def reset_password(token):
"""View function that handles a reset password request."""
expired, invalid, user = reset_password_token_status(token)
if invalid:
do_flash(*get_message('INVALID_RESET_PASSWORD_TOKEN'))
if expired:
do_flash(*get_message('PASSWORD_RESET_EXPIRED', email=user.email,
within=_security.reset_password_within))
if invalid or expired:
return redirect(url_for('forgot_password'))
form = _security.reset_password_form()
if form.validate_on_submit():
after_this_request(_commit)
update_password(user, form.password.data)
do_flash(*get_message('PASSWORD_RESET'))
login_user(user, True)
return redirect(get_url(_security.post_reset_view) or
get_url(_security.post_login_view))
return render_template(config_value('RESET_PASSWORD_TEMPLATE'),
reset_password_form=form,
reset_password_token=token,
**_ctx('reset_password'))
@login_required
def change_password():
"""View function which handles a change password request."""
form_class = _security.change_password_form
if request.json:
form = form_class(MultiDict(request.json))
else:
form = form_class()
if form.validate_on_submit():
after_this_request(_commit)
change_user_password(current_user, form.new_password.data)
if request.json is None:
do_flash(*get_message('PASSWORD_CHANGE'))
return redirect(get_url(_security.post_change_view) or
get_url(_security.post_login_view))
if request.json:
return _render_json(form)
return render_template('security/change_password.html',
change_password_form=form,
**_ctx('change_password'))
def create_blueprint(state, import_name):
"""Creates the security extension blueprint"""
bp = Blueprint(state.blueprint_name, import_name,
url_prefix=state.url_prefix,
subdomain=state.subdomain,
template_folder='templates')
bp.route(state.logout_url, endpoint='logout')(logout)
if state.passwordless:
bp.route(state.login_url,
methods=['GET', 'POST'],
endpoint='login')(send_login)
bp.route(state.login_url + '/<token>',
endpoint='token_login')(token_login)
else:
bp.route(state.login_url,
methods=['GET', 'POST'],
endpoint='login')(login)
if state.registerable:
bp.route(state.register_url,
methods=['GET', 'POST'],
endpoint='register')(register)
if state.recoverable:
bp.route(state.reset_url,
methods=['GET', 'POST'],
endpoint='forgot_password')(forgot_password)
bp.route(state.reset_url + '/<token>',
methods=['GET', 'POST'],
endpoint='reset_password')(reset_password)
if state.changeable:
bp.route(state.change_url,
methods=['GET', 'POST'],
endpoint='change_password')(change_password)
if state.confirmable:
bp.route(state.confirm_url,
methods=['GET', 'POST'],
endpoint='send_confirmation')(send_confirmation)
bp.route(state.confirm_url + '/<token>',
methods=['GET', 'POST'],
endpoint='confirm_email')(confirm_email)
return bp
+7
View File
@@ -0,0 +1,7 @@
Flask>=0.9
Flask-Login>=0.1.3
Flask-Mail>=0.7.3
Flask-Principal>=0.3.3
Flask-WTF>=0.8
itsdangerous>=0.17
passlib>=1.6.1
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
make-release
~~~~~~~~~~~~
Helper script that performs a release. Does pretty much everything
automatically for us.
:copyright: (c) 2011 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
import sys
import os
import re
from datetime import datetime, date
from subprocess import Popen, PIPE
_date_clean_re = re.compile(r'(\d+)(st|nd|rd|th)')
def installed_libraries():
return Popen(['pip', 'freeze'], stdout=PIPE).communicate()[0]
def has_library_installed(library):
return library + '==' in installed_libraries()
def parse_changelog():
with open('CHANGES') as f:
lineiter = iter(f)
for line in lineiter:
match = re.search('^Version\s+(.*)', line.strip())
if match is None:
continue
version = match.group(1).strip()
if lineiter.next().count('-') != len(line.strip()):
fail('Invalid hyphen count below version line: %s', line.strip())
while 1:
released = lineiter.next().strip()
if released:
break
match = re.search(r'Released (\w+\s+\d+\w+\s+\d+)', released)
if match is None:
fail('Could not find release date in version %s' % version)
datestr = parse_date(match.group(1).strip())
return version, datestr
def bump_version(version):
try:
parts = map(int, version.split('.'))
except ValueError:
fail('Current version is not numeric')
parts[-1] += 1
return '.'.join(map(str, parts))
def parse_date(string):
string = _date_clean_re.sub(r'\1', string)
return datetime.strptime(string, '%B %d %Y')
def set_filename_version(filename, version_number, pattern):
changed = []
def inject_version(match):
before, old, after = match.groups()
changed.append(True)
return before + version_number + after
with open(filename) as f:
contents = re.sub(r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern,
inject_version, f.read())
if not changed:
fail('Could not find %s in %s', pattern, filename)
with open(filename, 'w') as f:
f.write(contents)
def set_init_version(version):
info('Setting __init__.py version to %s', version)
set_filename_version('flask_security/__init__.py', version, '__version__')
def set_setup_version(version):
info('Setting setup.py version to %s', version)
set_filename_version('setup.py', version, 'version')
def set_docs_version(version):
info('Setting docs/conf.py version to %s', version)
set_filename_version('docs/conf.py', version, 'version')
def build_and_upload():
Popen([sys.executable, 'setup.py', 'sdist', 'build_sphinx', 'upload', 'upload_sphinx']).wait()
def fail(message, *args):
print >> sys.stderr, 'Error:', message % args
sys.exit(1)
def info(message, *args):
print >> sys.stderr, message % args
def get_git_tags():
return set(Popen(['git', 'tag'], stdout=PIPE).communicate()[0].splitlines())
def git_is_clean():
return Popen(['git', 'diff', '--quiet']).wait() == 0
def make_git_commit(message, *args):
message = message % args
Popen(['git', 'commit', '-am', message]).wait()
def make_git_tag(tag):
info('Tagging "%s"', tag)
Popen(['git', 'tag', '-a', tag, '-m', '%s release' % tag]).wait()
Popen(['git', 'push', '--tags']).wait()
def update_version(version):
for f in [set_init_version, set_setup_version, set_docs_version]:
f(version)
def get_branches():
return set(Popen(['git', 'branch'], stdout=PIPE).communicate()[0].splitlines())
def branch_is(branch):
return '* ' + branch in get_branches()
def main():
os.chdir(os.path.join(os.path.dirname(__file__), '..'))
rv = parse_changelog()
if rv is None:
fail('Could not parse changelog')
version, release_date = rv
tags = get_git_tags()
for lib in ['Sphinx', 'Sphinx-PyPI-upload']:
if not has_library_installed(lib):
fail('Build requires that %s be installed', lib)
if version in tags:
fail('Version "%s" is already tagged', version)
if release_date.date() != date.today():
fail('Release date is not today')
if not branch_is('master'):
fail('You are not on the master branch')
if not git_is_clean():
fail('You have uncommitted changes in git')
info('Releasing %s (release date %s)',
version, release_date.strftime('%d/%m/%Y'))
update_version(version)
make_git_commit('Bump version number to %s', version)
make_git_tag(version)
build_and_upload()
if __name__ == '__main__':
main()
+6
View File
@@ -0,0 +1,6 @@
[build_sphinx]
source-dir = docs/
build-dir = docs/_build
[upload_sphinx]
upload-dir = docs/_build/html
+23 -18
View File
@@ -1,50 +1,55 @@
""" """
Flask-Security Flask-Security
-------------- ==============
Simple security for Flask apps Flask-Security is a Flask extension that aims to add quick and simple security
to your Flask applications.
Links Resources
````` ---------
* `development version * `Documentation <http://packages.python.org/Flask-Security/>`_
* `Issue Tracker <https://github.com/mattupstate/flask-security/issues>`_
* `Source <https://github.com/mattupstate/flask-security>`_
* `Development Version
<https://github.com/mattupstate/flask-security/raw/develop#egg=Flask-Security-dev>`_ <https://github.com/mattupstate/flask-security/raw/develop#egg=Flask-Security-dev>`_
""" """
from setuptools import setup from setuptools import setup
setup( setup(
name='Flask-Security', name='Flask-Security',
version='1.1.0', version='1.6.2',
url='https://github.com/mattupstate/flask-security', url='https://github.com/mattupstate/flask-security',
license='MIT', license='MIT',
author='Matthew Wright', author='Matt Wright',
author_email='matt@nobien.net', author_email='matt@nobien.net',
description='Simple security for Flask apps', description='Simple security for Flask apps',
long_description=__doc__, long_description=__doc__,
packages=[ packages=[
'flask_security', 'flask_security'
'flask_security.datastore'
], ],
zip_safe=False, zip_safe=False,
include_package_data=True, include_package_data=True,
platforms='any', platforms='any',
install_requires=[ install_requires=[
'Flask', 'Flask>=0.9',
'Flask-Login', 'Flask-Login>=0.1.3',
'Flask-Principal', 'Flask-Mail>=0.7.3',
'Flask-WTF', 'Flask-Principal>=0.3.3',
'passlib' 'Flask-WTF>=0.8',
'itsdangerous>=0.17',
'passlib>=1.6.1',
], ],
test_suite='nose.collector', test_suite='nose.collector',
tests_require=[ tests_require=[
'nose', 'nose',
'Flask-SQLAlchemy', 'Flask-SQLAlchemy',
'Flask-MongoEngine', 'Flask-MongoEngine',
'py-bcrypt' 'Flask-Peewee',
], 'py-bcrypt',
dependency_links=[ 'simplejson'
'http://github.com/sbook/flask-mongoengine/tarball/master#egg=Flask-MongoEngine-0.1.3-dev'
], ],
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
+95
View File
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
import hmac
from hashlib import sha1
from unittest import TestCase
from tests.test_app.sqlalchemy import create_app
class SecurityTest(TestCase):
APP_KWARGS = {
'register_blueprint': True,
}
AUTH_CONFIG = None
def setUp(self):
super(SecurityTest, self).setUp()
app_kwargs = self.APP_KWARGS
app = self._create_app(self.AUTH_CONFIG or {}, **app_kwargs)
app.debug = False
app.config['TESTING'] = True
self.app = app
self.client = app.test_client()
with self.client.session_transaction() as session:
session['csrf'] = 'csrf_token'
csrf_hmac = hmac.new(self.app.config['SECRET_KEY'],
'csrf_token'.encode('utf8'), digestmod=sha1)
self.csrf_token = '##' + csrf_hmac.hexdigest()
def _create_app(self, auth_config, **kwargs):
return create_app(auth_config, **kwargs)
def _get(self, route, content_type=None, follow_redirects=None, headers=None):
return self.client.get(route, follow_redirects=follow_redirects,
content_type=content_type or 'text/html',
headers=headers)
def _post(self, route, data=None, content_type=None, follow_redirects=True, headers=None):
if isinstance(data, dict):
data['csrf_token'] = self.csrf_token
return self.client.post(route, data=data,
follow_redirects=follow_redirects,
content_type=content_type or 'application/x-www-form-urlencoded',
headers=headers)
def register(self, email, password='password'):
data = dict(email=email, password=password, csrf_token=self.csrf_token)
return self.client.post('/register', data=data, follow_redirects=True)
def authenticate(self, email="matt@lp.com", password="password", endpoint=None, **kwargs):
data = dict(email=email, password=password, remember='y')
return self._post(endpoint or '/login', data=data, **kwargs)
def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None):
data = """{
"email": "%s",
"password": "%s",
"csrf_token": "%s"
}"""
return self._post(endpoint or '/login', content_type="application/json",
data=data % (email, password, self.csrf_token))
def logout(self, endpoint=None):
return self._get(endpoint or '/logout', follow_redirects=True)
def assertIsHomePage(self, data):
self.assertIn('Home Page', data)
def assertIn(self, member, container, msg=None):
if hasattr(TestCase, 'assertIn'):
return TestCase.assertIn(self, member, container, msg)
return self.assertTrue(member in container)
def assertNotIn(self, member, container, msg=None):
if hasattr(TestCase, 'assertNotIn'):
return TestCase.assertNotIn(self, member, container, msg)
return self.assertFalse(member in container)
def assertIsNotNone(self, obj, msg=None):
if hasattr(TestCase, 'assertIsNotNone'):
return TestCase.assertIsNotNone(self, obj, msg)
return self.assertTrue(obj is not None)
def get_message(self, key, **kwargs):
return self.app.config['SECURITY_MSG_' + key][0] % kwargs
+773
View File
@@ -0,0 +1,773 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
import base64
import time
import simplejson as json
from flask.ext.security.utils import capture_registrations, \
capture_reset_password_requests, capture_passwordless_login_requests
from flask.ext.security.forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
from flask.ext.security.forms import TextField, SubmitField, valid_user_email
from tests import SecurityTest
class ConfiguredPasswordHashSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SALT': 'so-salty',
'USER_COUNT': 1
}
def test_authenticate(self):
r = self.authenticate(endpoint="/login")
self.assertIn('Home Page', r.data)
class ConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'SECURITY_LOGOUT_URL': '/custom_logout',
'SECURITY_LOGIN_URL': '/custom_login',
'SECURITY_POST_LOGIN_VIEW': '/post_login',
'SECURITY_POST_LOGOUT_VIEW': '/post_logout',
'SECURITY_POST_REGISTER_VIEW': '/post_register',
'SECURITY_UNAUTHORIZED_VIEW': '/unauthorized',
'SECURITY_DEFAULT_HTTP_AUTH_REALM': 'Custom Realm'
}
def test_login_view(self):
r = self._get('/custom_login')
self.assertIn("<h1>Login</h1>", r.data)
def test_authenticate(self):
r = self.authenticate(endpoint="/custom_login")
self.assertIn('Post Login', r.data)
def test_logout(self):
self.authenticate(endpoint="/custom_login")
r = self.logout(endpoint="/custom_logout")
self.assertIn('Post Logout', r.data)
def test_register_view(self):
r = self._get('/register')
self.assertIn('<h1>Register</h1>', r.data)
def test_register(self):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn('Post Register', r.data)
def test_register_json(self):
data = '{ "email": "dude@lp.com", "password": "password", "csrf_token":"%s" }' % self.csrf_token
r = self._post('/register', data=data, content_type='application/json')
data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200)
def test_register_existing_email(self):
data = dict(email='matt@lp.com',
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
msg = 'matt@lp.com is already associated with an account'
self.assertIn(msg, r.data)
def test_unauthorized(self):
self.authenticate("joe@lp.com", endpoint="/custom_auth")
r = self._get("/admin", follow_redirects=True)
msg = 'You are not allowed to access the requested resouce'
self.assertIn(msg, r.data)
def test_default_http_auth_realm(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Custom Realm"',
r.headers['WWW-Authenticate'])
class BadConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORD_HASH': 'bcrypt',
'USER_COUNT': 1
}
def test_bad_configuration_raises_runtimer_error(self):
self.assertRaises(RuntimeError, self.authenticate)
class DefaultTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_LOGIN_USER_TEMPLATE': 'custom_security/login_user.html',
}
def test_login_user_template(self):
r = self._get('/login')
self.assertIn('CUSTOM LOGIN USER', r.data)
class RegisterableTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'SECURITY_REGISTER_USER_TEMPLATE': 'custom_security/register_user.html'
}
def test_register_user_template(self):
r = self._get('/register')
self.assertIn('CUSTOM REGISTER USER', r.data)
class RecoverableTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_FORGOT_PASSWORD_TEMPLATE': 'custom_security/forgot_password.html',
'SECURITY_RESET_PASSWORD_TEMPLATE': 'custom_security/reset_password.html',
}
def test_forgot_password_template(self):
r = self._get('/reset')
self.assertIn('CUSTOM FORGOT PASSWORD', r.data)
def test_reset_password_template(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._get('/reset/' + t)
self.assertIn('CUSTOM RESET PASSWORD', r.data)
class ConfirmableTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_SEND_CONFIRMATION_TEMPLATE': 'custom_security/send_confirmation.html'
}
def test_send_confirmation_template(self):
r = self._get('/confirm')
self.assertIn('CUSTOM SEND CONFIRMATION', r.data)
class PasswordlessTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
'SECURITY_SEND_LOGIN_TEMPLATE': 'custom_security/send_login.html'
}
def test_send_login_template(self):
r = self._get('/login')
self.assertIn('CUSTOM SEND LOGIN', r.data)
class RegisterableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'USER_COUNT': 1
}
def test_register_valid_user(self):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
self._post('/register', data=data, follow_redirects=True)
r = self.authenticate('dude@lp.com')
self.assertIn('Hello dude@lp.com', r.data)
class ConfirmableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_EMAIL_SUBJECT_REGISTER': 'Custom welcome subject',
'USER_COUNT': 1
}
def test_login_before_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self.authenticate(email=e)
self.assertIn(self.get_message('CONFIRMATION_REQUIRED'), r.data)
def test_send_confirmation_of_already_confirmed_account(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
self.client.get('/confirm/' + token, follow_redirects=True)
self.logout()
r = self._post('/confirm', data=dict(email=e))
self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data)
def test_register_sends_confirmation_email(self):
e = 'dude@lp.com'
with self.app.extensions['mail'].record_messages() as outbox:
self.register(e)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
self.assertEqual('Custom welcome subject', outbox[0].subject)
def test_confirm_email(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
r = self.client.get('/confirm/' + token, follow_redirects=True)
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
self.assertIn(msg, r.data)
def test_invalid_token_when_confirming_email(self):
r = self.client.get('/confirm/bogus', follow_redirects=True)
msg = self.app.config['SECURITY_MSG_INVALID_CONFIRMATION_TOKEN'][0]
self.assertIn(msg, r.data)
def test_send_confirmation_json(self):
r = self._post('/confirm', data='{"email": "matt@lp.com"}',
content_type='application/json')
self.assertEquals(r.status_code, 200)
def test_send_confirmation_with_invalid_email(self):
r = self._post('/confirm', data=dict(email='bogus@bogus.com'))
msg = self.app.config['SECURITY_MSG_USER_DOES_NOT_EXIST'][0]
self.assertIn(msg, r.data)
def test_resend_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self._post('/confirm', data={'email': e})
msg = self.get_message('CONFIRMATION_REQUEST', email=e)
self.assertIn(msg, r.data)
def test_user_deleted_before_confirmation(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
user = registrations[0]['user']
token = registrations[0]['confirm_token']
with self.app.app_context():
from flask_security.core import _security
_security.datastore.delete(user)
_security.datastore.commit()
r = self.client.get('/confirm/' + token, follow_redirects=True)
msg = self.app.config['SECURITY_MSG_INVALID_CONFIRMATION_TOKEN'][0]
self.assertIn(msg, r.data)
class ExpiredConfirmationTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_CONFIRM_EMAIL_WITHIN': '1 milliseconds',
'USER_COUNT': 1
}
def test_expired_confirmation_token_sends_email(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
time.sleep(1.25)
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.get('/confirm/' + token, follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertNotIn(token, outbox[0].html)
expire_text = self.AUTH_CONFIG['SECURITY_CONFIRM_EMAIL_WITHIN']
msg = self.app.config['SECURITY_MSG_CONFIRMATION_EXPIRED'][0]
msg = msg % dict(within=expire_text, email=e)
self.assertIn(msg, r.data)
class LoginWithoutImmediateConfirmTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True,
'USER_COUNT': 1
}
def test_register_valid_user_automatically_signs_in(self):
e = 'dude@lp.com'
p = 'password'
data = dict(email=e, password=p, password_confirm=p)
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn(e, r.data)
class RecoverableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
'SECURITY_POST_FORGOT_VIEW': '/'
}
def test_reset_view(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._get('/reset/' + t)
self.assertIn('<h1>Reset password</h1>', r.data)
def test_forgot_post_sends_email(self):
with capture_reset_password_requests():
with self.app.extensions['mail'].record_messages() as outbox:
self._post('/reset', data=dict(email='joe@lp.com'))
self.assertEqual(len(outbox), 1)
def test_forgot_password_json(self):
r = self._post('/reset', data='{"email": "matt@lp.com"}',
content_type="application/json")
self.assertEquals(r.status_code, 200)
def test_forgot_password_invalid_email(self):
r = self._post('/reset',
data=dict(email='larry@lp.com'),
follow_redirects=True)
self.assertIn("Specified user does not exist", r.data)
def test_reset_password_with_valid_token(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
r = self.logout()
r = self.authenticate('joe@lp.com', 'newpassword')
self.assertIn('Hello joe@lp.com', r.data)
def test_reset_password_with_invalid_token(self):
r = self._post('/reset/bogus', data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn(self.get_message('INVALID_RESET_PASSWORD_TOKEN'), r.data)
class ExpiredResetPasswordTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_WITHIN': '1 milliseconds'
}
def test_reset_password_with_expired_token(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
time.sleep(1)
r = self._post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('You did not reset your password within', r.data)
class ChangePasswordTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_CHANGEABLE': True,
}
def test_change_password(self):
self.authenticate()
r = self.client.get('/change', follow_redirects=True)
self.assertIn('Change password', r.data)
def test_change_password_invalid(self):
self.authenticate()
r = self._post('/change', data={
'password': 'notpassword',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertNotIn('You successfully changed your password', r.data)
self.assertIn('Invalid password', r.data)
def test_change_password_mismatch(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'notnewpassword'
}, follow_redirects=True)
self.assertNotIn('You successfully changed your password', r.data)
self.assertIn('Passwords do not match', r.data)
def test_change_password_bad_password(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'a',
'new_password_confirm': 'a'
}, follow_redirects=True)
self.assertNotIn('You successfully changed your password', r.data)
self.assertIn('Field must be between', r.data)
def test_change_password_success(self):
self.authenticate()
with self.app.extensions['mail'].record_messages() as outbox:
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('You successfully changed your password', r.data)
self.assertIn('Home Page', r.data)
self.assertEqual(len(outbox), 1)
self.assertIn("Your password has been changed", outbox[0].html)
self.assertIn("/reset", outbox[0].html)
class ChangePasswordPostViewTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': True,
'SECURITY_POST_CHANGE_VIEW': '/profile',
}
def test_change_password_success(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('Profile Page', r.data)
class ChangePasswordDisabledTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': False,
}
def test_change_password_endpoint_is_404(self):
self.authenticate()
r = self.client.get('/change', follow_redirects=True)
self.assertEqual(404, r.status_code)
class TrackableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_TRACKABLE': True,
'USER_COUNT': 1
}
def test_did_track(self):
e = 'matt@lp.com'
self.authenticate(email=e)
self.logout()
self.authenticate(email=e)
with self.app.test_request_context('/profile'):
user = self.app.security.datastore.find_user(email=e)
self.assertIsNotNone(user.last_login_at)
self.assertIsNotNone(user.current_login_at)
self.assertEquals('untrackable', user.last_login_ip)
self.assertEquals('untrackable', user.current_login_ip)
self.assertEquals(2, user.login_count)
class PasswordlessTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True
}
def test_login_request_for_inactive_user(self):
msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0]
r = self._post('/login', data=dict(email='tiya@lp.com'),
follow_redirects=True)
self.assertIn(msg, r.data)
def test_request_login_token_with_json_and_valid_email(self):
data = '{"email": "matt@lp.com", "password": "password", "csrf_token":"%s"}' % self.csrf_token
r = self._post('/login', data=data, content_type='application/json')
self.assertEquals(r.status_code, 200)
self.assertNotIn('error', r.data)
def test_request_login_token_with_json_and_invalid_email(self):
data = '{"email": "nobody@lp.com", "password": "password"}'
r = self._post('/login', data=data, content_type='application/json')
self.assertIn('errors', r.data)
def test_request_login_token_sends_email_and_can_login(self):
e = 'matt@lp.com'
r, user, token = None, None, None
with capture_passwordless_login_requests() as requests:
with self.app.extensions['mail'].record_messages() as outbox:
r = self._post('/login', data=dict(email=e),
follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertEquals(1, len(requests))
self.assertIn('user', requests[0])
self.assertIn('login_token', requests[0])
user = requests[0]['user']
token = requests[0]['login_token']
msg = self.app.config['SECURITY_MSG_LOGIN_EMAIL_SENT'][0]
msg = msg % dict(email=user.email)
self.assertIn(msg, r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertIn(msg, r.data)
r = self.client.get('/profile')
self.assertIn('Profile Page', r.data)
def test_invalid_login_token(self):
msg = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0]
r = self._get('/login/bogus', follow_redirects=True)
self.assertIn(msg, r.data)
def test_token_login_when_already_authenticated(self):
with capture_passwordless_login_requests() as requests:
self._post('/login', data=dict(email='matt@lp.com'),
follow_redirects=True)
token = requests[0]['login_token']
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertIn(msg, r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertNotIn(msg, r.data)
def test_send_login_with_invalid_email(self):
r = self._post('/login', data=dict(email='bogus@bogus.com'))
self.assertIn('Specified user does not exist', r.data)
class ExpiredLoginTokenTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
'SECURITY_LOGIN_WITHIN': '1 milliseconds',
'USER_COUNT': 1
}
def test_expired_login_token_sends_email(self):
e = 'matt@lp.com'
with capture_passwordless_login_requests() as requests:
self._post('/login', data=dict(email=e), follow_redirects=True)
token = requests[0]['login_token']
time.sleep(1.25)
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.get('/login/' + token, follow_redirects=True)
expire_text = self.AUTH_CONFIG['SECURITY_LOGIN_WITHIN']
msg = self.app.config['SECURITY_MSG_LOGIN_EXPIRED'][0]
msg = msg % dict(within=expire_text, email=e)
self.assertIn(msg, r.data)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
self.assertNotIn(token, outbox[0].html)
class AsyncMailTaskTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'USER_COUNT': 1
}
def setUp(self):
super(AsyncMailTaskTests, self).setUp()
self.mail_sent = False
def test_send_email_task_is_called(self):
@self.app.security.send_mail_task
def send_email(msg):
self.mail_sent = True
self._post('/reset', data=dict(email='matt@lp.com'))
self.assertTrue(self.mail_sent)
class NoBlueprintTests(SecurityTest):
APP_KWARGS = {
'register_blueprint': False,
}
AUTH_CONFIG = {
'USER_COUNT': 1
}
def test_login_endpoint_is_404(self):
r = self._get('/login')
self.assertEqual(404, r.status_code)
def test_http_auth_without_blueprint(self):
auth = 'Basic ' + base64.b64encode("matt@lp.com:password")
r = self._get('/http', headers={'Authorization': auth})
self.assertIn('HTTP Authentication', r.data)
class ExtendFormsTest(SecurityTest):
class MyLoginForm(LoginForm):
email = TextField('My Login Email Address Field')
class MyRegisterForm(RegisterForm):
email = TextField('My Register Email Address Field')
APP_KWARGS = {
'login_form': MyLoginForm,
'register_form': MyRegisterForm,
}
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': False,
'SECURITY_REGISTERABLE': True,
}
def test_login_view(self):
r = self._get('/login', follow_redirects=True)
self.assertIn("My Login Email Address Field", r.data)
def test_register(self):
r = self._get('/register', follow_redirects=True)
self.assertIn("My Register Email Address Field", r.data)
class RecoverableExtendFormsTest(SecurityTest):
class MyForgotPasswordForm(ForgotPasswordForm):
email = TextField('My Forgot Password Email Address Field',
validators=[valid_user_email])
class MyResetPasswordForm(ResetPasswordForm):
submit = SubmitField("My Reset Password Submit Field")
APP_KWARGS = {
'forgot_password_form': MyForgotPasswordForm,
'reset_password_form': MyResetPasswordForm,
}
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
}
def test_forgot_password(self):
r = self._get('/reset', follow_redirects=True)
self.assertIn("My Forgot Password Email Address Field", r.data)
def test_reset_password(self):
with capture_reset_password_requests() as requests:
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
token = requests[0]['token']
r = self._get('/reset/' + token)
self.assertIn("My Reset Password Submit Field", r.data)
class PasswordlessExtendFormsTest(SecurityTest):
class MyPasswordlessLoginForm(PasswordlessLoginForm):
email = TextField('My Passwordless Login Email Address Field')
APP_KWARGS = {
'passwordless_login_form': MyPasswordlessLoginForm,
}
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
}
def test_passwordless_login(self):
r = self._get('/login', follow_redirects=True)
self.assertIn("My Passwordless Login Email Address Field", r.data)
class ConfirmableExtendFormsTest(SecurityTest):
class MyConfirmRegisterForm(ConfirmRegisterForm):
email = TextField('My Confirm Register Email Address Field')
class MySendConfirmationForm(SendConfirmationForm):
email = TextField('My Send Confirmation Email Address Field')
APP_KWARGS = {
'confirm_register_form': MyConfirmRegisterForm,
'send_confirmation_form': MySendConfirmationForm,
}
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
}
def test_register(self):
r = self._get('/register', follow_redirects=True)
self.assertIn("My Confirm Register Email Address Field", r.data)
def test_send_confirmation(self):
r = self._get('/confirm', follow_redirects=True)
self.assertIn("My Send Confirmation Email Address Field", r.data)
+239 -104
View File
@@ -1,133 +1,268 @@
import unittest # -*- coding: utf-8 -*-
from example import app
from __future__ import with_statement
import base64
import simplejson as json
from cookielib import Cookie
from werkzeug.utils import parse_cookie
from tests import SecurityTest
def get_cookies(rv):
cookies = {}
for value in rv.headers.get_all("Set-Cookie"):
cookies.update(parse_cookie(value))
return cookies
class SecurityTest(unittest.TestCase):
AUTH_CONFIG = None
def setUp(self):
super(SecurityTest, self).setUp()
self.app = self._create_app(self.AUTH_CONFIG or None)
self.app.debug = False
self.app.config['TESTING'] = True
self.client = self.app.test_client()
def _create_app(self, auth_config):
return app.create_sqlalchemy_app(auth_config)
def _get(self, route, content_type=None, follow_redirects=None):
return self.client.get(route, follow_redirects=follow_redirects,
content_type=content_type or 'text/html')
def _post(self, route, data=None, content_type=None, follow_redirects=True):
return self.client.post(route, data=data,
follow_redirects=follow_redirects,
content_type=content_type or 'text/html')
def authenticate(self, username, password, endpoint=None):
data = dict(username=username, password=password)
return self._post(endpoint or '/auth', data=data,
content_type='application/x-www-form-urlencoded')
def logout(self, endpoint=None):
return self._get(endpoint or '/logout', follow_redirects=True)
class DefaultSecurityTests(SecurityTest): class DefaultSecurityTests(SecurityTest):
def test_instance(self):
self.assertIsNotNone(self.app)
self.assertIsNotNone(self.app.security)
self.assertIsNotNone(self.app.security.pwd_context)
def test_login_view(self): def test_login_view(self):
r = self._get('/login') r = self._get('/login')
assert 'Login Page' in r.data self.assertIn('<h1>Login</h1>', r.data)
def test_authenticate(self): def test_authenticate(self):
r = self.authenticate("matt", "password") r = self.authenticate()
assert 'Home Page' in r.data self.assertIn('Hello matt@lp.com', r.data)
def test_unprovided_username(self): def test_unprovided_username(self):
r = self.authenticate("", "password") r = self.authenticate("")
assert "Username not provided" in r.data self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data)
def test_unprovided_password(self): def test_unprovided_password(self):
r = self.authenticate("matt", "") r = self.authenticate(password="")
assert "Password not provided" in r.data self.assertIn(self.get_message('PASSWORD_NOT_PROVIDED'), r.data)
def test_invalid_user(self): def test_invalid_user(self):
r = self.authenticate("bogus", "password") r = self.authenticate(email="bogus@bogus.com")
assert "Specified user does not exist" in r.data self.assertIn(self.get_message('USER_DOES_NOT_EXIST'), r.data)
def test_bad_password(self): def test_bad_password(self):
r = self.authenticate("matt", "bogus") r = self.authenticate(password="bogus")
assert "Password does not match" in r.data self.assertIn(self.get_message('INVALID_PASSWORD'), r.data)
def test_inactive_user(self): def test_inactive_user(self):
r = self.authenticate("tiya", "password") r = self.authenticate("tiya@lp.com", "password")
assert "Inactive user" in r.data self.assertIn(self.get_message('DISABLED_ACCOUNT'), r.data)
def test_logout(self): def test_logout(self):
self.authenticate("matt", "password") self.authenticate()
r = self.logout() r = self.logout()
assert 'Home Page' in r.data self.assertIsHomePage(r.data)
def test_unauthorized_access(self): def test_unauthorized_access(self):
r = self._get('/profile', follow_redirects=True) r = self._get('/profile', follow_redirects=True)
assert 'Please log in to access this page' in r.data self.assertIn('<li class="message">Please log in to access this page.</li>', r.data)
def test_authorized_access(self): def test_authorized_access(self):
self.authenticate("matt", "password") self.authenticate()
r = self._get("/profile") r = self._get("/profile")
assert 'profile' in r.data self.assertIn('profile', r.data)
def test_valid_admin_role(self): def test_valid_admin_role(self):
self.authenticate("matt", "password") self.authenticate()
r = self._get("/admin") r = self._get("/admin")
assert 'Admin Page' in r.data self.assertIn('Admin Page', r.data)
def test_invalid_admin_role(self): def test_invalid_admin_role(self):
self.authenticate("joe", "password") self.authenticate("joe@lp.com")
r = self._get("/admin", follow_redirects=True) r = self._get("/admin", follow_redirects=True)
assert 'Home Page' in r.data self.assertIsHomePage(r.data)
def test_roles_accepted(self): def test_roles_accepted(self):
for user in ("matt", "joe"): for user in ("matt@lp.com", "joe@lp.com"):
self.authenticate(user, "password") self.authenticate(user)
r = self._get("/admin_or_editor") r = self._get("/admin_or_editor")
self.assertIn('Admin or Editor Page', r.data) self.assertIn('Admin or Editor Page', r.data)
self.logout() self.logout()
self.authenticate("jill", "password") self.authenticate("jill@lp.com")
r = self._get("/admin_or_editor", follow_redirects=True) r = self._get("/admin_or_editor", follow_redirects=True)
self.assertIn('Home Page', r.data) self.assertIsHomePage(r.data)
def test_unauthenticated_role_required(self):
r = self._get('/admin', follow_redirects=True)
self.assertIn(self.get_message('UNAUTHORIZED'), r.data)
def test_multiple_role_required(self):
for user in ("matt@lp.com", "joe@lp.com"):
self.authenticate(user)
r = self._get("/admin_and_editor", follow_redirects=True)
self.assertIsHomePage(r.data)
self._get('/logout')
self.authenticate('dave@lp.com')
r = self._get("/admin_and_editor", follow_redirects=True)
self.assertIn('Admin and Editor Page', r.data)
def test_ok_json_auth(self):
r = self.json_authenticate()
data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200)
self.assertIn('authentication_token', data['response']['user'])
def test_invalid_json_auth(self):
r = self.json_authenticate(password='junk')
self.assertIn('"code": 400', r.data)
def test_token_auth_via_querystring_valid_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
r = self._get('/token?auth_token=' + token)
self.assertIn('Token Authentication', r.data)
def test_token_auth_via_header_valid_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
headers = {"Authentication-Token": token}
r = self._get('/token', headers=headers)
self.assertIn('Token Authentication', r.data)
def test_token_auth_via_querystring_invalid_token(self):
r = self._get('/token?auth_token=X')
self.assertEqual(401, r.status_code)
def test_token_auth_via_header_invalid_token(self):
r = self._get('/token', headers={"Authentication-Token": 'X'})
self.assertEqual(401, r.status_code)
def test_http_auth(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password")
})
self.assertIn('HTTP Authentication', r.data)
def test_http_auth_no_authorization(self):
r = self._get('/http', headers={})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Login Required"',
r.headers['WWW-Authenticate'])
def test_invalid_http_auth_invalid_username(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("bogus:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Login Required"',
r.headers['WWW-Authenticate'])
def test_invalid_http_auth_bad_password(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Login Required"',
r.headers['WWW-Authenticate'])
def test_custom_http_auth_realm(self):
r = self._get('/http_custom_realm', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="My Realm"',
r.headers['WWW-Authenticate'])
def test_multi_auth_basic(self):
r = self._get('/multi_auth', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password")
})
self.assertIn('Basic', r.data)
def test_multi_auth_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
r = self._get('/multi_auth?auth_token=' + token)
self.assertIn('Token', r.data)
def test_multi_auth_session(self):
self.authenticate()
r = self._get('/multi_auth')
self.assertIn('Session', r.data)
def test_user_deleted_during_session_reverts_to_anonymous_user(self):
self.authenticate()
with self.app.test_request_context('/'):
user = self.app.security.datastore.find_user(email='matt@lp.com')
self.app.security.datastore.delete_user(user)
self.app.security.datastore.commit()
r = self._get('/')
self.assertNotIn('Hello matt@lp.com', r.data)
def test_remember_token(self):
r = self.authenticate(follow_redirects=False)
self.client.cookie_jar.clear_session_cookies()
r = self._get('/profile')
self.assertIn('profile', r.data)
def test_token_loader_does_not_fail_with_invalid_token(self):
c = Cookie(version=0, name='remember_token', value='None', port=None,
port_specified=False, domain='www.example.com',
domain_specified=False, domain_initial_dot=False, path='/',
path_specified=True, secure=False, expires=None,
discard=True, comment=None, comment_url=None,
rest={'HttpOnly': None}, rfc2109=False)
self.client.cookie_jar.set_cookie(c)
r = self._get('/')
self.assertNotIn('BadSignature', r.data)
class ConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_USER_DATASTORE': 'custom_datastore_name',
'SECURITY_AUTH_URL': '/custom_auth',
'SECURITY_LOGOUT_URL': '/custom_logout',
'SECURITY_LOGIN_VIEW': '/custom_login',
'SECURITY_POST_LOGIN': '/post_login',
'SECURITY_POST_LOGOUT': '/post_logout'
}
def test_login_view(self):
r = self._get('/custom_login')
assert "Custom Login Page" in r.data
def test_authenticate(self):
r = self.authenticate("matt", "password", endpoint="/custom_auth")
assert 'Post Login' in r.data
def test_logout(self):
self.authenticate("matt", "password", endpoint="/custom_auth")
r = self.logout(endpoint="/custom_logout")
assert 'Post Logout' in r.data
class MongoEngineSecurityTests(DefaultSecurityTests): class MongoEngineSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config): def _create_app(self, auth_config, **kwargs):
return app.create_mongoengine_app(auth_config) from tests.test_app.mongoengine import create_app
return create_app(auth_config, **kwargs)
# class PeeweeSecurityTests(DefaultSecurityTests):
# def _create_app(self, auth_config, **kwargs):
# from tests.test_app.peewee_app import create_app
# return create_app(auth_config, **kwargs)
class DefaultDatastoreTests(SecurityTest):
def test_add_role_to_user(self):
r = self._get('/coverage/add_role_to_user')
self.assertIn('success', r.data)
def test_remove_role_from_user(self):
r = self._get('/coverage/remove_role_from_user')
self.assertIn('success', r.data)
def test_activate_user(self):
r = self._get('/coverage/activate_user')
self.assertIn('success', r.data)
def test_deactivate_user(self):
r = self._get('/coverage/deactivate_user')
self.assertIn('success', r.data)
def test_invalid_role(self):
r = self._get('/coverage/invalid_role')
self.assertIn('success', r.data)
class MongoEngineDatastoreTests(DefaultDatastoreTests):
def _create_app(self, auth_config, **kwargs):
from tests.test_app.mongoengine import create_app
return create_app(auth_config, **kwargs)
+238
View File
@@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
from flask_security.utils import capture_registrations, \
capture_reset_password_requests, capture_signals
from flask_security.signals import user_registered, user_confirmed, \
confirm_instructions_sent, login_instructions_sent, \
password_reset, password_changed, reset_password_instructions_sent
from tests import SecurityTest
def compare_user(a, b):
"""Helper to compare two users."""
return a.id == b.id and a.email == b.email and a.password == b.password
class RegisterableSignalsTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
}
def test_register(self):
e = 'dude@lp.com'
with capture_signals() as mocks:
self.register(e)
user = self.app.security.datastore.find_user(email='dude@lp.com')
self.assertEqual(mocks.signals_sent(), set([user_registered]))
calls = mocks[user_registered]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('confirm_token', kwargs)
self.assertEqual(args[0], self.app)
def test_register_without_password(self):
e = 'dude@lp.com'
with capture_signals() as mocks:
self.register(e, password='')
self.assertEqual(mocks.signals_sent(), set())
class ConfirmableSignalsTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
}
def test_confirm(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
with capture_signals() as mocks:
self.client.get('/confirm/' + token, follow_redirects=True)
user = self.app.security.datastore.find_user(email='dude@lp.com')
self.assertTrue(mocks.signals_sent(), set([user_confirmed]))
calls = mocks[user_confirmed]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertEqual(args[0], self.app)
self.assertTrue(compare_user(kwargs['user'], user))
def test_confirm_bad_token(self):
e = 'dude@lp.com'
with capture_registrations():
self.register(e)
with capture_signals() as mocks:
self.client.get('/confirm/bogus', follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_confirm_twice(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
self.client.get('/confirm/' + token, follow_redirects=True)
self.logout()
with capture_signals() as mocks:
self.client.get('/confirm/' + token, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([user_confirmed]))
# TODO: is that the desired behaviour?
def test_resend_confirmation(self):
e = 'dude@lp.com'
self.register(e)
with capture_signals() as mocks:
self._post('/confirm', data={'email': e})
user = self.app.security.datastore.find_user(email='dude@lp.com')
self.assertEqual(mocks.signals_sent(), set([confirm_instructions_sent]))
calls = mocks[confirm_instructions_sent]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertEqual(args[0], self.app)
def test_send_confirmation_bad_email(self):
with capture_signals() as mocks:
self._post('/confirm', data=dict(email='bogus@bogus.com'))
self.assertEqual(mocks.signals_sent(), set())
class RecoverableSignalsTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
'SECURITY_POST_FORGOT_VIEW': '/'
}
def test_reset_password_request(self):
with capture_signals() as mocks:
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([reset_password_instructions_sent]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[reset_password_instructions_sent]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('token', kwargs)
self.assertEqual(args[0], self.app)
def test_reset_password(self):
with capture_reset_password_requests() as requests:
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
token = requests[0]['token']
with capture_signals() as mocks:
data = dict(password='newpassword', password_confirm='newpassword')
self._post('/reset/' + token, data, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([password_reset]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[password_reset]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertEqual(args[0], self.app)
def test_reset_password_invalid_emails(self):
with capture_signals() as mocks:
self._post('/reset', data=dict(email='nobody@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_reset_password_invalid_token(self):
with capture_signals() as mocks:
data = dict(password='newpassword', password_confirm='newpassword')
self._post('/reset/bogus', data, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
class ChangeableSignalsTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': True,
}
def test_change_password(self):
self.authenticate('joe@lp.com')
with capture_signals() as mocks:
with self.client as client:
client.post('/change',
data=dict(password='password',
new_password='newpassword',
new_password_confirm='newpassword',
csrf_token=self.csrf_token))
self.assertEqual(mocks.signals_sent(), set([password_changed]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[password_changed]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(args[0], user))
self.assertEqual(kwargs['app'], self.app)
def test_change_password_invalid_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='notpassword',
new_password='newpassword',
new_password_confirm='newpassword'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_change_password_bad_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='notpassword',
new_password='a',
new_password_confirm='a'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_change_password_mismatch_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='password',
new_password='newpassword',
new_password_confirm='notnewpassword'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
class PasswordlessTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True
}
def test_login_request_for_inactive_user(self):
with capture_signals() as mocks:
self._post('/login', data=dict(email='tiya@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_login_request_for_invalid_email(self):
with capture_signals() as mocks:
self._post('/login', data=dict(email='nobody@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_request_login_token_sends_email_and_can_login(self):
e = 'matt@lp.com'
with capture_signals() as mocks:
self._post('/login', data=dict(email=e), follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([login_instructions_sent]))
user = self.app.security.datastore.find_user(email='matt@lp.com')
calls = mocks[login_instructions_sent]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('login_token', kwargs)
self.assertEqual(args[0], self.app)
+176
View File
@@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
from flask import Flask, render_template, current_app
from flask.ext.mail import Mail
from flask.ext.security import login_required, roles_required, roles_accepted
from flask.ext.security.decorators import http_auth_required, \
auth_token_required, auth_required
from flask.ext.security.utils import encrypt_password
from werkzeug.local import LocalProxy
ds = LocalProxy(lambda: current_app.extensions['security'].datastore)
def create_app(config):
app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'secret'
app.config['TESTING'] = True
for key, value in config.items():
app.config[key] = value
mail = Mail(app)
app.extensions['mail'] = mail
@app.route('/')
def index():
return render_template('index.html', content='Home Page')
@app.route('/profile')
@login_required
def profile():
return render_template('index.html', content='Profile Page')
@app.route('/post_login')
@login_required
def post_login():
return render_template('index.html', content='Post Login')
@app.route('/http')
@http_auth_required
def http():
return 'HTTP Authentication'
@app.route('/http_custom_realm')
@http_auth_required('My Realm')
def http_custom_realm():
return render_template('index.html', content='HTTP Authentication')
@app.route('/token')
@auth_token_required
def token():
return render_template('index.html', content='Token Authentication')
@app.route('/multi_auth')
@auth_required('session', 'token', 'basic')
def multi_auth():
return render_template('index.html', content='Session, Token, Basic auth')
@app.route('/post_logout')
def post_logout():
return render_template('index.html', content='Post Logout')
@app.route('/post_register')
def post_register():
return render_template('index.html', content='Post Register')
@app.route('/admin')
@roles_required('admin')
def admin():
return render_template('index.html', content='Admin Page')
@app.route('/admin_and_editor')
@roles_required('admin', 'editor')
def admin_and_editor():
return render_template('index.html', content='Admin and Editor Page')
@app.route('/admin_or_editor')
@roles_accepted('admin', 'editor')
def admin_or_editor():
return render_template('index.html', content='Admin or Editor Page')
@app.route('/unauthorized')
def unauthorized():
return render_template('unauthorized.html')
@app.route('/coverage/add_role_to_user')
def add_role_to_user():
u = ds.find_user(email='joe@lp.com')
r = ds.find_role('admin')
ds.add_role_to_user(u, r)
return 'success'
@app.route('/coverage/remove_role_from_user')
def remove_role_from_user():
u = ds.find_user(email='matt@lp.com')
ds.remove_role_from_user(u, 'admin')
return 'success'
@app.route('/coverage/deactivate_user')
def deactivate_user():
u = ds.find_user(email='matt@lp.com')
ds.deactivate_user(u)
return 'success'
@app.route('/coverage/activate_user')
def activate_user():
u = ds.find_user(email='tiya@lp.com')
ds.activate_user(u)
return 'success'
@app.route('/coverage/invalid_role')
def invalid_role():
return 'success' if ds.find_role('bogus') is None else 'failure'
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
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask.ext.mongoengine import MongoEngine
from flask.ext.security import Security, UserMixin, RoleMixin, \
MongoEngineUserDatastore
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config, **kwargs):
app = create_base_app(config)
app.config['MONGODB_SETTINGS'] = dict(
db='flask_security_test',
host='localhost',
port=27017
)
db = MongoEngine(app)
class Role(db.Document, RoleMixin):
name = db.StringField(required=True, unique=True, max_length=80)
description = db.StringField(max_length=255)
class User(db.Document, UserMixin):
email = db.StringField(unique=True, max_length=255)
password = db.StringField(required=True, max_length=255)
last_login_at = db.DateTimeField()
current_login_at = db.DateTimeField()
last_login_ip = db.StringField(max_length=100)
current_login_ip = db.StringField(max_length=100)
login_count = db.IntField()
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
@app.before_first_request
def before_first_request():
User.drop_collection()
Role.drop_collection()
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, datastore=MongoEngineUserDatastore(db, User, Role), **kwargs)
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
+62
View File
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask_peewee.db import Database
from peewee import *
from flask.ext.security import Security, UserMixin, RoleMixin, \
PeeweeUserDatastore
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config, **kwargs):
app = create_base_app(config)
app.config['DATABASE'] = {
'name': 'example2.db',
'engine': 'peewee.SqliteDatabase',
}
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
description = TextField(null=True)
class User(db.Model, UserMixin):
email = TextField()
password = TextField()
last_login_at = DateTimeField(null=True)
current_login_at = DateTimeField(null=True)
last_login_ip = TextField(null=True)
current_login_ip = TextField(null=True)
login_count = IntegerField(null=True)
active = BooleanField(default=True)
confirmed_at = DateTimeField(null=True)
class UserRoles(db.Model):
""" Peewee does not have built-in many-to-many support, so we have to
create this mapping class to link users to roles."""
user = ForeignKeyField(User, related_name='roles')
role = ForeignKeyField(Role, related_name='users')
name = property(lambda self: self.role.name)
description = property(lambda self: self.role.description)
@app.before_first_request
def before_first_request():
for Model in (Role, User, UserRoles):
Model.drop_table(fail_silently=True)
Model.create_table(fail_silently=True)
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, datastore=PeeweeUserDatastore(db, User, Role, UserRoles), **kwargs)
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
+61
View File
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, UserMixin, RoleMixin, \
SQLAlchemyUserDatastore
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config, **kwargs):
app = create_base_app(config)
#app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
db = SQLAlchemy(app)
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
last_login_at = db.Column(db.DateTime())
current_login_at = db.Column(db.DateTime())
last_login_ip = db.Column(db.String(100))
current_login_ip = db.Column(db.String(100))
login_count = db.Column(db.Integer)
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
@app.before_first_request
def before_first_request():
db.drop_all()
db.create_all()
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, datastore=SQLAlchemyUserDatastore(db, User, Role), **kwargs)
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
@@ -12,9 +12,9 @@
{% endif -%} {% endif -%}
<li> <li>
{%- if current_user.is_authenticated() -%} {%- if current_user.is_authenticated() -%}
<a href="{{ url_for('auth.logout') }}">Log out</a> <a href="{{ url_for('security.logout') }}">Log out</a>
{%- else -%} {%- else -%}
<a href="{{ url_for('login') }}">Log in</a> <a href="{{ url_for('security.login') }}">Log in</a>
{%- endif -%} {%- endif -%}
</li> </li>
</ul> </ul>
@@ -0,0 +1 @@
CUSTOM FORGOT PASSWORD
@@ -0,0 +1 @@
CUSTOM LOGIN USER
@@ -0,0 +1 @@
CUSTOM REGISTER USER
@@ -0,0 +1 @@
CUSTOM RESET PASSWORD
@@ -0,0 +1 @@
CUSTOM SEND CONFIRMATION
@@ -0,0 +1 @@
CUSTOM SEND LOGIN
+11
View File
@@ -0,0 +1,11 @@
{% include "_messages.html" %}
{% include "_nav.html" %}
<h1>Register</h1>
<form action="{{ url_for_security('register') }}" method="POST" name="register_form">
{{ register_user_form.hidden_tag() }}
{{ register_user_form.email.label }} {{ register_user_form.email }}<br/>
{{ register_user_form.password.label }} {{ register_user_form.password }}<br/>
{{ register_user_form.password_confirm.label }} {{ register_user_form.password_confirm }}<br/>
{{ register_user_form.submit }}
</form>
<p>{{ content }}</p>
@@ -0,0 +1,3 @@
{% include "_messages.html" %}
{% include "_nav.html" %}
<h1>You are not allowed to access the requested resouce</h1>
+63 -19
View File
@@ -1,44 +1,88 @@
# -*- coding: utf-8 -*-
import unittest import unittest
import flask_security
from flask_security import RoleMixin, UserMixin, AnonymousUser from flask_security import RoleMixin, UserMixin, AnonymousUser
from flask_security.datastore import Datastore, UserDatastore
class Role(RoleMixin): class Role(RoleMixin):
def __init__(self, name, description=None): def __init__(self, name):
self.name = name self.name = name
self.description = description
class User(UserMixin): class User(UserMixin):
def __init__(self, username, email, roles): def __init__(self, email, roles):
self.username = username
self.email = email self.email = email
self.roles = roles self.roles = roles
# set the models or we'll get errors
flask_security.User = User
flask_security.Role = Role
admin = Role('admin') admin = Role('admin')
admin2 = Role('admin') admin2 = Role('admin')
editor = Role('editor') editor = Role('editor')
user = User('matt', 'matt@lp.com', [admin, editor]) user = User('matt@lp.com', [admin, editor])
class SecurityEntityTests(unittest.TestCase): class SecurityEntityTests(unittest.TestCase):
def test_role_mixin_equal(self): def test_role_mixin_equal(self):
self.assertEqual(admin, admin2) self.assertEqual(admin, admin2)
def test_role_mixin_not_equal(self): def test_role_mixin_not_equal(self):
self.assertNotEqual(admin, editor) self.assertNotEqual(admin, editor)
def test_user_mixin_has_role_with_string(self): def test_user_mixin_has_role_with_string(self):
self.assertTrue(user.has_role('admin')) self.assertTrue(user.has_role('admin'))
def test_user_mixin_has_role_with_role_obj(self): def test_user_mixin_has_role_with_role_obj(self):
self.assertTrue(user.has_role(Role('admin'))) self.assertTrue(user.has_role(Role('admin')))
def test_anonymous_user_has_no_roles(self): def test_anonymous_user_has_no_roles(self):
au = AnonymousUser() au = AnonymousUser()
self.assertEqual(0, len(au.roles)) self.assertEqual(0, len(au.roles))
self.assertFalse(au.has_role('admin')) self.assertFalse(au.has_role('admin'))
class DatastoreTests(unittest.TestCase):
def setUp(self):
super(DatastoreTests, self).setUp()
self.ds = UserDatastore(None, None)
def test_unimplemented_datastore_methods(self):
ds = Datastore(None)
self.assertRaises(NotImplementedError, ds.put, None)
self.assertRaises(NotImplementedError, ds.delete, None)
def test_unimplemented_user_datastore_methods(self):
self.assertRaises(NotImplementedError, self.ds.find_user, None)
self.assertRaises(NotImplementedError, self.ds.find_role, None)
def test_toggle_active(self):
user.active = True
rv = self.ds.toggle_active(user)
self.assertTrue(rv)
self.assertFalse(user.active)
rv = self.ds.toggle_active(user)
self.assertTrue(rv)
self.assertTrue(user.active)
def test_deactivate_user(self):
user.active = True
rv = self.ds.deactivate_user(user)
self.assertTrue(rv)
self.assertFalse(user.active)
def test_activate_user(self):
ds = UserDatastore(None, None)
user.active = False
ds.activate_user(user)
self.assertTrue(user.active)
def test_deactivate_returns_false_if_already_false(self):
user.active = False
self.assertFalse(self.ds.deactivate_user(user))
def test_activate_returns_false_if_already_true(self):
user.active = True
self.assertFalse(self.ds.activate_user(user))