Fix merge conflicts and fix release.py

This commit is contained in:
Matt Wright
2012-10-11 17:34:28 -04:00
75 changed files with 4257 additions and 1505 deletions
+24 -5
View File
@@ -1,8 +1,27 @@
.DS_Store
*.pyc
*.py[co]
# Packages
*.egg
*.egg-info
.project
.pydevproject
.settings
dist
*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
+22
View File
@@ -0,0 +1,22 @@
language: python
python:
- "2.6"
- "2.7"
install:
- pip install . --quiet --use-mirrors
- "if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then pip install importlib --quiet --use-mirrors; fi"
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors
before_script:
- mysql -e 'create database flask_security_test;'
services:
- mongodb
script: nosetests
branches:
only:
- develop
+13 -4
View File
@@ -3,17 +3,26 @@ Flask-Security Changelog
Here you can see the full list of changes between each Flask-Security release.
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
Released June 12th 2012
- Fixed a bug in the RoleMixin eq/ne functions
Version 1.2.2
-------------
Released April 27th, 2012
Released April 27th 2012
- Fixed bug where `roles_required` and `roles_accepted` did not pass the next
argument to the login view
@@ -21,7 +30,7 @@ Released April 27th, 2012
Version 1.2.1
-------------
Released March 28th, 2012
Released March 28th 2012
- Added optional user model mixin parameter for datastores
- Added CreateRoleCommand to available Flask-Script commands
@@ -29,7 +38,7 @@ Released March 28th, 2012
Version 1.2.0
-------------
Released March 12th, 2012
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.
+1 -1
View File
@@ -1,6 +1,6 @@
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
+2 -1
View File
@@ -1 +1,2 @@
include tests/*.py
recursive-include tests *.py
recursive-include flask_security/templates *.*
-11
View File
@@ -1,11 +0,0 @@
Flask-Security
==============
Simple security for Flask applications combining Flask-Login, Flask-Principal,
Flask-WTF, passlib, and your choice of datastore. Currently SQLAlchemy via
Flask-SQLAlchemy and MongoEngine via 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 to support bcrypt passwords.
Documentation: http://packages.python.org/Flask-Security/
+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

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>
+57
View File
@@ -0,0 +1,57 @@
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:
Signals
-------
See the documentation for the signals provided by the Flask-Login and
Flask-Principal extensions. Flask-Security does not provide any additional
signals.
+11 -6
View File
@@ -18,7 +18,6 @@ import sys, os
# 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'))
#from setup import __version__
# -- General configuration -----------------------------------------------------
@@ -50,7 +49,7 @@ copyright = u'2012, Matt Wright'
# built documents.
#
# The short X.Y version.
version = '1.2.3'
version = '1.3.0-dev'
# The full version, including alpha/beta/rc tags.
release = version
@@ -93,14 +92,16 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'flask_small'
html_theme = 'flask'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
'github_fork': 'mattupstate/flask-security',
'index_logo': False
#'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.
@@ -136,7 +137,11 @@ html_static_path = ['_static']
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
html_sidebars = {
'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
'**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
'sourcelink.html', 'searchbox.html']
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
+170
View File
@@ -0,0 +1,170 @@
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``.
=============================== ================================================
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``.
========================= ======================================================
Miscellaneous
-------------
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
======================================= ========================================
``SECURITY_CONFIRM_EMAIL_WITHIN`` Specifies the amount of time a user has
before their confirmation link expires.
Always pluralized the time unit for this
value. Defaults to ``5 days``.
``SECURITY_RESET_PASSWORD_WITHIN`` Specifies the amount of time a user has
before their password reset link
expires. Always pluralized the time unit
for this value. Defaults to ``5 days``.
``SECURITY_LOGIN_WITHIN`` Specifies the amount of time a user has
before a login link expires. This is
only used when the passwordless login
feature is enabled. Always pluralized
the time unit for this value. Defaults
to ``1 days``.
``SECURITY_LOGIN_WITHOUT_CONFIRMATION`` Specifies if a user may login before
confirming their email when the value
of ``SECURITY_CONFIRMABLE`` is set to
``True``. Defaults to ``False``.
``SECURITY_CONFIRM_SALT`` Specifies the salt value when generating
confirmation links/tokens. Defaults to
``confirm-salt``.
``SECURITY_RESET_SALT`` Specifies the salt value when generating
password reset links/tokens. Defaults to
``reset-salt``.
``SECURITY_LOGIN_SALT`` Specifies the salt value when generating
login links/tokens. Defaults to
``login-salt``.
``SECURITY_REMEMBER_SALT`` Specifies the salt value when generating
remember tokens. Remember tokens are
used instead of user ID's as it is more
secure. Defaults to ``remember-salt``.
======================================= ========================================
+13
View File
@@ -0,0 +1,13 @@
Contents
--------
.. toctree::
:maxdepth: 1
features
configuration
quickstart
models
customizing
api
changelog
+96
View File
@@ -0,0 +1,96 @@
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/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
Each template is passed a template context object that includes the following,
including the objects/values that are passed to the template by the main
Flask application context processory:
* ``<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
* ``send_confirmation_context_processor``: Send confirmation view
* ``send_login_context_processor``: Send login view
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/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/
+33 -344
View File
@@ -1,347 +1,36 @@
.. Flask-Security documentation master file, created by
sphinx-quickstart on Mon Mar 12 15:35:21 2012.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Flask-Security
==============
.. module:: 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 on your own. Additionally, you may need to install an encryption
library such as `py-bcrypt <http://www.mindrot.org/projects/py-bcrypt/>`_ (if
you plan to use bcrypt) for your desired encryption method.
Contents
=========
* :ref:`overview`
* :ref:`installation`
* :ref:`getting-started`
* :ref:`additional-user-fields`
* :ref:`flask-script-commands`
* :ref:`api`
* :doc:`Changelog </changelog>`
.. _overview:
Overview
========
Flask-Security does a few things that Flask-Login and Flask-Principal don't
provide out of the box. They are:
1. Setting up login and logout endpoints
2. Authenticating users based on username or email
3. Limiting access based on user 'roles'
4. User and role creation
5. Password encryption
That being said, you can still hook into things such as the Flask-Login and
Flask-Principal signals if need be.
.. _installation:
Installation
============
First, install Flask-Security::
$ mkvirtualenv app-name
$ pip install Flask-Security
Then install your datastore requirement.
**SQLAlchemy**::
$ pip install Flask-SQLAlchemy
**MongoEngine**::
$ pip install https://github.com/sbook/flask-mongoengine/tarball/master
.. _getting-started:
Getting Started
===============
The following code samples will illustrate how to get started using SQLAlchemy.
First thing you'll want to do is setup your application and datastore::
from flask import Flask, render_template
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import (User, Security, LoginForm, login_required,
roles_accepted, user_datastore)
from flask.ext.security.datastore.sqlalchemy import SQLAlchemyUserDatastore
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
db = SQLAlchemy(app)
Security(app, SQLAlchemyUserDatastore(db))
You'll probably want to at least one user to the database to test this out.
There are many ways to do this, but this is a quick and dirty way to do it::
@app.before_first_request
def before_first_request():
user_datastore.create_role(name='admin')
user_datastore.create_user(username='matt', email='matt@something.com',
password='password', roles=['admin'])
Next you'll want to setup your login screen. Setup your view::
@app.route("/login")
def login():
return render_template('login.html', form=LoginForm())
And corresponding template::
<form action="{{ url_for('auth.authenticate') }}" method="POST">
{{ form.hidden_tag() }}
{{ form.username.label }} {{ form.username }}<br/>
{{ form.password.label }} {{ form.password }}<br/>
{{ form.remember.label }} {{ form.remember }}<br/>
{{ form.submit }}
</form>
By default, Flask-Security will redirect a user to `/profile` after logging in.
You can set this page up yourself or set the `SECURITY_POST_LOGIN` config
value to change this behavior. Regardless, setup a protected view as such::
@app.route('/profile')
@login_required
def profile():
return render_template('profile.html')
Now you have an application with basic authentication. If you run the local
development server you can visit `http://localhost:5000/login <http://localhost:5000/login>`_
to login.
The last thing you'll want to do is add a logout link to your templates. This
can be achieved with::
<a href="{{ url_for('auth.logout') }}">Logout</a>
Now, for instance, say you want to protect an admin area to users that are
administrators. You can use the `roles_accepted` decorator to prevent access.
The corresponding view would look like such::
@app.route('/admin')
@roles_accepted('admin')
def admin():
return render_template('admin/index.html')
And lastly, maybe you only want to show something in a template if a user has a
specific role::
{% if current_user.has_role('admin') %}
<a href="{{ url_for('admin.index') }}">Admin Panel</a>
{$ endif %}
.. _additional-user-fields:
Additional User Fields
----------------------
If you'd like to add additional fields to the user model you can use a mixin
class that specifies your additional fields. The following is an example of
how you might do this::
db = SQLAlchemy(app)
class UserAccountMixin():
first_name = db.Column(db.String(120))
last_name = db.Column(db.String(120))
Security(app, SQLAlchemyUserDatastore(db, UserAccountMixin))
.. _flask-script-commands:
Flask-Script Commands
---------------------
Flask-Security comes packed with a few Flask-Script commands. They are:
* :class:`flask.ext.security.script.CreateUserCommand`
* :class:`flask.ext.security.script.CreateRoleCommand`
* :class:`flask.ext.security.script.AddRoleCommand`
* :class:`flask.ext.security.script.RemoveRoleCommand`
* :class:`flask.ext.security.script.DeactivateUserCommand`
* :class:`flask.ext.security.script.ActivateUserCommand`
Register these on your script manager for pure convenience.
.. _configuration:
Configuration Values
====================
* :attr:`SECURITY_URL_PREFIX`: Specifies the URL prefix for the Security
blueprint
* :attr:`SECURITY_AUTH_PROVIDER`: Specifies the class to use as the
authentication provider. Such as `flask.ext.security.AuthenticationProvider`
* :attr:`SECURITY_PASSWORD_HASH`: Specifies the encryption method to use. e.g.:
plaintext, bcrypt, etc
* :attr:`SECURITY_USER_DATASTORE`: Specifies the property name to use for the
user datastore on the application instance
* :attr:`SECURITY_LOGIN_FORM`: Specifies the form class to use when processing
an authentication request
* :attr:`SECURITY_AUTH_URL`: Specifies the URL to to handle authentication
* :attr:`SECURITY_LOGOUT_URL`: Specifies the URL to process a logout request
* :attr:`SECURITY_LOGIN_VIEW`: Specifies the URL to redirect to when
authentication is required
* :attr:`SECURITY_POST_LOGIN`: Specifies the URL to redirect to after a user is
authenticated
* :attr:`SECURITY_POST_LOGOUT`: Specifies the URL to redirect to after a user
logs out
* :attr:`SECURITY_FLASH_MESSAGES`: Specifies wether or not to flash messages
during authentication request
.. _api:
API
===
.. autoclass:: flask_security.Security
:members:
.. data:: flask_security.current_user
A proxy for the current user.
Protecting Views
----------------
.. autofunction:: flask_security.login_required
.. autofunction:: flask_security.roles_required
.. autofunction:: flask_security.roles_accepted
User Object Helpers
-------------------
.. autoclass:: flask_security.UserMixin
:members:
.. autoclass:: flask_security.RoleMixin
:members:
.. autoclass:: flask_security.AnonymousUser
:members:
Datastores
----------
.. autoclass:: flask_security.datastore.UserDatastore
:members:
.. autoclass:: flask_security.datastore.sqlalchemy.SQLAlchemyUserDatastore
:members:
:inherited-members:
.. autoclass:: flask_security.datastore.mongoengine.MongoEngineUserDatastore
:members:
:inherited-members:
Models
------
.. autoclass:: flask_security.User
.. attribute:: id
User ID
.. attribute:: username
Username
.. attribute:: email
Email address
.. attribute:: password
Password
.. attribute:: active
Active state
.. attribute:: roles
User roles
.. attribute:: created_at
Created date
.. attribute:: modified_at
Modified date
.. autoclass:: flask_security.Role
.. attribute:: id
Role ID
.. attribute:: name
Role name
.. attribute:: description
Role description
Exceptions
----------
.. autoexception:: flask_security.BadCredentialsError
.. autoexception:: flask_security.AuthenticationError
.. autoexception:: flask_security.UserNotFoundError
.. autoexception:: flask_security.RoleNotFoundError
.. autoexception:: flask_security.UserIdNotFoundError
.. autoexception:: flask_security.UserDatastoreError
.. autoexception:: flask_security.UserCreationError
.. autoexception:: flask_security.RoleCreationError
Signals
-------
See the documentation for the signals provided by the Flask-Login and
Flask-Principal extensions. Flask-Security does not provide any additional
signals.
Changelog
=========
.. toctree::
:maxdepth: 2
changelog
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-Mail/>`_
6. `itsdangerous <http://packages.python.org/itsdangerous/>`_
7. `passlib <http://packages.python.org/passlib/>`_
Additionally, it assumes you'll be using a common library for your database
connections and model definitions. Flask-Security supports the following Flask
extensions out of the box for data persistance:
1. `Flask-SQLAlchemy <http://packages.python.org/Flask-SQLAlchemy/>`_
2. `Flask-MongoEngine <http://packages.python.org/Flask-MongoEngine/>`_
.. include:: contents.rst.inc
+51
View File
@@ -0,0 +1,51 @@
Models
======
Flask-Security assumes you'll be using libraries such as SQLAlchemy or
MongoEngine to define a data model that includes a `User` and `Role` model. The
fields on your models must follow a particular convention depending on the
functionality your app requires. Aside from this, you're free to add any
additional fields to your model(s) if you want. At the bear minimum your `User`
and `Role` model should include the following fields:
**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``
+70
View File
@@ -0,0 +1,70 @@
Quick Start
===========
Installation
------------
Install requirements:
$ mkvirtualenv <your-app-name>
$ pip install flask-security, flask-sqlalchemy
Basic 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
# 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('/')
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run()
View File
-128
View File
@@ -1,128 +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)
class UserAccountMixin():
first_name = db.Column(db.String(120))
last_name = db.Column(db.String(120))
Security(app, SQLAlchemyUserDatastore(db, UserAccountMixin))
@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)
class UserAccountMixin():
first_name = db.StringField(max_length=120)
last_name = db.StringField(max_length=120)
Security(app, MongoEngineUserDatastore(db, UserAccountMixin))
@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()
-11
View File
@@ -1,11 +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.next }}
{{ form.submit }}
</form>
<p>{{ content }}</p>
+11 -474
View File
@@ -10,477 +10,14 @@
:license: MIT, see LICENSE for more details.
"""
from functools import wraps
from flask import current_app, Blueprint, flash, redirect, request, \
session, url_for
from flask.ext.login import AnonymousUser as AnonymousUserBase, \
UserMixin as BaseUserMixin, LoginManager, login_required, login_user, \
logout_user, current_user, login_url
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, BooleanField
from passlib.context import CryptContext
from werkzeug.local import LocalProxy
class User(object):
"""User model"""
class Role(object):
"""Role model"""
URL_PREFIX_KEY = 'SECURITY_URL_PREFIX'
AUTH_PROVIDER_KEY = 'SECURITY_AUTH_PROVIDER'
PASSWORD_HASH_KEY = 'SECURITY_PASSWORD_HASH'
USER_DATASTORE_KEY = 'SECURITY_USER_DATASTORE'
LOGIN_FORM_KEY = 'SECURITY_LOGIN_FORM'
AUTH_URL_KEY = 'SECURITY_AUTH_URL'
LOGOUT_URL_KEY = 'SECURITY_LOGOUT_URL'
LOGIN_VIEW_KEY = 'SECURITY_LOGIN_VIEW'
POST_LOGIN_KEY = 'SECURITY_POST_LOGIN'
POST_LOGOUT_KEY = 'SECURITY_POST_LOGOUT'
FLASH_MESSAGES_KEY = 'SECURITY_FLASH_MESSAGES'
DEBUG_LOGIN = 'User %s logged in. Redirecting to: %s'
ERROR_LOGIN = 'Unsuccessful authentication attempt: %s. Redirecting to: %s'
DEBUG_LOGOUT = 'User logged out, redirecting to: %s'
FLASH_INACTIVE = 'Inactive user'
FLASH_PERMISSIONS = 'You do not have permission to view this resource.'
#: Default Flask-Security configuration
default_config = {
URL_PREFIX_KEY: None,
FLASH_MESSAGES_KEY: True,
PASSWORD_HASH_KEY: 'plaintext',
USER_DATASTORE_KEY: 'user_datastore',
AUTH_PROVIDER_KEY: 'flask.ext.security.AuthenticationProvider',
LOGIN_FORM_KEY: 'flask.ext.security.LoginForm',
AUTH_URL_KEY: '/auth',
LOGOUT_URL_KEY: '/logout',
LOGIN_VIEW_KEY: '/login',
POST_LOGIN_KEY: '/',
POST_LOGOUT_KEY: '/',
}
class BadCredentialsError(Exception):
"""Raised when an authentication attempt fails due to an error with the
provided credentials.
"""
class AuthenticationError(Exception):
"""Raised when an authentication attempt fails due to invalid configuration
or an unknown reason.
"""
class UserNotFoundError(Exception):
"""Raised by a user datastore when there is an attempt to find a user by
their identifier, often username or email, and the user is not found.
"""
class RoleNotFoundError(Exception):
"""Raised by a user datastore when there is an attempt to find a role and
the role cannot be found.
"""
class UserIdNotFoundError(Exception):
"""Raised by a user datastore when there is an attempt to find a user by
ID and the user is not found.
"""
class UserDatastoreError(Exception):
"""Raised when a user datastore experiences an unexpected error
"""
class UserCreationError(Exception):
"""Raised when an error occurs when creating a user
"""
class RoleCreationError(Exception):
"""Raised when an error occurs when creating a role
"""
#: App logger for convenience
logger = LocalProxy(lambda: current_app.logger)
#: Authentication provider
auth_provider = LocalProxy(lambda: current_app.auth_provider)
#: Login manager
login_manager = LocalProxy(lambda: current_app.login_manager)
#: Password encyption context
pwd_context = LocalProxy(lambda: current_app.pwd_context)
#: User datastore
user_datastore = LocalProxy(lambda: getattr(current_app,
current_app.config[USER_DATASTORE_KEY]))
def roles_required(*args):
"""View decorator which specifies that a user must have all the specified
roles. Example::
@app.route('/dashboard')
@roles_required('admin', 'editor')
def dashboard():
return 'Dashboard'
The current user must have both the `admin` role and `editor` role in order
to view the page.
:param args: The required roles.
"""
roles = args
perm = Permission(*[RoleNeed(role) for role in roles])
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated():
return redirect(
login_url(current_app.config[LOGIN_VIEW_KEY], request.url))
if perm.can():
return fn(*args, **kwargs)
logger.debug('Identity does not provide all of the '
'following roles: %s' % [r for r in roles])
do_flash(FLASH_PERMISSIONS, 'error')
return redirect(request.referrer or '/')
return decorated_view
return wrapper
def roles_accepted(*args):
"""View decorator which specifies that a user must have at least one of the
specified roles. Example::
@app.route('/create_post')
@roles_accepted('editor', 'author')
def create_post():
return 'Create Post'
The current user must have either the `editor` role or `author` role in
order to view the page.
:param args: The possible roles.
"""
roles = args
perms = [Permission(RoleNeed(role)) for role in roles]
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated():
return redirect(
login_url(current_app.config[LOGIN_VIEW_KEY], request.url))
for perm in perms:
if perm.can():
return fn(*args, **kwargs)
logger.debug('Identity does not provide at least one of '
'the following roles: %s' % [r for r in roles])
do_flash(FLASH_PERMISSIONS, 'error')
return redirect(request.referrer or '/')
return decorated_view
return wrapper
class RoleMixin(object):
"""Mixin for `Role` model definitions"""
def __eq__(self, other):
return self.name == other or self.name == getattr(other, 'name', None)
def __ne__(self, other):
return self.name != other and self.name != getattr(other, 'name', None)
def __str__(self):
return '<Role name=%s, description=%s>' % (self.name, self.description)
class UserMixin(BaseUserMixin):
"""Mixin for `User` model definitions"""
def is_active(self):
"""Returns `True` if the user is active."""
return self.active
def has_role(self, role):
"""Returns `True` if the user identifies with the specified role.
:param role: A role name or `Role` instance"""
return role in self.roles
def __str__(self):
ctx = (str(self.id), self.username, self.email)
return '<User id=%s, username=%s, email=%s>' % ctx
class AnonymousUser(AnonymousUserBase):
def __init__(self):
super(AnonymousUser, self).__init__()
self.roles = [] # TODO: Make this immutable?
def has_role(self, *args):
"""Returns `False`"""
return False
class Security(object):
"""The :class:`Security` class initializes the Flask-Security extension.
:param app: The application.
:param datastore: An instance of a user datastore.
"""
def __init__(self, app=None, datastore=None):
self.init_app(app, datastore)
def init_app(self, app, datastore):
"""Initializes the Flask-Security extension for the specified
application and datastore implentation.
:param app: The application.
:param datastore: An instance of a user datastore.
"""
if app is None or datastore is None:
return
# TODO: change blueprint name
blueprint = Blueprint('auth', __name__)
configured = {}
for key, value in default_config.items():
configured[key] = app.config.get(key, value)
app.config.update(configured)
config = app.config
# setup the login manager extension
login_manager = LoginManager()
login_manager.anonymous_user = AnonymousUser
login_manager.login_view = config[LOGIN_VIEW_KEY]
login_manager.setup_app(app)
app.login_manager = login_manager
Provider = get_class_from_config(AUTH_PROVIDER_KEY, config)
Form = get_class_from_config(LOGIN_FORM_KEY, config)
pw_hash = config[PASSWORD_HASH_KEY]
app.pwd_context = CryptContext(schemes=[pw_hash], default=pw_hash)
app.auth_provider = Provider(Form)
app.principal = Principal(app)
from flask.ext import security as s
s.User, s.Role = datastore.get_models()
setattr(app, config[USER_DATASTORE_KEY], datastore)
@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
if hasattr(current_user, 'id'):
identity.provides.add(UserNeed(current_user.id))
for role in current_user.roles:
identity.provides.add(RoleNeed(role.name))
identity.user = current_user
@login_manager.user_loader
def load_user(user_id):
try:
return datastore.with_id(user_id)
except Exception, e:
logger.error('Error getting user: %s' % e)
return None
auth_url = config[AUTH_URL_KEY]
@blueprint.route(auth_url, methods=['POST'], endpoint='authenticate')
def authenticate():
try:
form = Form()
user = auth_provider.authenticate(form)
if login_user(user, remember=form.remember.data):
redirect_url = get_post_login_redirect()
identity_changed.send(app, identity=Identity(user.id))
logger.debug(DEBUG_LOGIN % (user, redirect_url))
return redirect(redirect_url)
raise BadCredentialsError(FLASH_INACTIVE)
except BadCredentialsError, e:
message = '%s' % e
do_flash(message, 'error')
redirect_url = request.referrer or login_manager.login_view
logger.error(ERROR_LOGIN % (message, redirect_url))
return redirect(redirect_url)
@blueprint.route(config[LOGOUT_URL_KEY], endpoint='logout')
@login_required
def logout():
for value in ('identity.name', 'identity.auth_type'):
session.pop(value, None)
identity_changed.send(app, identity=AnonymousIdentity())
logout_user()
redirect_url = find_redirect(POST_LOGOUT_KEY)
logger.debug(DEBUG_LOGOUT % redirect_url)
return redirect(redirect_url)
app.register_blueprint(blueprint, url_prefix=config[URL_PREFIX_KEY])
class LoginForm(Form):
"""The default login form"""
username = TextField("Username or Email",
validators=[Required(message="Username not provided")])
password = PasswordField("Password",
validators=[Required(message="Password not provided")])
remember = BooleanField("Remember Me")
next = HiddenField()
submit = SubmitField("Login")
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
self.next.data = request.args.get('next', None)
class AuthenticationProvider(object):
"""The default authentication provider implementation.
:param login_form_class: The login form class to use when authenticating a
user
"""
def __init__(self, login_form_class=None):
self.login_form_class = login_form_class or LoginForm
def login_form(self, formdata=None):
"""Returns an instance of the login form with the provided form.
:param formdata: The incoming form data"""
return self.login_form_class(formdata)
def authenticate(self, form):
"""Processes an authentication request and returns a user instance if
authentication is successful.
:param form: An instance of a populated login form
"""
if not form.validate():
if form.username.errors:
raise BadCredentialsError(form.username.errors[0])
if form.password.errors:
raise BadCredentialsError(form.password.errors[0])
return self.do_authenticate(form.username.data, form.password.data)
def do_authenticate(self, user_identifier, password):
"""Returns the authenticated user if authentication is successfull. If
authentication fails an appropriate error is raised
:param user_identifier: The user's identifier, either an email address
or username
:param password: The user's unencrypted password
"""
try:
user = user_datastore.find_user(user_identifier)
except AttributeError, e:
self.auth_error("Could not find user service: %s" % e)
except UserNotFoundError, e:
raise BadCredentialsError("Specified user does not exist")
except AttributeError, e:
self.auth_error('Invalid user service: %s' % e)
except Exception, e:
self.auth_error('Unexpected authentication error: %s' % e)
# compare passwords
if pwd_context.verify(password, user.password):
return user
# bad match
raise BadCredentialsError("Password does not match")
def auth_error(self, msg):
"""Sends an error log message and raises an authentication error.
:param msg: An authentication error message"""
logger.error(msg)
raise AuthenticationError(msg)
def do_flash(message, category):
if current_app.config[FLASH_MESSAGES_KEY]:
flash(message, category)
def get_class_by_name(clazz):
"""Get a reference to a class by its string representation."""
parts = clazz.split('.')
module = ".".join(parts[:-1])
m = __import__(module)
for comp in parts[1:]:
m = getattr(m, comp)
return m
def get_class_from_config(key, config):
"""Get a reference to a class by its configuration key name."""
try:
return get_class_by_name(config[key])
except Exception, e:
raise AttributeError(
"Could not get class '%s' for Auth setting '%s' >> %s" %
(config[key], key, e))
def get_url(endpoint_or_url):
"""Returns a URL if a valid endpoint is found. Otherwise, returns the
provided value."""
try:
return url_for(endpoint_or_url)
except:
return endpoint_or_url
def get_post_login_redirect():
"""Returns the URL to redirect to after a user logs in successfully"""
return (get_url(request.args.get('next')) or
get_url(request.form.get('next')) or
find_redirect(POST_LOGIN_KEY))
def find_redirect(key):
"""Returns the URL to redirect to after a user logs in successfully"""
result = (get_url(session.pop(key.lower(), None)) or
get_url(current_app.config[key.upper()] or None) or '/')
try:
del session[key.lower()]
except:
pass
return result
__version__ = '1.3.0-dev'
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore
from .decorators import auth_token_required, http_auth_required, \
login_required, roles_accepted, roles_required
from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
from .signals import confirm_instructions_sent, password_reset, \
reset_password_instructions_sent, user_confirmed, user_registered
from .utils import login_user, logout_user, url_for_security
+82
View File
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.confirmable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security confirmable module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from datetime import datetime
from flask import current_app as app, request
from werkzeug.local import LocalProxy
from .utils import send_mail, md5, url_for_security, get_token_status
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('Please confirm your email', user.email,
'confirmation_instructions', user=user,
confirmation_link=confirmation_link)
confirm_instructions_sent.send(user, app=app._get_current_object())
return token
def generate_confirmation_token(user):
"""Generates a unique confirmation token for the specified user.
:param user: The user to work with
"""
data = [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(user, app=app._get_current_object())
+303
View File
@@ -0,0 +1,303 @@
# -*- 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
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
#: Default Flask-Security configuration
_default_config = {
'BLUEPRINT_NAME': 'security',
'URL_PREFIX': None,
'FLASH_MESSAGES': True,
'PASSWORD_HASH': 'plaintext',
'PASSWORD_SALT': None,
'LOGIN_URL': '/login',
'LOGOUT_URL': '/logout',
'REGISTER_URL': '/register',
'RESET_URL': '/reset',
'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,
'UNAUTHORIZED_VIEW': None,
'CONFIRMABLE': False,
'REGISTERABLE': False,
'RECOVERABLE': False,
'TRACKABLE': False,
'PASSWORDLESS': False,
'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',
'REMEMBER_SALT': 'remember-salt',
'DEFAULT_HTTP_AUTH_REALM': 'Login Required'
}
#: Default Flask-Security messages
_default_messages = {
'UNAUTHORIZED': ('You do not have permission to view this resource.', 'error'),
'CONFIRM_REGISTRATION': ('Thank you. Confirmation instructions have been sent to %(email)s.', 'success'),
'EMAIL_CONFIRMED': ('Thank you. Your email has been confirmed.', 'success'),
'ALREADY_CONFIRMED': ('Your email has already been confirmed.', 'info'),
'INVALID_CONFIRMATION_TOKEN': ('Invalid confirmation token.', 'error'),
'ALREADY_CONFIRMED': ('This email has already been confirmed', 'info'),
'PASSWORD_MISMATCH': ('Password does not match', 'error'),
'PASSWORD_RESET_REQUEST': ('Instructions to reset your password have been sent to %(email)s.', 'info'),
'PASSWORD_RESET_EXPIRED': ('You did not reset your password within %(within)s. New instructions have been sent to %(email)s.', 'error'),
'INVALID_RESET_PASSWORD_TOKEN': ('Invalid reset password token.', 'error'),
'CONFIRMATION_REQUIRED': ('Email requires confirmation.', 'error'),
'CONFIRMATION_REQUEST': ('Confirmation instructions have been sent to %(email)s.', 'info'),
'CONFIRMATION_EXPIRED': ('You did not confirm your email within %(within)s. New instructions to confirm your email have been sent to %(email)s.', 'error'),
'LOGIN_EXPIRED': ('You did not login within %(within)s. New instructions to login have been sent to %(email)s.', 'error'),
'LOGIN_EMAIL_SENT': ('Instructions to login have been sent to %(email)s.', 'success'),
'INVALID_LOGIN_TOKEN': ('Invalid login token.', 'error'),
'DISABLED_ACCOUNT': ('Account is disabled.', 'error'),
'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'),
'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success')
}
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.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)
return CryptContext(schemes=[pw_hash], 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
))
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 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):
"""Initializes the Flask-Security extension for the specified
application and datastore implentation.
:param app: The application.
:param datastore: An instance of a user datastore.
"""
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)
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)
+178
View File
@@ -0,0 +1,178 @@
# -*- 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 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):
role = role.name if isinstance(role, self.role_model) else role
return self.find_user(email=user.email), self.find_role(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, **kwargs):
"""Returns a user matching the provided paramters."""
raise NotImplementedError
def find_role(self, **kwargs):
"""Returns a role matching the provided paramters."""
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 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()
-205
View File
@@ -1,205 +0,0 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.datastore
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains an abstracted user datastore.
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from datetime import datetime
from flask.ext import security
from flask.ext.security import UserCreationError, RoleCreationError, pwd_context
class UserDatastore(object):
"""Abstracted user datastore. Always extend this class and implement the
:attr:`get_models`, :attr:`_save_model`, :attr:`_do_with_id`,
:attr:`_do_find_user`, and :attr:`_do_find_role` methods.
:param db: An instance of a configured databse manager from a Flask
extension such as Flask-SQLAlchemy or Flask-MongoEngine
:param user_account_mixin: An optional mixin class that specifies additional
fields to be added to the user model
"""
def __init__(self, db, user_account_mixin=None):
self.db = db
self.user_account_mixin = user_account_mixin or object
def get_models(self):
"""Returns configured `User` and `Role` models for the datastore
implementation"""
raise NotImplementedError(
"User datastore does not implement get_models method")
def _save_model(self, model, **kwargs):
raise NotImplementedError(
"User datastore does not implement _save_model method")
def _do_with_id(self, id):
raise NotImplementedError(
"User datastore does not implement _do_with_id method")
def _do_find_user(self):
raise NotImplementedError(
"User datastore does not implement _do_find_user method")
def _do_find_role(self):
raise NotImplementedError(
"User datastore does not implement _do_find_role method")
def _do_add_role(self, user, role):
user, role = self._prepare_role_modify_args(user, role)
if role not in user.roles:
user.roles.append(role)
return user
def _do_remove_role(self, user, role):
user, role = self._prepare_role_modify_args(user, role)
if role in user.roles:
user.roles.remove(role)
return user
def _do_toggle_active(self, user, active=None):
user = self.find_user(user)
if active is None:
user.active = not user.active
elif active != user.active:
user.active = active
return user
def _do_deactive_user(self, user):
return self._do_toggle_active(user, False)
def _do_active_user(self, user):
return self._do_toggle_active(user, True)
def _prepare_role_modify_args(self, user, role):
if isinstance(user, security.User):
user = user.username or user.email
if isinstance(role, security.Role):
role = role.name
return self.find_user(user), self.find_role(role)
def _prepare_create_role_args(self, kwargs):
for key in ('name', 'description'):
kwargs[key] = kwargs.get(key, None)
if kwargs['name'] is None:
raise RoleCreationError("Missing name argument")
return kwargs
def _prepare_create_user_args(self, kwargs):
username = kwargs.get('username', None)
email = kwargs.get('email', None)
password = kwargs.get('password', None)
if username is None and email is None:
raise UserCreationError('Missing username and/or email arguments')
if password is None:
raise UserCreationError('Missing password argument')
roles = kwargs.get('roles', [])
for i, role in enumerate(roles):
rn = role.name if isinstance(role, security.Role) else role
# see if the role exists
roles[i] = self.find_role(rn)
kwargs['roles'] = roles
now = datetime.utcnow()
kwargs['created_at'], kwargs['modified_at'] = now, now
pw = kwargs['password']
if not pwd_context.identify(pw):
kwargs['password'] = pwd_context.encrypt(pw)
return kwargs
def with_id(self, id):
"""Returns a user with the specified ID.
:param id: User ID"""
user = self._do_with_id(id)
if user:
return user
raise security.UserIdNotFoundError()
def find_user(self, user):
"""Returns a user based on the specified identifier.
:param user: User identifier, usually a username or email address
"""
user = self._do_find_user(user)
if user:
return user
raise security.UserNotFoundError()
def find_role(self, role):
"""Returns a role based on its name.
:param role: Role name
"""
role = self._do_find_role(role)
if role:
return role
raise security.RoleNotFoundError()
def create_role(self, **kwargs):
"""Creates and returns a new role.
:param name: Role name
:param description: Role description
"""
role = security.Role(**self._prepare_create_role_args(kwargs))
return self._save_model(role)
def create_user(self, **kwargs):
"""Creates and returns a new user.
:param username: Username
:param email: Email address
:param password: Unencrypted password
:param active: The optional active state
"""
user = security.User(**self._prepare_create_user_args(kwargs))
return self._save_model(user)
def add_role_to_user(self, user, role):
"""Adds a role to a user if the user does not have it already. Returns
the modified user.
:param user: A User instance or a user identifier
:param role: A Role instance or a role name
"""
return self._save_model(self._do_add_role(user, role))
def remove_role_from_user(self, user, role, commit=True):
"""Removes a role from a user if the user has the role. Returns the
modified user.
:param user: A User instance or a user identifier
:param role: A Role instance or a role name
"""
return self._save_model(self._do_remove_role(user, role))
def deactivate_user(self, user):
"""Deactivates a user and returns the modified user.
:param user: A User instance or a user identifier
"""
return self._save_model(self._do_deactive_user(user))
def activate_user(self, user, commit=True):
"""Activates a user and returns the modified user.
:param user: A User instance or a user identifier
"""
return self._save_model(self._do_active_user(user))
-73
View File
@@ -1,73 +0,0 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.datastore.mongoengine
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains a Flask-Security MongoEngine datastore implementation
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from flask.ext import security
from flask.ext.security import UserMixin, RoleMixin
from flask.ext.security.datastore import UserDatastore
class MongoEngineUserDatastore(UserDatastore):
"""A MongoEngine datastore implementation for Flask-Security.
Example usage::
from flask import Flask
from flask.ext.mongoengine import MongoEngine
from flask.ext.security import Security
from flask.ext.security.datastore.mongoengine import MongoEngineUserDatastore
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
app.config['MONGODB_DB'] = 'flask_security_example'
app.config['MONGODB_HOST'] = 'localhost'
app.config['MONGODB_PORT'] = 27017
db = MongoEngine(app)
Security(app, MongoEngineUserDatastore(db))
"""
def get_models(self):
db = self.db
class Role(db.Document, RoleMixin):
"""MongoEngine Role model"""
name = db.StringField(required=True, unique=True, max_length=80)
description = db.StringField(max_length=255)
class User(db.Document, UserMixin, self.user_account_mixin):
"""MongoEngine User model"""
username = db.StringField(unique=True, max_length=255)
email = db.StringField(unique=True, max_length=255)
password = db.StringField(required=True, max_length=120)
active = db.BooleanField(default=True)
roles = db.ListField(db.ReferenceField(Role), default=[])
created_at = db.DateTimeField()
modified_at = db.DateTimeField()
return User, Role
def _save_model(self, model):
model.save()
return model
def _do_with_id(self, id):
try:
return security.User.objects.get(id=id)
except:
return None
def _do_find_user(self, user):
return security.User.objects(username=user).first() or \
security.User.objects(email=user).first()
def _do_find_role(self, role):
return security.Role.objects(name=role).first()
-92
View File
@@ -1,92 +0,0 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.datastore.sqlalchemy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains a Flask-Security SQLAlchemy datastore implementation
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from flask.ext import security
from flask.ext.security import UserMixin, RoleMixin
from flask.ext.security.datastore import UserDatastore
class SQLAlchemyUserDatastore(UserDatastore):
"""A SQLAlchemy datastore implementation for Flask-Security.
Example usage::
from flask import Flask
from flask.ext.security import Security
from flask.ext.security.datastore.sqlalchemy import SQLAlchemyUserDatastore
from flask.ext.sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/flask_security_example.sqlite'
db = SQLAlchemy(app)
Security(app, SQLAlchemyUserDatastore(db))
"""
def get_models(self):
db = self.db
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('role.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('user.id')))
class Role(db.Model, RoleMixin):
"""SQLAlchemy Role model"""
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
def __init__(self, name=None, description=None):
self.name = name
self.description = description
class User(db.Model, UserMixin, self.user_account_mixin):
"""SQLAlchemy User model"""
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), unique=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(120))
active = db.Column(db.Boolean())
created_at = db.Column(db.DateTime())
modified_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
def __init__(self, username=None, email=None, password=None,
active=True, roles=None,
created_at=None, modified_at=None):
self.username = username
self.email = email
self.password = password
self.active = active
self.roles = roles or []
self.created_at = created_at
self.modified_at = modified_at
return User, Role
def _save_model(self, model):
self.db.session.add(model)
self.db.session.commit()
return model
def _do_with_id(self, id):
return security.User.query.get(id)
def _do_find_user(self, user):
return security.User.query.filter_by(username=user).first() or \
security.User.query.filter_by(email=user).first()
def _do_find_role(self, role):
return security.Role.query.filter_by(name=role).first()
+172
View File
@@ -0,0 +1,172 @@
# -*- 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 functools import wraps
from flask import current_app, Response, request, redirect
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>
"""
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)
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()
identity_changed.send(app, identity=Identity(user.id))
return True
def _check_http_auth():
auth = request.authorization or dict(username=None, password=None)
user = _security.datastore.find_user(email=auth.username)
if user and utils.verify_password(auth.password, user.password):
app = current_app._get_current_object()
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 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
+187
View File
@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.forms
~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security forms module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from flask import request, current_app
from flask.ext.wtf import Form as BaseForm, TextField, PasswordField, \
SubmitField, HiddenField, Required, BooleanField, EqualTo, Email, \
ValidationError, Length
from werkzeug.local import LocalProxy
from .confirmable import requires_confirmation
from .utils import verify_password, get_message
# Convenient reference
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
email_required = Required(message='Email not provided')
email_validator = Email(message='Invalid email address')
password_required = Required(message="Password not provided")
def unique_user_email(form, field):
if _datastore.find_user(email=field.data) is not None:
raise ValidationError(field.data +
' is already associated with an account')
def valid_user_email(form, field):
form.user = _datastore.find_user(email=field.data)
if form.user is None:
raise ValidationError('Specified user does not exist')
class Form(BaseForm):
def __init__(self, *args, **kwargs):
kwargs.setdefault('csrf_enabled', not current_app.testing)
super(Form, self).__init__(*args, **kwargs)
class EmailFormMixin():
email = TextField("Email Address",
validators=[email_required,
email_validator])
class UserEmailFormMixin():
user = None
email = TextField("Email Address",
validators=[email_required,
email_validator,
valid_user_email])
class UniqueEmailFormMixin():
email = TextField("Email Address",
validators=[email_required,
email_validator,
unique_user_email])
class PasswordFormMixin():
password = PasswordField("Password",
validators=[password_required])
class NewPasswordFormMixin():
password = PasswordField("Password",
validators=[password_required,
Length(min=6, max=128)])
class PasswordConfirmFormMixin():
password_confirm = PasswordField("Retype Password",
validators=[EqualTo('password', message="Passwords do not match")])
class NextFormMixin():
next = HiddenField()
class RegisterFormMixin():
submit = SubmitField("Register")
class SendConfirmationForm(Form, UserEmailFormMixin):
"""The default forgot password form"""
submit = SubmitField("Resend Confirmation Instructions")
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("Recover Password")
class PasswordlessLoginForm(Form, UserEmailFormMixin):
"""The passwordless login form"""
submit = SubmitField("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('Email Address')
password = PasswordField('Password')
remember = BooleanField("Remember Me")
submit = SubmitField("Login")
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
def validate(self):
super(LoginForm, self).validate()
if self.email.data.strip() == '':
self.email.errors.append('Email not provided')
return False
if self.password.data.strip() == '':
self.password.errors.append('Password not provided')
return False
self.user = _datastore.find_user(email=self.email.data)
if self.user is None:
self.email.errors.append('Specified user does not exist')
return False
if not verify_password(self.password.data, self.user.password):
self.password.errors.append('Invalid password')
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):
def to_dict(self):
return dict(email=self.email.data,
password=self.password.data)
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
pass
class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
"""The default reset password form"""
submit = SubmitField("Reset Password")
+58
View File
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.passwordless
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security passwordless module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from flask import request, current_app as app
from werkzeug.local import LocalProxy
from .signals import login_instructions_sent
from .utils import send_mail, url_for_security, get_token_status
# 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('Login Instructions', user.email,
'login_instructions', user=user, login_link=login_link)
login_instructions_sent.send(dict(user=user, login_token=token),
app=app._get_current_object())
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([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
# 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('Password reset instructions', user.email,
'reset_instructions',
user=user, reset_link=reset_link)
reset_password_instructions_sent.send(dict(user=user, token=token),
app=app._get_current_object())
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('Your password has been reset', 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 = [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(user, app=app._get_current_object())
+41
View File
@@ -0,0 +1,41 @@
# -*- 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
# 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(dict(user=user, confirm_token=token),
app=app._get_current_object())
send_mail('Welcome', user.email, 'welcome',
user=user, confirmation_link=confirmation_link)
return user
+41 -17
View File
@@ -3,49 +3,68 @@
flask.ext.security.script
~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains commands for use with the Flask-Script extension
Flask-Security script module
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
try:
import simplejson as json
except ImportError:
import json
import json
import re
from flask import current_app
from flask.ext.script import Command, Option
from werkzeug.local import LocalProxy
from flask.ext.security import user_datastore
from .utils import encrypt_password
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
def pprint(obj):
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):
"""Create a user"""
option_list = (
Option('-u', '--username', dest='username', default=None),
Option('-e', '--email', dest='email', default=None),
Option('-p', '--password', dest='password', default=None),
Option('-a', '--active', dest='active', default=''),
Option('-r', '--roles', dest='roles', default=''),
)
@commit
def run(self, **kwargs):
# sanitize active input
ai = re.sub(r'\s', '', str(kwargs['active']))
kwargs['active'] = ai.lower() in ['', 'y', 'yes', '1', 'active']
# sanitize role input a bit
ri = re.sub(r'\s', '', kwargs['roles'])
kwargs['roles'] = [] if ri == '' else ri.split(',')
from flask_security.forms import ConfirmRegisterForm
from werkzeug.datastructures import MultiDict
user_datastore.create_user(**kwargs)
form = ConfirmRegisterForm(MultiDict(kwargs), csrf_enabled=False)
print 'User created successfully.'
kwargs['password'] = '****'
pprint(kwargs)
if form.validate():
kwargs['password'] = encrypt_password(kwargs['password'])
_datastore.create_user(**kwargs)
print 'User created successfully.'
kwargs['password'] = '****'
pprint(kwargs)
else:
print 'Error creating user'
pprint(form.errors)
class CreateRoleCommand(Command):
@@ -56,8 +75,9 @@ class CreateRoleCommand(Command):
Option('-d', '--desc', dest='description', default=None),
)
@commit
def run(self, **kwargs):
user_datastore.create_role(**kwargs)
_datastore.create_role(**kwargs)
print 'Role "%(name)s" created successfully.' % kwargs
@@ -71,16 +91,18 @@ class _RoleCommand(Command):
class AddRoleCommand(_RoleCommand):
"""Add a role to a user"""
@commit
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)
class RemoveRoleCommand(_RoleCommand):
"""Add a role to a user"""
@commit
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)
@@ -93,14 +115,16 @@ class _ToggleActiveCommand(Command):
class DeactivateUserCommand(_ToggleActiveCommand):
"""Deactive a user"""
@commit
def run(self, user_identifier):
user_datastore.deactivate_user(user_identifier)
_datastore.deactivate_user(user_identifier)
print "User '%s' has been deactivated" % user_identifier
class ActivateUserCommand(_ToggleActiveCommand):
"""Deactive a user"""
@commit
def run(self, user_identifier):
user_datastore.activate_user(user_identifier)
_datastore.activate_user(user_identifier)
print "User '%s' has been activated" % user_identifier
+27
View File
@@ -0,0 +1,27 @@
# -*- 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")
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,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" %}
+311
View File
@@ -0,0 +1,311 @@
# -*- 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 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, reset_password_instructions_sent, \
login_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."""
_login_user(user, remember)
if _security.trackable:
old_current, new_current = user.current_login_at, datetime.utcnow()
user.last_login_at = old_current or new_current
user.current_login_at = new_current
remote_addr = request.remote_addr or 'untrackable'
old_current, new_current = user.current_login_ip, remote_addr
user.last_login_ip = old_current or new_current
user.current_login_ip = new_current
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))
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, hashlib.sha512)
return base64.b64encode(h.digest())
def verify_password(password, password_hash):
return _pwd_context.verify(get_hmac(password), password_hash)
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)
return int(expires.strftime('%s')) - int(now.strftime('%s'))
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(data, app):
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(data, app):
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(request, app):
reset_requests.append(request)
reset_password_instructions_sent.connect(_on)
try:
yield reset_requests
finally:
reset_password_instructions_sent.disconnect(_on)
+314
View File
@@ -0,0 +1,314 @@
# -*- 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 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 .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
from .passwordless import send_login_instructions, \
login_token_status
from .recoverable import reset_password_token_status, \
send_reset_password_instructions, update_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
# 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"""
if request.json:
form = LoginForm(MultiDict(request.json))
else:
form = LoginForm()
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())
if request.json:
return _render_json(form, True)
return render_template('security/login_user.html',
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 = ConfirmRegisterForm
else:
form_class = RegisterForm
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('security/register_user.html',
register_user_form=form,
**_ctx('register'))
def send_login():
"""View function that sends login instructions for passwordless login"""
if request.json:
form = PasswordlessLoginForm(MultiDict(request.json))
else:
form = PasswordlessLoginForm()
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('security/send_login.html',
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."""
if request.json:
form = SendConfirmationForm(MultiDict(request.json))
else:
form = SendConfirmationForm()
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('security/send_confirmation.html',
send_confirmation_form=form,
**_ctx('send_confirmation'))
@anonymous_user_required
def confirm_email(token):
"""View function which handles a email confirmation request."""
expired, invalid, user = confirm_email_token_status(token)
if invalid:
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."""
if request.json:
form = ForgotPasswordForm(MultiDict(request.json))
else:
form = ForgotPasswordForm()
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('security/forgot_password.html',
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 = ResetPasswordForm()
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('security/reset_password.html',
reset_password_form=form,
reset_password_token=token,
**_ctx('reset_password'))
def create_blueprint(state, import_name):
"""Creates the security extension blueprint"""
bp = Blueprint(state.blueprint_name, import_name,
url_prefix=state.url_prefix,
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.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
+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
+20 -18
View File
@@ -1,14 +1,17 @@
"""
Flask-Security
--------------
==============
Flask-Security is a Flask extension that aims to add quick and simple security
via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
to your Flask applications.
Links
`````
Resources
---------
* `development version
* `Documentation <http://packages.python.org/Flask-Security/>`_
* `Issue Tracker <https://github.com/mattupstate/flask-security/issues>`_
* `Source <https://github.com/mattupstate/flask-security>`_
* `Development Version
<https://github.com/mattupstate/flask-security/raw/develop#egg=Flask-Security-dev>`_
"""
@@ -17,36 +20,35 @@ from setuptools import setup
setup(
name='Flask-Security',
version='1.2.3',
version='1.5.0-dev',
url='https://github.com/mattupstate/flask-security',
license='MIT',
author='Matthew Wright',
author='Matt Wright',
author_email='matt@nobien.net',
description='Simple security for Flask apps',
long_description=__doc__,
packages=[
'flask_security',
'flask_security.datastore'
'flask_security'
],
zip_safe=False,
include_package_data=True,
platforms='any',
install_requires=[
'Flask',
'Flask-Login',
'Flask-Principal',
'Flask-WTF',
'passlib'
'Flask>=0.8',
'Flask-Login==0.1.3',
'Flask-Mail==0.7.3',
'Flask-Principal==0.3.3',
'Flask-WTF==0.8',
'itsdangerous==0.17',
'passlib==1.6.1',
],
test_suite='nose.collector',
tests_require=[
'nose',
'Flask-SQLAlchemy',
'Flask-MongoEngine',
'py-bcrypt'
],
dependency_links=[
'http://github.com/sbook/flask-mongoengine/tarball/master#egg=Flask-MongoEngine-0.1.3-dev'
'py-bcrypt',
'simplejson'
],
classifiers=[
'Development Status :: 4 - Beta',
+79
View File
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
from unittest import TestCase
from tests.test_app.sqlalchemy import create_app
class SecurityTest(TestCase):
AUTH_CONFIG = None
def setUp(self):
super(SecurityTest, self).setUp()
app = self._create_app(self.AUTH_CONFIG or {})
app.debug = False
app.config['TESTING'] = True
self.app = app
self.client = app.test_client()
def _create_app(self, auth_config, register_blueprint=True):
return create_app(auth_config, register_blueprint)
def _get(self, route, content_type=None, follow_redirects=None, headers=None):
return self.client.get(route, follow_redirects=follow_redirects,
content_type=content_type or 'text/html',
headers=headers)
def _post(self, route, data=None, content_type=None, follow_redirects=True, headers=None):
return self.client.post(route, data=data,
follow_redirects=follow_redirects,
content_type=content_type or 'application/x-www-form-urlencoded',
headers=headers)
def register(self, email, password='password'):
data = dict(email=email, password=password)
return self.client.post('/register', data=data, follow_redirects=True)
def authenticate(self, email="matt@lp.com", password="password", endpoint=None, **kwargs):
data = dict(email=email, password=password, remember='y')
r = self._post(endpoint or '/login', data=data, **kwargs)
return r
def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None):
data = """
{
"email": "%s",
"password": "%s"
}
"""
return self._post(endpoint or '/login',
content_type="application/json",
data=data % (email, password))
def logout(self, endpoint=None):
return self._get(endpoint or '/logout', follow_redirects=True)
def assertIsHomePage(self, data):
self.assertIn('Home Page', data)
def assertIn(self, member, container, msg=None):
if hasattr(TestCase, 'assertIn'):
return TestCase.assertIn(self, member, container, msg)
return self.assertTrue(member in container)
def assertNotIn(self, member, container, msg=None):
if hasattr(TestCase, 'assertNotIn'):
return TestCase.assertNotIn(self, member, container, msg)
return self.assertFalse(member in container)
def assertIsNotNone(self, obj, msg=None):
if hasattr(TestCase, 'assertIsNotNone'):
return TestCase.assertIsNotNone(self, obj, msg)
return self.assertTrue(obj is not None)
def get_message(self, key, **kwargs):
return self.app.config['SECURITY_MSG_' + key][0] % kwargs
+483
View File
@@ -0,0 +1,483 @@
from __future__ import with_statement
import base64
import time
import simplejson as json
from flask.ext.security.utils import capture_registrations, \
capture_reset_password_requests, capture_passwordless_login_requests
from tests import SecurityTest
class ConfiguredPasswordHashSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SALT': 'so-salty',
'USER_COUNT': 1
}
def test_authenticate(self):
r = self.authenticate(endpoint="/login")
self.assertIn('Home Page', r.data)
class ConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'SECURITY_LOGOUT_URL': '/custom_logout',
'SECURITY_LOGIN_URL': '/custom_login',
'SECURITY_POST_LOGIN_VIEW': '/post_login',
'SECURITY_POST_LOGOUT_VIEW': '/post_logout',
'SECURITY_POST_REGISTER_VIEW': '/post_register',
'SECURITY_UNAUTHORIZED_VIEW': '/unauthorized',
'SECURITY_DEFAULT_HTTP_AUTH_REALM': 'Custom Realm'
}
def test_login_view(self):
r = self._get('/custom_login')
self.assertIn("<h1>Login</h1>", r.data)
def test_authenticate(self):
r = self.authenticate(endpoint="/custom_login")
self.assertIn('Post Login', r.data)
def test_logout(self):
self.authenticate(endpoint="/custom_login")
r = self.logout(endpoint="/custom_logout")
self.assertIn('Post Logout', r.data)
def test_register_view(self):
r = self._get('/register')
self.assertIn('<h1>Register</h1>', r.data)
def test_register(self):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn('Post Register', r.data)
def test_register_json(self):
data = '{ "email": "dude@lp.com", "password": "password" }'
r = self._post('/register', data=data, content_type='application/json')
data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200)
def test_register_existing_email(self):
data = dict(email='matt@lp.com',
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
msg = 'matt@lp.com is already associated with an account'
self.assertIn(msg, r.data)
def test_unauthorized(self):
self.authenticate("joe@lp.com", endpoint="/custom_auth")
r = self._get("/admin", follow_redirects=True)
msg = 'You are not allowed to access the requested resouce'
self.assertIn(msg, r.data)
def test_default_http_auth_realm(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Custom Realm"',
r.headers['WWW-Authenticate'])
class BadConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORD_HASH': 'bcrypt',
'USER_COUNT': 1
}
def test_bad_configuration_raises_runtimer_error(self):
self.assertRaises(RuntimeError, self.authenticate)
class RegisterableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'USER_COUNT': 1
}
def test_register_valid_user(self):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
self.client.post('/register', data=data, follow_redirects=True)
r = self.authenticate('dude@lp.com')
self.assertIn('Hello dude@lp.com', r.data)
class ConfirmableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'USER_COUNT': 1
}
def test_login_before_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self.authenticate(email=e)
self.assertIn(self.get_message('CONFIRMATION_REQUIRED'), r.data)
def test_send_confirmation_of_already_confirmed_account(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
self.client.get('/confirm/' + token, follow_redirects=True)
self.logout()
r = self.client.post('/confirm', data=dict(email=e))
self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data)
def test_register_sends_confirmation_email(self):
e = 'dude@lp.com'
with self.app.extensions['mail'].record_messages() as outbox:
self.register(e)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
def test_confirm_email(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
r = self.client.get('/confirm/' + token, follow_redirects=True)
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
self.assertIn(msg, r.data)
def test_invalid_token_when_confirming_email(self):
r = self.client.get('/confirm/bogus', follow_redirects=True)
self.assertIn('Invalid confirmation token', r.data)
def test_send_confirmation_json(self):
r = self._post('/confirm', data='{"email": "matt@lp.com"}',
content_type='application/json')
self.assertEquals(r.status_code, 200)
def test_send_confirmation_with_invalid_email(self):
r = self._post('/confirm', data=dict(email='bogus@bogus.com'))
self.assertIn('Specified user does not exist', r.data)
def test_resend_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self._post('/confirm', data={'email': e})
msg = self.get_message('CONFIRMATION_REQUEST', email=e)
self.assertIn(msg, r.data)
class ExpiredConfirmationTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_CONFIRM_EMAIL_WITHIN': '1 milliseconds',
'USER_COUNT': 1
}
def test_expired_confirmation_token_sends_email(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
time.sleep(1.25)
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.get('/confirm/' + token, follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertNotIn(token, outbox[0].html)
expire_text = self.AUTH_CONFIG['SECURITY_CONFIRM_EMAIL_WITHIN']
msg = self.app.config['SECURITY_MSG_CONFIRMATION_EXPIRED'][0]
msg = msg % dict(within=expire_text, email=e)
self.assertIn(msg, r.data)
class LoginWithoutImmediateConfirmTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True,
'USER_COUNT': 1
}
def test_register_valid_user_automatically_signs_in(self):
e = 'dude@lp.com'
p = 'password'
data = dict(email=e, password=p, password_confirm=p)
r = self.client.post('/register', data=data, follow_redirects=True)
self.assertIn(e, r.data)
class RecoverableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
'SECURITY_POST_FORGOT_VIEW': '/'
}
def test_reset_view(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._get('/reset/' + t)
self.assertIn('<h1>Reset password</h1>', r.data)
def test_forgot_post_sends_email(self):
with capture_reset_password_requests():
with self.app.extensions['mail'].record_messages() as outbox:
self.client.post('/reset', data=dict(email='joe@lp.com'))
self.assertEqual(len(outbox), 1)
def test_forgot_password_json(self):
r = self.client.post('/reset', data='{"email": "matt@lp.com"}',
content_type="application/json")
self.assertEquals(r.status_code, 200)
def test_forgot_password_invalid_email(self):
r = self.client.post('/reset',
data=dict(email='larry@lp.com'),
follow_redirects=True)
self.assertIn("Specified user does not exist", r.data)
def test_reset_password_with_valid_token(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
r = self.logout()
r = self.authenticate('joe@lp.com', 'newpassword')
self.assertIn('Hello joe@lp.com', r.data)
def test_reset_password_with_invalid_token(self):
r = self._post('/reset/bogus', data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn(self.get_message('INVALID_RESET_PASSWORD_TOKEN'), r.data)
class ExpiredResetPasswordTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_WITHIN': '1 milliseconds'
}
def test_reset_password_with_expired_token(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
time.sleep(1)
r = self.client.post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('You did not reset your password within', r.data)
class TrackableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_TRACKABLE': True,
'USER_COUNT': 1
}
def test_did_track(self):
e = 'matt@lp.com'
self.authenticate(email=e)
self.logout()
self.authenticate(email=e)
with self.app.test_request_context('/profile'):
user = self.app.security.datastore.find_user(email=e)
self.assertIsNotNone(user.last_login_at)
self.assertIsNotNone(user.current_login_at)
self.assertEquals('untrackable', user.last_login_ip)
self.assertEquals('untrackable', user.current_login_ip)
self.assertEquals(2, user.login_count)
class PasswordlessTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True
}
def test_login_request_for_inactive_user(self):
msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0]
r = self.client.post('/login',
data=dict(email='tiya@lp.com'),
follow_redirects=True)
self.assertIn(msg, r.data)
def test_request_login_token_with_json_and_valid_email(self):
data = '{"email": "matt@lp.com", "password": "password"}'
r = self.client.post('/login', data=data, content_type='application/json')
self.assertEquals(r.status_code, 200)
self.assertNotIn('error', r.data)
def test_request_login_token_with_json_and_invalid_email(self):
data = '{"email": "nobody@lp.com", "password": "password"}'
r = self.client.post('/login', data=data, content_type='application/json')
self.assertIn('errors', r.data)
def test_request_login_token_sends_email_and_can_login(self):
e = 'matt@lp.com'
r, user, token = None, None, None
with capture_passwordless_login_requests() as requests:
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.post('/login',
data=dict(email=e),
follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertEquals(1, len(requests))
self.assertIn('user', requests[0])
self.assertIn('login_token', requests[0])
user = requests[0]['user']
token = requests[0]['login_token']
msg = self.app.config['SECURITY_MSG_LOGIN_EMAIL_SENT'][0]
msg = msg % dict(email=user.email)
self.assertIn(msg, r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertIn(msg, r.data)
r = self.client.get('/profile')
self.assertIn('Profile Page', r.data)
def test_invalid_login_token(self):
msg = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0]
r = self._get('/login/bogus', follow_redirects=True)
self.assertIn(msg, r.data)
def test_token_login_when_already_authenticated(self):
with capture_passwordless_login_requests() as requests:
self.client.post('/login',
data=dict(email='matt@lp.com'),
follow_redirects=True)
token = requests[0]['login_token']
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertIn(msg, r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertNotIn(msg, r.data)
def test_send_login_with_invalid_email(self):
r = self._post('/login', data=dict(email='bogus@bogus.com'))
self.assertIn('Specified user does not exist', r.data)
class ExpiredLoginTokenTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
'SECURITY_LOGIN_WITHIN': '1 milliseconds',
'USER_COUNT': 1
}
def test_expired_login_token_sends_email(self):
e = 'matt@lp.com'
with capture_passwordless_login_requests() as requests:
self.client.post('/login',
data=dict(email=e),
follow_redirects=True)
token = requests[0]['login_token']
time.sleep(1.25)
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.get('/login/' + token, follow_redirects=True)
expire_text = self.AUTH_CONFIG['SECURITY_LOGIN_WITHIN']
msg = self.app.config['SECURITY_MSG_LOGIN_EXPIRED'][0]
msg = msg % dict(within=expire_text, email=e)
self.assertIn(msg, r.data)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
self.assertNotIn(token, outbox[0].html)
class AsyncMailTaskTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'USER_COUNT': 1
}
def setUp(self):
super(AsyncMailTaskTests, self).setUp()
self.mail_sent = False
def test_send_email_task_is_called(self):
@self.app.security.send_mail_task
def send_email(msg):
self.mail_sent = True
self.client.post('/reset', data=dict(email='matt@lp.com'))
self.assertTrue(self.mail_sent)
class NoBlueprintTests(SecurityTest):
AUTH_CONFIG = {
'USER_COUNT': 1
}
def _create_app(self, auth_config):
return super(NoBlueprintTests, self)._create_app(auth_config, False)
def test_login_endpoint_is_404(self):
r = self._get('/login')
self.assertEqual(404, r.status_code)
def test_http_auth_without_blueprint(self):
auth = 'Basic ' + base64.b64encode("matt@lp.com:password")
r = self._get('/http', headers={'Authorization': auth})
self.assertIn('HTTP Authentication', r.data)
+180 -82
View File
@@ -1,138 +1,236 @@
import unittest
from example import app
# -*- coding: utf-8 -*-
from __future__ import with_statement
import base64
import simplejson as json
from cookielib import Cookie
from werkzeug.utils import parse_cookie
from tests import SecurityTest
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)
def get_cookies(rv):
cookies = {}
for value in rv.headers.get_all("Set-Cookie"):
cookies.update(parse_cookie(value))
return cookies
class DefaultSecurityTests(SecurityTest):
def test_instance(self):
self.assertIsNotNone(self.app)
self.assertIsNotNone(self.app.security)
self.assertIsNotNone(self.app.security.pwd_context)
def test_login_view(self):
r = self._get('/login')
assert 'Login Page' in r.data
self.assertIn('<h1>Login</h1>', r.data)
def test_authenticate(self):
r = self.authenticate("matt", "password")
assert 'Home Page' in r.data
r = self.authenticate()
self.assertIn('Hello matt@lp.com', r.data)
def test_unprovided_username(self):
r = self.authenticate("", "password")
assert "Username not provided" in r.data
r = self.authenticate("")
self.assertIn("Email not provided", r.data)
def test_unprovided_password(self):
r = self.authenticate("matt", "")
assert "Password not provided" in r.data
r = self.authenticate(password="")
self.assertIn("Password not provided", r.data)
def test_invalid_user(self):
r = self.authenticate("bogus", "password")
assert "Specified user does not exist" in r.data
r = self.authenticate(email="bogus@bogus.com")
self.assertIn("Specified user does not exist", r.data)
def test_bad_password(self):
r = self.authenticate("matt", "bogus")
assert "Password does not match" in r.data
r = self.authenticate(password="bogus")
self.assertIn("Invalid password", r.data)
def test_inactive_user(self):
r = self.authenticate("tiya", "password")
assert "Inactive user" in r.data
r = self.authenticate("tiya@lp.com", "password")
self.assertIn("Account is disabled", r.data)
def test_logout(self):
self.authenticate("matt", "password")
self.authenticate()
r = self.logout()
assert 'Home Page' in r.data
self.assertIsHomePage(r.data)
def test_unauthorized_access(self):
r = self._get('/profile', follow_redirects=True)
assert 'Please log in to access this page' in r.data
self.assertIn('Please log in to access this page', r.data)
def test_authorized_access(self):
self.authenticate("matt", "password")
self.authenticate()
r = self._get("/profile")
assert 'profile' in r.data
self.assertIn('profile', r.data)
def test_valid_admin_role(self):
self.authenticate("matt", "password")
self.authenticate()
r = self._get("/admin")
assert 'Admin Page' in r.data
self.assertIn('Admin Page', r.data)
def test_invalid_admin_role(self):
self.authenticate("joe", "password")
self.authenticate("joe@lp.com")
r = self._get("/admin", follow_redirects=True)
assert 'Home Page' in r.data
self.assertIsHomePage(r.data)
def test_roles_accepted(self):
for user in ("matt", "joe"):
self.authenticate(user, "password")
for user in ("matt@lp.com", "joe@lp.com"):
self.authenticate(user)
r = self._get("/admin_or_editor")
self.assertIn('Admin or Editor Page', r.data)
self.logout()
self.authenticate("jill", "password")
self.authenticate("jill@lp.com")
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('<input id="next"', r.data)
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')
class ConfiguredSecurityTests(SecurityTest):
self.authenticate('dave@lp.com')
r = self._get("/admin_and_editor", follow_redirects=True)
self.assertIn('Admin and Editor Page', r.data)
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_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_login_view(self):
r = self._get('/custom_login')
assert "Custom Login Page" in r.data
def test_invalid_json_auth(self):
r = self.json_authenticate(password='junk')
self.assertIn('"code": 400', r.data)
def test_authenticate(self):
r = self.authenticate("matt", "password", endpoint="/custom_auth")
assert 'Post Login' in 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_logout(self):
self.authenticate("matt", "password", endpoint="/custom_auth")
r = self.logout(endpoint="/custom_logout")
assert 'Post Logout' in r.data
def test_token_auth_via_header_valid_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
headers = {"Authentication-Token": token}
r = self._get('/token', headers=headers)
self.assertIn('Token Authentication', r.data)
def test_token_auth_via_querystring_invalid_token(self):
r = self._get('/token?auth_token=X')
self.assertEqual(401, r.status_code)
def test_token_auth_via_header_invalid_token(self):
r = self._get('/token', headers={"Authentication-Token": 'X'})
self.assertEqual(401, r.status_code)
def test_http_auth(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password")
})
self.assertIn('HTTP Authentication', r.data)
def test_invalid_http_auth_invalid_username(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("bogus:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Login Required"',
r.headers['WWW-Authenticate'])
def test_invalid_http_auth_bad_password(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Login Required"',
r.headers['WWW-Authenticate'])
def test_custom_http_auth_realm(self):
r = self._get('/http_custom_realm', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="My Realm"',
r.headers['WWW-Authenticate'])
def test_user_deleted_during_session_reverts_to_anonymous_user(self):
self.authenticate()
with self.app.test_request_context('/'):
user = self.app.security.datastore.find_user(email='matt@lp.com')
self.app.security.datastore.delete_user(user)
self.app.security.datastore.commit()
r = self._get('/')
self.assertNotIn('Hello matt@lp.com', r.data)
def test_remember_token(self):
r = self.authenticate(follow_redirects=False)
self.client.cookie_jar.clear_session_cookies()
r = self._get('/profile')
self.assertIn('profile', r.data)
def test_token_loader_does_not_fail_with_invalid_token(self):
c = Cookie(version=0, name='remember_token', value='None', port=None,
port_specified=False, domain='www.example.com',
domain_specified=False, domain_initial_dot=False, path='/',
path_specified=True, secure=False, expires=None,
discard=True, comment=None, comment_url=None,
rest={'HttpOnly': None}, rfc2109=False)
self.client.cookie_jar.set_cookie(c)
r = self._get('/')
self.assertNotIn('BadSignature', r.data)
class MongoEngineSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config):
return app.create_mongoengine_app(auth_config)
from tests.test_app.mongoengine import create_app
return create_app(auth_config)
class DefaultDatastoreTests(SecurityTest):
def test_add_role_to_user(self):
r = self._get('/coverage/add_role_to_user')
self.assertIn('success', r.data)
def test_remove_role_from_user(self):
r = self._get('/coverage/remove_role_from_user')
self.assertIn('success', r.data)
def test_activate_user(self):
r = self._get('/coverage/activate_user')
self.assertIn('success', r.data)
def test_deactivate_user(self):
r = self._get('/coverage/deactivate_user')
self.assertIn('success', r.data)
def test_invalid_role(self):
r = self._get('/coverage/invalid_role')
self.assertIn('success', r.data)
class MongoEngineDatastoreTests(DefaultDatastoreTests):
def _create_app(self, auth_config):
from tests.test_app.mongoengine import create_app
return create_app(auth_config)
+165
View File
@@ -0,0 +1,165 @@
# -*- coding: utf-8 -*-
from flask import Flask, render_template, current_app
from flask.ext.mail import Mail
from flask.ext.security import login_required, roles_required, roles_accepted
from flask.ext.security.decorators import http_auth_required, \
auth_token_required
from flask.ext.security.utils import encrypt_password
from werkzeug.local import LocalProxy
ds = LocalProxy(lambda: current_app.extensions['security'].datastore)
def create_app(config):
app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'secret'
for key, value in config.items():
app.config[key] = value
mail = Mail(app)
app.extensions['mail'] = mail
@app.route('/')
def index():
return render_template('index.html', content='Home Page')
@app.route('/profile')
@login_required
def profile():
return render_template('index.html', content='Profile Page')
@app.route('/post_login')
@login_required
def post_login():
return render_template('index.html', content='Post Login')
@app.route('/http')
@http_auth_required
def http():
return 'HTTP Authentication'
@app.route('/http_custom_realm')
@http_auth_required('My Realm')
def http_custom_realm():
return render_template('index.html', content='HTTP Authentication')
@app.route('/token')
@auth_token_required
def token():
return render_template('index.html', content='Token Authentication')
@app.route('/post_logout')
def post_logout():
return render_template('index.html', content='Post Logout')
@app.route('/post_register')
def post_register():
return render_template('index.html', content='Post Register')
@app.route('/admin')
@roles_required('admin')
def admin():
return render_template('index.html', content='Admin Page')
@app.route('/admin_and_editor')
@roles_required('admin', 'editor')
def admin_and_editor():
return render_template('index.html', content='Admin and Editor Page')
@app.route('/admin_or_editor')
@roles_accepted('admin', 'editor')
def admin_or_editor():
return render_template('index.html', content='Admin or Editor Page')
@app.route('/unauthorized')
def unauthorized():
return render_template('unauthorized.html')
@app.route('/coverage/add_role_to_user')
def add_role_to_user():
u = ds.find_user(email='joe@lp.com')
r = ds.find_role('admin')
ds.add_role_to_user(u, r)
return 'success'
@app.route('/coverage/remove_role_from_user')
def remove_role_from_user():
u = ds.find_user(email='matt@lp.com')
ds.remove_role_from_user(u, 'admin')
return 'success'
@app.route('/coverage/deactivate_user')
def deactivate_user():
u = ds.find_user(email='matt@lp.com')
ds.deactivate_user(u)
return 'success'
@app.route('/coverage/activate_user')
def activate_user():
u = ds.find_user(email='tiya@lp.com')
ds.activate_user(u)
return 'success'
@app.route('/coverage/invalid_role')
def invalid_role():
return 'success' if ds.find_role('bogus') is None else 'failure'
return app
def create_roles():
for role in ('admin', 'editor', 'author'):
ds.create_role(name=role)
ds.commit()
def create_users(count=None):
users = [('matt@lp.com', 'password', ['admin'], True),
('joe@lp.com', 'password', ['editor'], True),
('dave@lp.com', 'password', ['admin', 'editor'], True),
('jill@lp.com', 'password', ['author'], True),
('tiya@lp.com', 'password', [], False)]
count = count or len(users)
for u in users[:count]:
pw = encrypt_password(u[1])
ds.create_user(email=u[0], password=pw,
roles=u[2], active=u[3])
ds.commit()
def populate_data(user_count=None):
create_roles()
create_users(user_count)
def add_context_processors(s):
@s.context_processor
def for_all():
return dict()
@s.forgot_password_context_processor
def forgot_password():
return dict()
@s.login_context_processor
def login():
return dict()
@s.register_context_processor
def register():
return dict()
@s.reset_password_context_processor
def reset_password():
return dict()
@s.send_confirmation_context_processor
def send_confirmation():
return dict()
@s.send_login_context_processor
def send_login():
return dict()
@s.mail_context_processor
def mail():
return dict()
+56
View File
@@ -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):
app = create_base_app(config)
app.config['MONGODB_SETTINGS'] = dict(
db='flask_security_test',
host='localhost',
port=27017
)
db = MongoEngine(app)
class Role(db.Document, RoleMixin):
name = db.StringField(required=True, unique=True, max_length=80)
description = db.StringField(max_length=255)
class User(db.Document, UserMixin):
email = db.StringField(unique=True, max_length=255)
password = db.StringField(required=True, max_length=255)
last_login_at = db.DateTimeField()
current_login_at = db.DateTimeField()
last_login_ip = db.StringField(max_length=100)
current_login_ip = db.StringField(max_length=100)
login_count = db.IntField()
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
@app.before_first_request
def before_first_request():
User.drop_collection()
Role.drop_collection()
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, MongoEngineUserDatastore(db, User, Role))
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
+61
View File
@@ -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, register_blueprint=True):
app = create_base_app(config)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
db = SQLAlchemy(app)
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
last_login_at = db.Column(db.DateTime())
current_login_at = db.Column(db.DateTime())
last_login_ip = db.Column(db.String(100))
current_login_ip = db.Column(db.String(100))
login_count = db.Column(db.Integer)
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
@app.before_first_request
def before_first_request():
db.drop_all()
db.create_all()
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, SQLAlchemyUserDatastore(db, User, Role),
register_blueprint=register_blueprint)
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
@@ -12,9 +12,9 @@
{% endif -%}
<li>
{%- if current_user.is_authenticated() -%}
<a href="{{ url_for('auth.logout') }}">Log out</a>
<a href="{{ url_for('security.logout') }}">Log out</a>
{%- else -%}
<a href="{{ url_for('login') }}">Log in</a>
<a href="{{ url_for('security.login') }}">Log in</a>
{%- endif -%}
</li>
</ul>
+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>
+52 -10
View File
@@ -1,29 +1,26 @@
# -*- coding: utf-8 -*-
import unittest
import flask_security
from flask_security import RoleMixin, UserMixin, AnonymousUser
from flask_security.datastore import Datastore, UserDatastore
class Role(RoleMixin):
def __init__(self, name, description=None):
def __init__(self, name):
self.name = name
self.description = description
class User(UserMixin):
def __init__(self, username, email, roles):
self.username = username
def __init__(self, email, roles):
self.email = email
self.roles = roles
# set the models or we'll get errors
flask_security.User = User
flask_security.Role = Role
admin = Role('admin')
admin2 = Role('admin')
editor = Role('editor')
user = User('matt', 'matt@lp.com', [admin, editor])
user = User('matt@lp.com', [admin, editor])
class SecurityEntityTests(unittest.TestCase):
@@ -44,3 +41,48 @@ class SecurityEntityTests(unittest.TestCase):
au = AnonymousUser()
self.assertEqual(0, len(au.roles))
self.assertFalse(au.has_role('admin'))
class DatastoreTests(unittest.TestCase):
def setUp(self):
super(DatastoreTests, self).setUp()
self.ds = UserDatastore(None, None)
def test_unimplemented_datastore_methods(self):
ds = Datastore(None)
self.assertRaises(NotImplementedError, ds.put, None)
self.assertRaises(NotImplementedError, ds.delete, None)
def test_unimplemented_user_datastore_methods(self):
self.assertRaises(NotImplementedError, self.ds.find_user)
self.assertRaises(NotImplementedError, self.ds.find_role)
def test_toggle_active(self):
user.active = True
rv = self.ds.toggle_active(user)
self.assertTrue(rv)
self.assertFalse(user.active)
rv = self.ds.toggle_active(user)
self.assertTrue(rv)
self.assertTrue(user.active)
def test_deactivate_user(self):
user.active = True
rv = self.ds.deactivate_user(user)
self.assertTrue(rv)
self.assertFalse(user.active)
def test_activate_user(self):
ds = UserDatastore(None, None)
user.active = False
ds.activate_user(user)
self.assertTrue(user.active)
def test_deactivate_returns_false_if_already_false(self):
user.active = False
self.assertFalse(self.ds.deactivate_user(user))
def test_activate_returns_false_if_already_true(self):
user.active = True
self.assertFalse(self.ds.activate_user(user))