mirror of
https://github.com/wassname/flask-security.git
synced 2026-06-27 16:10:11 +08:00
Fix merge conflicts and fix release.py
This commit is contained in:
+24
-5
@@ -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
@@ -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
|
||||
@@ -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,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
@@ -1 +1,2 @@
|
||||
include tests/*.py
|
||||
recursive-include tests *.py
|
||||
recursive-include flask_security/templates *.*
|
||||
@@ -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
@@ -0,0 +1,15 @@
|
||||
Flask-Security
|
||||
==============
|
||||
|
||||
.. image:: https://secure.travis-ci.org/mattupstate/flask-security.png?branch=develop
|
||||
|
||||
Flask-Security quickly adds security features to your Flask application.
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
- `Documentation <http://packages.python.org/Flask-Security/>`_
|
||||
- `Issue Tracker <http://github.com/mattupstate/flask-security/issues>`_
|
||||
- `Code <http://github.com/mattupstate/flask-security/>`_
|
||||
- `Development Version
|
||||
<http://github.com/mattupstate/flask-security/zipball/develop#egg=Flask-Security-dev>`_
|
||||
@@ -0,0 +1,297 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="184.621px" height="267.873px" viewBox="0 0 184.621 267.873" style="enable-background:new 0 0 184.621 267.873;"
|
||||
xml:space="preserve">
|
||||
<path d="M42.3,6.712c0.374-3.417,6.743-3.01,6.24,0C48.174,8.905,43.253,8.469,42.3,6.712z M46.14,7.432
|
||||
c0.102-0.539,1.273-0.006,1.44-0.48c-0.011-0.331,0.113-0.527,0.24-0.72c-0.386-1.446-4.505-1.467-4.32,0.479
|
||||
c0.463,0.287,1.217,0.915,1.68,0.48c0.409-0.099-0.051-0.31,0-0.72c0.689-0.053,1.668-1.192,1.92-0.24
|
||||
C47.161,6.872,45.517,7.023,46.14,7.432z"/>
|
||||
<path d="M61.02,8.872c-1.514,1.428-5.308,0.636-6.239-0.72C54.424,4.301,62.875,4.691,61.02,8.872z M55.98,8.152
|
||||
c0.547,0.174,1.227,0.214,1.681,0.48c-0.31-1.429,1.293-0.947,2.16-1.2c0.022,0.377-0.032,0.832,0.479,0.72
|
||||
C60.766,5.738,55.542,5.836,55.98,8.152z"/>
|
||||
<path d="M75.18,11.272c-2.44,1.066-5.342-0.024-6.479-1.68C69,5.478,77.3,6.38,75.18,11.272z M70.14,9.352
|
||||
c0.348,0.453,0.536,1.064,1.44,0.96c-0.42-1.412,1.778-1.107,2.16-0.96c-0.09,0.79-1.276,0.484-1.2,1.44
|
||||
c1.447,0.248,1.821-0.578,2.16-1.44C74.528,7.553,70.227,7.19,70.14,9.352z"/>
|
||||
<path d="M38.701,9.352c-0.632,0.249-1.473,0.287-2.4,0.24C36.181,8.614,38.749,8.071,38.701,9.352z"/>
|
||||
<path d="M44.94,9.352c-0.336,1.452-3.19,0.276-4.319,0.24C41.392,8.351,43.547,9.639,44.94,9.352z"/>
|
||||
<path d="M51.661,10.792c-2.162,0.242-3.118-0.722-5.28-0.48C46.883,9.482,51.405,9.765,51.661,10.792z"/>
|
||||
<path d="M86.22,16.072c-2.685,0.164-5.312-0.956-5.28-2.88C81.011,8.895,91.422,11.543,86.22,16.072z M82.14,13.192
|
||||
c0.572,1.569,2.509,2.415,4.32,1.44c0.099-1.059-0.065-1.855-0.48-2.4C84.701,11.79,82.343,11.814,82.14,13.192z"/>
|
||||
<path d="M60.3,11.992c-1.742,0.618-4.228-0.089-5.76-0.48C55.288,10.927,59.201,11.604,60.3,11.992z"/>
|
||||
<path d="M66.781,13.672c-1.624-0.216-3.132-0.548-4.561-0.96C63.243,11.997,65.987,12.678,66.781,13.672z"/>
|
||||
<path d="M85.98,13.432c0.126,1.215-1.898,1.554-2.399,0.72C83.832,13.513,85.72,13.266,85.98,13.432z"/>
|
||||
<path d="M70.14,14.392c3.491-0.13,7.434,1.367,11.04,1.92c-0.615,0.954-2.605-0.277-3.6-0.48
|
||||
C74.54,15.913,72.676,14.816,70.14,14.392z"/>
|
||||
<path d="M84.3,16.792c1.247,0.273,2.713,0.327,3.84,0.72C87.493,17.593,84.601,17.856,84.3,16.792z"/>
|
||||
<path d="M43.5,20.632c-0.992,0.528-1.736,1.304-1.68,2.88c-0.07,1.03,1.131,0.789,1.199,1.68
|
||||
C40.576,24.951,40.535,20.329,43.5,20.632z"/>
|
||||
<path d="M43.98,20.872c2.421-0.874,2.685,4.746-0.479,4.08c0.299-0.661,1.226-0.694,1.68-1.2
|
||||
C45.374,22.199,45.004,21.208,43.98,20.872z"/>
|
||||
<path d="M32.46,29.752c-0.48,0.342,0.184,1.561,0.72,1.68c-1.511,0.933-1.827-2.226-0.96-3.12c3.597-2.01,5.532,3.849,1.44,3.6
|
||||
C33.964,30.488,32.242,31.091,32.46,29.752z"/>
|
||||
<path d="M25.741,33.112c1.15,0.084-0.513,0.719-0.48,1.2c0.018,1.183-0.203,2.604,0.96,2.64c-0.592-0.841,0.243-2.458,0.72-3.6
|
||||
c2.731,0.658,1.781,4.199-0.239,4.32C24.472,37.806,23.329,34.219,25.741,33.112z"/>
|
||||
<path d="M38.22,42.712c-0.253-1.106-1.293-1.427-0.96-3.12c-0.61,0.029-0.931,0.349-0.96,0.96
|
||||
C35.887,35.461,44.143,41.79,38.22,42.712z"/>
|
||||
<path d="M47.1,46.072c-1.321-2.109-1.041-5.914,2.88-4.8C52.478,43.065,50.231,46.405,47.1,46.072z M46.861,42.472
|
||||
c0.049,0.751-0.162,1.762,0.239,2.16c0.739,0.179,0.462-0.659,1.2-0.48c-0.267,0.753-0.112,0.634,0.24,1.2
|
||||
C52.363,44.767,48.946,39.561,46.861,42.472z"/>
|
||||
<path d="M37.5,42.232C36.934,43.004,36.53,41.388,37.5,42.232L37.5,42.232z"/>
|
||||
<path d="M30.781,42.952c0.56,0,1.12,0,1.68,0c1.986,1.451,0.51,4.935-2.4,3.6c-0.156-0.835-0.882-2.003-0.239-2.88
|
||||
c0.47,0.97-0.006,2.885,1.68,2.64C31.005,45.534,30.001,44.231,30.781,42.952z"/>
|
||||
<path d="M4.861,47.512c0.233,0.006,0.186,0.293,0.239,0.48c-0.204,1.471-1.334,2.713-1.92,4.08c-0.557,1.3-0.519,3.166-1.92,3.84
|
||||
C1.854,52.506,3.317,49.968,4.861,47.512z"/>
|
||||
<path d="M54.3,62.632c2.747,3.82-5.6,6.558-4.08,0.72C50.871,62.01,52.984,62.126,54.3,62.632z M53.821,63.592
|
||||
c-1.709-2.567-5,1.958-2.16,1.92c-0.536-0.474,1.761-1.205,1.68,0C53.614,64.986,53.959,64.531,53.821,63.592z"/>
|
||||
<path d="M53.821,85.192c0.891-1.578,4.269-2.012,5.279-0.24C60.584,90.521,52.591,90.472,53.821,85.192z M57.901,84.952
|
||||
c-0.284-0.116-0.545-0.256-0.721-0.48c-2.651-0.412-3.296,2.625-1.68,3.84C57.95,88.96,58.699,86.766,57.901,84.952z"/>
|
||||
<path d="M56.701,86.152c0.16,0,0.319,0,0.479,0c0,0.16,0,0.32,0,0.48c-0.16,0-0.319,0-0.479,0
|
||||
C56.701,86.472,56.701,86.312,56.701,86.152z"/>
|
||||
<path d="M55.5,87.352c0.621-0.306,1.539-0.306,2.16,0C57.558,88.531,55.73,88.054,55.5,87.352z"/>
|
||||
<path d="M47.821,95.272c0.654,3.768-3.346,3.171-3.601,1.2C43.964,94.484,46.394,93.683,47.821,95.272z M45.901,97.432
|
||||
c0.159-0.334-0.05-1.905-0.721-2.16C44.746,95.882,44.954,97.453,45.901,97.432z"/>
|
||||
<path d="M40.861,98.632c0.105,1.306-0.608,1.792-0.96,2.64c-0.801,0-1.601,0-2.4,0C35.321,99.4,38.75,95.462,40.861,98.632z
|
||||
M38.94,101.032c-0.208-0.752-0.594-1.325-1.199-1.68C37.674,100.378,38.437,100.576,38.94,101.032z"/>
|
||||
<path d="M61.5,108.232c0.545,1.2,0.064,2.764-0.24,3.84C55.917,115.484,55.839,104.695,61.5,108.232z M57.661,110.632
|
||||
c0.652,3.805,5.875-1.417,1.439-2.4C58.46,108.886,57.509,109.749,57.661,110.632z"/>
|
||||
<path d="M40.621,113.512c-0.878-0.801-1.726-1.134-1.44-2.64c0.344-1.811,3.449-2.23,3.84-0.24
|
||||
C43.258,111.84,42.221,112.906,40.621,113.512z M40.861,112.552c-0.223-0.578-0.438-1.162-0.721-1.68
|
||||
c-0.075,0.325-0.342,0.458-0.239,0.96C40.671,111.621,40.229,112.623,40.861,112.552z"/>
|
||||
<path d="M59.581,109.672c0.295,0.105,0.493,0.308,0.479,0.72c-0.388,0.068-0.413-0.227-0.72-0.24
|
||||
C59.386,109.958,59.582,109.913,59.581,109.672z"/>
|
||||
<path d="M58.621,110.632c0.744-0.024,1.146,0.293,1.68,0.48C60.05,111.75,58.254,111.922,58.621,110.632z"/>
|
||||
<path d="M64.621,135.353c0.086-1.606-0.272-2.769-0.96-3.601c-3.562-0.15-7.859,0.044-11.28,0.72c0.133,0.987,0.688,1.552,0.72,2.64
|
||||
c-0.504,0.299-1.229,1.1-1.92,0.479c-0.299-1.02,0.799-0.642,0.96-1.2c0.019-1.458-0.993-1.886-0.72-3.6
|
||||
c1.929,0.088,2.346-0.335,3.36,0.24c0.645,0.165,0.464-0.496,0.72-0.72c0.646-0.165,0.464,0.496,0.72,0.72
|
||||
c0.461-0.957,1.359-0.047,1.681-0.96c0.645-0.166,0.463,0.496,0.72,0.72c0.422-0.058,0.547-0.413,0.479-0.96
|
||||
c0.505,0.528,0.937,0.528,1.44,0c0.523-0.043,0.764,0.196,0.72,0.72c0.946-0.028,0.734-1.043,1.68-0.479
|
||||
c0.631-0.161,0.505,0.614,0.721,0.72c0.215,0.105,0.423-0.477,0.479-0.48c0.373-0.021,0.549,1.291,1.92,0.72
|
||||
c0.171,1.864,0.423,2.749,0.721,4.08c3.811,0.014,0.37-4.601,0-6.24c-5.258-0.676-12.689,0.141-17.521,1.44
|
||||
c0.228,1.728,1.729,3.344,1.2,5.041c-1.065-1.496-2.048-3.073-2.4-5.28c0.233-0.007,0.187-0.294,0.24-0.48
|
||||
c4.899-1.854,12.457-2.218,18.48-2.16c0.611,0.645,1.104,1.897,1.68,3.36c0.457,1.163,1.41,2.914,1.2,3.84
|
||||
C69.249,136.444,66.248,137.033,64.621,135.353z"/>
|
||||
<path d="M44.22,138.231c0.101,2.525-3.294,3.207-5.04,2.4C39.008,138.113,42.058,137.625,44.22,138.231z M40.14,140.394
|
||||
c1.207,0.246,1.316-0.604,1.92-0.961c0.345,0.366,1.251,0.233,1.2-0.479C42.089,138.349,39.939,138.825,40.14,140.394z"/>
|
||||
<path d="M64.38,138.474c3.496-0.535,7.481,0.518,10.08,1.68c-0.366,1.195-1.776,0.125-2.4,0
|
||||
C69.596,139.659,66.998,138.842,64.38,138.474z"/>
|
||||
<path d="M95.581,143.271c-2.942,0.644-5.514-1.002-6-3.6C91.774,136.823,96.583,138.774,95.581,143.271z M91.98,139.192
|
||||
c-0.631,0.644-1.162,2.258,0.24,2.398c-0.14-0.859,0.711-0.729,1.44-0.72c0.058,0.343,0.395,0.405,0.479,0.72
|
||||
c-0.166,0.475-1.339-0.059-1.438,0.48c0.669,0.305,1.187,0.592,1.92,0.24C94.852,140.16,93.672,139.42,91.98,139.192z"/>
|
||||
<path d="M32.22,145.192c-0.032,2.367-1.528,3.271-3.84,3.359C27.594,146.242,29.819,143.58,32.22,145.192z M29.1,147.832
|
||||
c1.404-0.115,1.685-1.354,2.4-2.16C30.131,144.871,28.847,146.213,29.1,147.832z"/>
|
||||
<path d="M86.701,146.151c1.792,0.688,3.44,1.52,4.56,2.881C89.319,148.494,87.486,147.847,86.701,146.151z"/>
|
||||
<path d="M114.061,170.151c-0.939,0.369-1.713-1.262-2.16-2.159c-3-4.039-7.217-6.863-10.319-10.8
|
||||
c-3.623-1.817-5.706-5.176-9.36-6.961c0.085-0.274,0.423-0.607,0.48-0.239c3.505,1.541,6.238,4.271,9.119,6.72
|
||||
c1.355,1.153,2.771,2.489,4.08,3.841C108.777,163.521,112.146,165.974,114.061,170.151z"/>
|
||||
<path d="M114.541,155.271c-2.948-0.092-4.854-1.227-4.8-4.318C112.334,147.631,117.725,152.144,114.541,155.271z M110.939,151.433
|
||||
c0.082,1.918,1.41,2.592,3.121,2.881c0.135-0.347,0.375-0.586,0.721-0.722C115.197,151.412,112.15,149.688,110.939,151.433z"/>
|
||||
<path d="M111.9,152.151c0.914,0.046,1.58,0.342,1.681,1.199C112.934,153.824,111.773,153.204,111.9,152.151z"/>
|
||||
<path d="M130.621,163.912c-2.326,0.858-4.162-0.562-4.32-2.642C127.866,158.646,132.686,160.926,130.621,163.912z M127.741,161.992
|
||||
c1.332-0.946,1.198,0.989,1.68,1.2c0.062-0.338,0.526-0.274,0.72-0.48c-0.092-0.788-0.701-1.059-0.959-1.68
|
||||
C128.372,160.879,127.077,161.467,127.741,161.992z"/>
|
||||
<path d="M146.939,179.992c-3.486,2.58-6.062-3.995-3.358-5.04C146.005,174.017,147.825,177.625,146.939,179.992z M145.741,179.512
|
||||
c1.25-1.596-0.574-3.557-1.921-4.079C143.225,177.119,143.824,179.551,145.741,179.512z"/>
|
||||
<path d="M144.061,177.353c0.805-0.164,0.877,0.402,0.959,0.961C144.27,178.424,143.949,178.104,144.061,177.353z"/>
|
||||
<path d="M153.42,206.151c0.619,0.422,0.499,1.582,0.721,2.4C153.458,208.192,153.473,207.14,153.42,206.151z"/>
|
||||
<path d="M170.701,263.271c1.24,0.039,1.096,1.465,2.64,1.201c0.423-0.059,0.548-0.414,0.479-0.961c0.088-1.207-2.1-0.139-1.68-1.68
|
||||
c1.793-0.824,3.146,0.895,3.119,1.92C175.193,266.483,169.829,266.367,170.701,263.271z"/>
|
||||
<path d="M166.621,263.271c0.523,0.037,0.516,0.605,0.479,1.201C166.387,264.522,166.254,263.616,166.621,263.271z"/>
|
||||
<path d="M29.581,13.432c-1.118,1.762-2.535,3.225-4.561,4.08c-0.978,1.865-3.021,2.788-4.319,4.32
|
||||
c-0.634,0.748-1.043,1.827-1.681,2.64c-0.617,0.788-1.547,1.365-2.159,2.16c-1.095,1.42-1.889,3.176-2.881,4.8
|
||||
c-2.798,4.579-5.413,9.826-8.159,14.88c-0.323-1.007,0.498-2.206,0.96-3.12c1.375-2.724,2.946-5.975,4.56-8.64
|
||||
c1.747-4.172,4.961-6.879,6.96-10.8c2.189-1.661,3.682-4.135,5.76-6c0.666-0.597,1.669-0.798,2.4-1.44
|
||||
C27.522,15.38,27.938,13.633,29.581,13.432z"/>
|
||||
<path d="M150.061,192.231c1.83,1.771,1.967,5.232,2.399,8.4C151.748,197.745,150.889,195.005,150.061,192.231z"/>
|
||||
<path d="M152.46,200.872c0.647,1.271,0.92,2.92,0.96,4.8C152.854,204.317,152.702,202.551,152.46,200.872z"/>
|
||||
<path d="M48.541,110.872c-3.429-3.012,2.593-5.527,2.88-2.16C51.547,110.201,50.337,110.811,48.541,110.872z M48.3,109.672
|
||||
c0.668,0.385,0.664-1.036,0.24-0.96C48.465,109.037,48.199,109.17,48.3,109.672z"/>
|
||||
<path d="M21.42,160.792c1.934-1.906,2.754-4.927,5.28-6.239c-2.298,3.544-5.058,6.622-6.48,11.039
|
||||
c3.271-4.889,6.15-10.17,10.561-13.92c0.353-0.912-1.073-0.047-0.721-0.96c2.43-1.57,3.76-4.24,6.96-5.04
|
||||
c-0.101,0.779-0.997,0.763-1.199,1.44c2.234-1.125,3.954-2.766,6.479-3.602c0.562-0.078,0.233-1.047,0.72-1.199
|
||||
c3.039-1.44,6.383-2.578,10.561-2.881c0.624,0.633-0.388,0.189-0.24,0.961c0.72,0,1.44,0,2.16,0c-1.44-1.789-1.267-3.211,0.24-5.76
|
||||
c0.573-0.972,1.111-2.216,1.92-2.4c0.204,2.628-2.214,3.833-1.92,5.76c1.9,0.694,4.79-0.966,6.96,0
|
||||
c-0.317,1.442-3.182,0.339-4.32,0.96c6.704,0.176,12.813,0.945,17.521,3.12c3.945-1.339,7.001,2.024,9.6,3.6
|
||||
c0.481,0.488,0.062,0.64-0.24,0.961c4.866,3.396,9.953,6.079,14.4,10.079c2.279,2.051,4.136,3.671,6.24,6
|
||||
c0.603,0.668,1.541,1.234,2.158,1.921c0.623,0.689,0.996,1.632,1.682,2.399c1.046,1.172,2.52,1.681,2.88,3.601
|
||||
c3.112,1.927,6.022,4.056,10.08,5.039c-2.345-1.815-5.804-2.518-8.16-4.319c1.046-0.662,1.854,0.629,2.64,0.961
|
||||
c0.989,0.416,2.129,0.528,3.12,0.959c1.798,0.779,3.322,1.97,5.04,2.881c4.528,2.402,9.151,4.309,14.16,6.479
|
||||
c-0.441,1-0.904,0.837,0.24,1.439c0.377-0.503,0.219-2.104,0-3.12c-0.719-3.334-2.582-6.436-4.08-9.602
|
||||
c-1.439-3.043-3.422-6.012-3.84-9.6c0.725-1.034,2-1.52,3.359-1.92c-1.265-7.012-1.57-15.238,0.239-22.32
|
||||
c0.979-3.827,2.716-7.71,4.08-11.76c1.356-4.028,2.086-8.338,3.36-12.48c1.813-5.897,4.24-11.626,6-17.76
|
||||
c0.576-2.014,1.359-3.847,1.92-5.76c0.439-1.503,0.457-3.101,0.96-4.56c2.241-6.504,4.963-13.056,7.92-19.2
|
||||
c1.229-2.554,2.055-5.326,3.358-7.44c0.334-0.541,1.066-0.881,1.44-1.44c0.547-0.818,0.683-1.998,1.2-2.88
|
||||
c0.468-0.798,1.158-1.408,1.68-2.16c1.143-1.652,2.04-3.624,3.12-4.8c2.771-3.019,6.896-4.626,9.12-6.96
|
||||
c-1.417-0.79-3.008-0.504-4.561-0.72c-15.819-2.198-31.188-5.902-45.119-10.08c-6.028-1.02-12.305-2.654-18-4.32
|
||||
c-4.121-1.205-7.994-2.168-12-3.36c-1.084-0.322-2.811-0.442-3.36-1.68c-1.199,0.555-2.575-0.564-3.84-0.72
|
||||
c-1.944-2.305-1.838-4.593-3.36-6.96c-2.579-4.011-10.535-4.885-16.8-6c-5.283-0.94-11.398-1.612-17.28-1.92
|
||||
c-2.405-0.126-4.836,0.255-6.72,0c-0.96-0.13-1.744-0.689-2.64-0.72c-4.317-0.151-7.787,3.14-10.801,4.08
|
||||
c1.681,8.378,5.883,16.13,8.16,24.72c0.181,0.679,0.077,1.48,0.24,2.16c1.192,4.97,2.028,11.915,2.88,18.24
|
||||
c0.325,2.417,0.567,4.839,0.96,6.96c0.995,5.377,1.769,10.711,2.64,16.08c0.621,3.823,1.868,7.249,1.921,11.28
|
||||
c0.023,1.867,0.833,3.842,1.199,5.76c1.145,5.99,1.941,11.323,3.12,17.76c0.664,3.625,1.143,6.605,1.92,10.56
|
||||
c0.213,1.082,0.807,3.903,0.48,5.76c-1.362-0.676-0.612-2.755-0.72-3.84c-0.321-3.231-1.526-6.321-2.16-9.6
|
||||
c-0.812-4.2-1.122-8.545-1.92-12.96c-0.743-4.109-1.776-8.241-2.4-12.48c-0.624-4.238-1.288-8.525-2.16-12.72
|
||||
c-0.891-4.285-1.224-8.516-1.92-12.72c-1.09-6.581-2.213-13.001-2.88-19.44c-1.357-13.099-6.619-24.24-10.8-34.56
|
||||
c-1.124,0.297-2.572,1.345-3.601,0.96c1.537-1.967,4.458-2.744,7.2-4.08c3.129-1.523,6.393-3.895,10.32-2.64
|
||||
c9.006-3.378,18.614-3.365,29.04-2.88c3.564,0.166,6.843,0.097,10.08,0.72c2.062,0.397,4.294,0.803,6.479,1.44
|
||||
c0.57,0.166,1.107,0.608,1.681,0.72c0.588,0.115,1.326-0.156,1.92,0c3.017,0.792,6.084,2.503,9.121,3.6
|
||||
c9.119,3.295,17.252,8.793,24,14.16c13.316,2.752,27.009,5.719,39.84,9.12c3.088,0.819,6.115,2.127,9.119,3.12
|
||||
c2.354,0.778,4.222,0.81,5.761,2.4c0,1.12,0,2.24,0,3.36c-1.306,1.416-2.587,2.854-4.8,3.36c-1.762,1.354-3.555,2.489-5.28,3.84
|
||||
c-4.033,3.158-6.948,7.572-9.601,12.24c-2.646,4.656-5.75,9.431-6.961,14.88c-3.103,4.097-3.572,10.826-6,15.6
|
||||
c-0.846,4.674-2.132,8.907-3.84,12.72c-3.023,12.093-7.494,23.404-10.8,36c-1.067,4.07-2.05,8.055-1.68,12.721
|
||||
c0.192,2.447,1.304,4.592,0.96,7.92c2.38,2.41,5.91,1.207,6.479,4.32c0.149,0.815-0.469,1.732-0.238,2.641
|
||||
c0.31,1.228,1.4,1.582,2.158,2.64c0.367,0.513,0.401,1.11,0.721,1.681c0.6,1.071,1.973,1.904,1.921,3.12
|
||||
c-1.71-1.116-5.939-0.12-6.722-2.641c-0.326-1.056-0.051-2.962,0.961-3.84c-0.551-0.81-1.742-0.979-2.399-1.681
|
||||
c-0.479,0.642-0.043,2.196-0.96,2.399c-1.686,0.006-2.42-0.94-3.36-1.681c2.378,6.023,5.962,10.84,7.2,18
|
||||
c-0.499,0.521-0.775-0.028-1.2-0.239c0.568,1.512,1.103,3.06,2.642,3.601c0.24-0.32,0.396-0.725,0.72-0.961
|
||||
c-0.04-1.24-2.19-0.369-1.92-1.92c2.896,1.111,8.071,0.932,6.72,5.521c1.604-0.086,1.508-1.227,1.439-2.16
|
||||
c-0.116-1.637-1.732-1.745-2.159-2.641c2.247-0.848,2.809,1.961,3.6,3.359c1.896-1.439-0.931-3.092-0.72-4.08
|
||||
c2.275-0.436,1.814,1.866,2.88,2.641c0.519-1.938-1.698-2.217-0.96-3.6c-1.312-0.848-1.551-2.77-2.16-4.32
|
||||
c-0.871-0.131-1.943,0.078-2.88-0.479c-0.745-1.229-1.812-3.401-0.72-5.04c1.875-0.539,3.267,0.706,3.84,1.92
|
||||
c0.294,0.623,0.039,1.396,0.24,2.16c0.467,1.771,1.71,3.521,1.92,5.04c1.044,0.637,2.349,1.011,2.88,2.16
|
||||
c-0.806,3.459-0.021,6.384,0.96,10.319c0.884,3.547,1.48,6.951,2.4,10.319c0.781,2.868,1.963,7.187,2.64,10.56
|
||||
c0.11,0.556-0.071,1.119,0,1.682c0.508,3.995,2.243,8.225,3.36,12.479c1.8,6.861,2.722,13.881,4.318,21.121
|
||||
c0.688,3.112,2.145,5.914,2.4,8.879c6.355-0.617,5.865,10.313-1.44,8.16c-0.606-0.666-2.256-1.324-1.438-2.64
|
||||
c1.234,1.05,1.816,1.95,3.6,1.681c1.979-0.301,3.092-2.835,1.92-4.801c-1.634-1.834-3.564,0.15-5.279-0.24
|
||||
c-0.351-1.471,1.483-0.756,1.439-1.92c0.104-1.543-0.771-2.107-0.96-3.359c-0.397,0.242-0.261,1.021-0.479,1.439
|
||||
c-1.474-0.113-1.64,1.081-2.881,1.2c-0.815-1.983-1.211-4.39-1.68-6.72c0.596-0.604,2.02-0.381,2.16-1.44
|
||||
c0.73-0.011,0.934,0.507,1.439,0.72c0.184-1.303-0.297-1.943-0.24-3.119c-0.743,0.317-1.233,0.744-1.92,1.199
|
||||
c-0.512,0.342-0.961,1.149-1.92,0.961c-0.873-1.607-0.926-4.035-1.199-6.24c1.027-0.469,2.085-1.998,3.6-1.199
|
||||
c-0.049-2.191-0.803-3.678-0.96-5.762c-0.661,0.756,0.343,1.889,0,3.121c-0.632,0.172-0.909,1.42-1.68,0.721
|
||||
c-0.278,0.762-0.327,1.752-1.44,1.68c-0.63-1.93-1.523-3.596-1.439-6.239c1.488-0.493,2.322-2.764,4.08-1.2
|
||||
c-0.291-2.67-1.105-4.814-1.681-7.199c-0.632,0.961,0.533,2.18,0.24,3.6c-0.776,1.223-2.252,1.748-2.88,3.12
|
||||
c-1.539-1.251-1.508-4.423-1.92-6.72c0.602-0.373,0.804-0.975,1.68-0.48c0.105-0.693,0.506-1.094,1.2-1.199
|
||||
c0.582,0.297,1.116,0.643,1.438,1.199c-0.136-1.464-0.621-2.578-0.959-3.84c-1.062,0.138-0.719,1.682-1.92,1.68
|
||||
c-0.482,0.398-0.183,1.578-0.961,1.68c-2.101-0.4-2.123-4.084-2.16-5.76c0.31-1.292,1.816-1.384,2.642-2.16
|
||||
c1.34,0.42,1.255,2.267,1.92,3.36c-0.037-1.646-0.894-4.545-1.2-6.72c-1.124,0.044-3.02,1.266-3.6,2.641
|
||||
c-0.644,0.646-0.913-0.307-1.44-0.48c0.653,3.693,1.729,7.789,2.64,11.761c0.703,3.063,1.023,6.019,2.4,7.92
|
||||
c0.721,8.088,4.225,15.449,5.28,24c-1.283-1.156-1.265-3.088-1.681-4.562c-2.156-7.615-4.125-15.852-5.761-24.238
|
||||
c-1.569-8.059-4.251-15.672-5.04-23.762c1.043,0.799,0.825,2.855,1.2,4.32c0.996-0.684,2.788-0.572,3.12-1.92
|
||||
c0.891-0.17,1.029,0.41,1.92,0.24c-1.221-6.25-2.62-12.502-4.08-18.721c-0.438-1.863-0.727-4.033-1.92-5.521
|
||||
c-1.1,0.021-1.098,1.144-1.92,1.44c-2.129-0.365-2.719,1.443-5.28,1.68c1.589,9.702,4.788,19.036,6.479,28.8
|
||||
c0.302,1.738,0.367,3.38,0.721,5.041c0.557,2.615,1.475,5.364,2.16,8.64c2.146,10.254,6.004,21.727,8.16,32.399
|
||||
c2.027,0.351,2.928-0.432,4.318-0.72c0.418,2.652-3.131,2.361-5.039,1.92c-6.931-24.35-12.082-50.479-18-75.84
|
||||
c-2.002-1.438-2.99-3.891-4.801-5.521c-0.678-0.197-0.621,0.34-1.199,0.24c-4.386-2.818-9.349-4.729-14.16-7.199
|
||||
c-4.744-2.436-9.889-4.223-14.159-7.201c-1.263-3.646-4.685-6.091-7.681-9.119c-2.334-2.357-4.869-4.594-7.439-6.721
|
||||
c-2.421-2.001-5.087-3.658-8.4-5.76c-2.435-1.543-6.654-4.709-9.12-4.8c-1.204-0.044-2.162,0.894-3.359,1.44
|
||||
c-1.265,0.575-2.666,0.76-3.84,1.438c-2.599,1.504-6.572,4.528-8.641,6.721c-0.7,0.742-1.223,2.046-1.92,3.119
|
||||
c-1.686,2.597-2.355,3.652-3.6,6.961c-1.01,2.685-1.321,5.697-2.16,9.119c-0.304,1.24-0.88,3.11-0.96,5.28
|
||||
c-0.161,4.354-0.081,9.812,0.479,14.16c0.47,3.643,1.634,5.873-1.439,7.199c1.128,5.896-2.103,8.493-7.2,7.92
|
||||
c-1.755-0.196-3.433-1.703-4.8-1.439c-0.772,0.148-1.158,1.165-1.68,1.199c-0.012,2.816,3.443,8.568,3.119,12
|
||||
c-0.345,3.664-5.084,4.781-8.159,2.4c-2.018-5.104-3.257-10.984-5.04-16.32c-1.104-1.088-2.422-0.971-4.32-1.68
|
||||
c-0.448-0.168-0.741-0.642-1.2-0.721c-2.181-0.375-4.735,0.471-6.479-0.24c-1.671-0.68-1.605-2.477-2.881-3.359
|
||||
c-2.689-13.207-0.238-23.942,3.36-34.08c0.355-1,1.099-1.922,1.44-2.879c0.316-0.893,0.148-1.949,0.479-2.881
|
||||
c0.616-1.729,1.61-3.226,2.16-5.041c1.552-5.117,3.843-11.199,4.8-17.278c1.435-9.108,0.173-19.464-1.439-27.84
|
||||
c-0.581-3.016-0.713-5.869-1.44-8.64c-0.86-3.279-2.544-6.25-3.36-9.36c-0.188-0.718,0.001-1.275-0.239-1.92
|
||||
c-0.484-1.298-1.316-2.454-1.92-3.84c-0.254-0.583-0.681-1.277-0.96-1.92c-0.551-1.265-1.039-2.307-1.44-4.08
|
||||
c-1-1.933-1.815-3.799-2.88-5.52c-3.057-4.945-6.939-9.452-10.561-14.16c-1.982-1.733-3.234-2.961-2.88-6.96
|
||||
c0.488-5.49,3.643-10.933,6-16.32c2.413-5.511,5.669-10.261,8.16-13.92c-0.681-3.26,0.508-5.816,4.32-5.28
|
||||
c1.648-1.083,0.856-4.006,2.88-4.8c0.818-0.321,1.811,0.003,2.4-0.24c1.817-0.749,1.167-2.514,2.399-3.36
|
||||
c1.08-0.742,2.506-0.357,3.601,0c-0.578,2.316-2.125,4.626-5.04,3.84c0.284,2.28-1.472,3.962-3.12,5.04
|
||||
c-0.904,0.519-1.017-0.376-1.92-0.479c-0.671,0.613,1.168,1.048,0.239,1.92c-0.883,2.076-2.336,3.584-4.56,4.32
|
||||
c-3.07,3.984-6.422,8.912-8.64,14.16c-0.396,0.937-0.553,2.007-0.96,2.88c-0.777,1.662-1.557,3.044-2.4,5.28
|
||||
c-0.337,0.892-0.643,1.788-0.96,2.88c-0.519,1.786-1.834,4.843-1.68,6.24c0.31,2.806,5.646,6.404,7.439,8.4
|
||||
c1.528,1.702,3.668,5.2,5.28,7.68c1.484,2.283,3.561,5.287,4.8,8.16c0.76,1.76,1.024,3.335,1.68,4.8
|
||||
c0.614,1.371,1.233,2.71,2.16,4.32c1.305,6.888,4.467,14.05,5.761,21.84c0.322,1.945,0.454,4.176,0.72,6.24
|
||||
C30.615,132.855,26.544,148.64,21.42,160.792z M127.5,20.152c-5.646-3.555-10.348-8.052-17.521-10.08
|
||||
c-0.875-1.489-2.273-2.128-4.078-2.88c-3.35-1.396-7.562-3.075-10.801-2.64c-1.82-1.175-4.289-0.83-6.721-1.2
|
||||
c-2.28-0.347-4.444-1.439-6.72-1.68c-1.344-0.142-2.785,0.07-4.32,0c-7.497-0.34-13.854-0.484-19.68,0.48
|
||||
c-1.943,0.321-3.91,0.105-4.8,1.2c12.866,0.814,25.084,2.275,35.04,6c2.174,1.906,3.231,4.928,4.56,7.68
|
||||
c3.756,0.281,7.337,0.812,11.04,1.44c3.771,0.638,7.745,0.647,11.28,1.44c1.232,0.276,2.434,0.909,3.6,1.2
|
||||
c1.477,0.369,2.948,0.386,4.32,0.72c3.857,0.938,7.084,3.071,10.8,3.6C132.611,22.562,128.557,22.856,127.5,20.152z M26.701,10.552
|
||||
c-0.081,0.799-0.695,1.064-0.48,2.16c1.457-0.143,2.016-1.185,2.4-2.4C28.04,9.76,27.455,10.557,26.701,10.552z M21.18,17.752
|
||||
c1.42-0.58,2.396-1.604,2.641-3.36C21.856,12.896,19.951,16.054,21.18,17.752z M15.901,19.192c-0.65,1.087-1.234,3.719,0.479,4.08
|
||||
c1.305-0.855,2.313-2.007,2.64-3.84C18.334,18.765,16.911,18.93,15.901,19.192z M100.38,20.872c-0.043-0.69-1.745-0.956-2.159-0.48
|
||||
C99.227,20.265,99.561,20.812,100.38,20.872z M119.581,26.152c8.754,2.136,16.763,4.572,25.2,6.72
|
||||
c3.148,0.801,6.432,1.065,9.84,1.68c2.46,0.443,5.003,1.364,6.96,1.68c4.544,0.733,8.42,1.37,12.72,1.92
|
||||
c2.854,0.364,6.668,1.791,9.12-0.72c-0.155-1.847-2.051-2.091-3.36-2.64c-9.778-4.101-21.683-7.286-32.64-8.88
|
||||
c1.938,0.772,3.869,1.362,6,1.92c2.259,0.591,4.64,0.699,6.96,1.2c0.835,0.18,1.508,0.71,2.4,0.96
|
||||
c0.902,0.253,1.931,0.237,2.88,0.48c3.551,0.907,6.789,2.409,9.84,3.6c1.567,0.612,3.974,0.669,4.319,2.64
|
||||
c-1.635,2.107-4.569,0.608-6.959,0.24c-3.174-0.488-5.125-0.856-7.44-1.44c-4.142-1.043-8.385-1.535-12.72-2.4
|
||||
c-5.418-1.082-10.871-2.907-16.32-4.08c-5.503-1.185-10.977-2.667-16.32-4.08c-2.738-0.725-5.652-0.984-8.398-1.68
|
||||
c-3.6-0.912-7.153-2.42-10.561-2.16C106.85,22.779,113.795,24.741,119.581,26.152z M135.42,23.032c-1.168,0.12-2.3-1.049-3.12-0.48
|
||||
C132.919,23.471,134.123,23.59,135.42,23.032z M137.581,23.512c-0.429,0.001-1.396-0.646-1.681,0
|
||||
C136.659,23.182,137.35,24.106,137.581,23.512z M138.781,23.752C137.244,23.475,138.253,24.485,138.781,23.752L138.781,23.752z
|
||||
M140.701,24.232c-0.418,0.018-0.747-0.054-0.96-0.24C139.355,24.661,140.777,24.656,140.701,24.232z M146.939,25.672
|
||||
c-0.42,0.066-1.196-0.675-1.438,0C145.921,25.606,146.698,26.348,146.939,25.672z M50.46,140.872
|
||||
c0.413,0.013,0.615-0.186,0.72-0.479C50.767,140.379,50.565,140.576,50.46,140.872z M83.34,145.192
|
||||
c-0.599,0.171-0.986-1.058-1.439-0.24C82.156,145.124,83.005,145.89,83.34,145.192z M43.741,158.151
|
||||
c-0.613,1.31-1.417,2.559-1.92,3.841c-1.446,3.681-2.29,8.405-3.36,12.239c-0.197,0.707-0.124,1.976-0.96,2.4
|
||||
c0.58-5.143,1.586-9.746,3.12-13.921c1.579-4.298,3.745-8.028,4.8-12c-1.188,2.661-2.916,5.207-4.08,8.16
|
||||
c-0.661,1.677-0.845,3.556-1.439,5.28c-0.392,1.133-1.079,2.006-1.44,3.119c-0.397,1.227-0.381,2.485-0.72,3.84
|
||||
c-0.347,1.387-1.107,2.686-1.44,4.08c-0.313,1.313,0.229,3.015-1.2,3.841c0.68-4.237,1.386-8.304,2.4-12.24
|
||||
c0.995-3.864,2.999-7.331,3.6-10.8c-2.037,3.979-3.683,9.367-4.8,13.68c-0.562,2.166-1.047,4.053-1.68,5.76
|
||||
c-0.333,0.898-0.376,2.038-1.2,2.642c0.995-3.695,1.656-7.688,2.64-11.521c0.853-3.32,2.179-6.689,3.36-9.601
|
||||
c0.623-1.534,1.54-2.929,1.68-4.56c-4.529,7.229-6.201,17.317-8.88,26.398c0.265-7.02,3.061-14.855,4.561-20.641
|
||||
c-3.25,7.15-5.026,15.774-6.721,24.48c-0.278-3.48,0.833-6.847,0.721-9.361c-0.837,2.524-0.938,5.783-1.92,8.16
|
||||
c0.249-8.07,2.284-14.354,4.079-20.879c-1.099,0.604-1.108,2.125-1.439,3.359c-0.33,1.229-0.892,2.562-1.2,3.84
|
||||
c-0.954,3.941-1.397,8.85-2.399,12.24c-0.175,0.59,0.17,1.729-0.721,1.92c0.333-4.309,1.486-9.072,1.681-12.721
|
||||
c-1.289,3.992-1.331,9.229-2.641,13.201c-0.339-2.184,0.607-4.351,0-6c0,0.56,0,1.118,0,1.68c-0.873,1.207-0.36,3.8-1.2,5.039
|
||||
c-0.27-0.689-0.048-1.871-0.479-2.399c0.245,0.824-0.396,1.979-0.72,1.44c-0.22-1.982,1.028-3.769,0.239-5.521
|
||||
c-0.602,3.717-0.384,8.256-1.92,11.039c-0.146-0.574,0.302-1.741-0.239-1.92c-0.188,0.625-0.413,2.451-0.961,1.92
|
||||
c0.091-1.16,0.98-3.078,0-3.84c0.321,1.521-0.976,3.624-1.68,1.92c-0.085,1.595-0.108,3.252-0.479,4.562
|
||||
c-0.516-0.525-0.113-1.969-0.721-2.4c-0.175,0.545,0.433,1.873-0.479,1.68c-1.05-2.633,0.628-7.181-0.48-9.84
|
||||
c-1.418,6.688-0.179,14.127,0,21.6c2.076,1.42,3.499,0.545,5.761,0.721c1.369,0.106,3.233,0.787,5.04,1.439
|
||||
c1.765,0.639,3.736,0.938,5.039,1.44c2.034,0.786,3.52,2.353,5.28,2.159c1.335-0.146,2.019-1.488,3.12-1.92
|
||||
c2.678,1.703,7.881,2.992,10.32,0.239c1.113-1.255,1.084-3.026,0.479-5.278c0.331-1.271,1.749-1.451,2.4-2.4
|
||||
c-2.405-9.068-1.622-20.285,0.479-28.561c0.488-1.918,0.75-4.174,1.44-6c0.134-0.353,0.766-0.771,0.96-1.199
|
||||
c0.886-1.948,1.236-2.836,2.16-4.32c1.059-1.702,2.208-3.387,3.12-4.8c-2.922,1.388-4.375,5.03-5.521,7.681
|
||||
c-0.801,1.852-1.863,3.79-2.399,5.279c-0.763,2.117-0.429,4.688-2.16,6.24c0.534-2.24,0.688-4.629,1.439-6.721
|
||||
c0.263-0.728,0.917-1.197,1.2-1.921c0.23-0.586,0.055-1.355,0.24-1.92c0.108-0.33,0.553-0.344,0.72-0.72
|
||||
c0.202-0.454,0.059-1.048,0.24-1.439c1.094-2.361,2.931-4.074,3.36-6.24c-0.611,0.59-1.02,1.382-1.92,1.681
|
||||
c-0.104,1.799-1.573,2.836-2.4,4.319c-1.735,3.113-3.22,6.397-4.561,9.36c-0.674,1.49-1.021,3.412-2.399,4.56
|
||||
c1.534-4.945,3.68-9.279,5.76-13.68c0.425-0.216,0.701-0.578,1.2-0.72c0.619-2.661,2.062-4.499,3.36-6.479
|
||||
c-3.013,0.856-3.713,4.425-5.04,7.199c-0.45,0.94-1.206,1.77-1.681,2.641c-0.541,0.994-0.717,2.013-1.2,3.119
|
||||
c-0.875,2.007-2.194,3.858-2.88,5.761c-0.434,1.204-0.337,3.796-1.92,3.84c1.629-3.503,2.437-7.214,3.84-10.8
|
||||
c1.331-3.398,3.999-6.447,4.08-10.08c-3.672,5.197-6.271,12.139-8.399,18.48c-0.297,0.883-0.301,2.074-1.2,2.64
|
||||
c0.978-7.423,4.625-13.454,6.479-19.2c-1.604,2.814-3.279,5.785-4.56,9.12c-1.296,3.375-1.818,7.194-3.36,10.56
|
||||
c0.077-3.477,1.145-7.088,2.4-10.56c0.404-1.118,0.97-2.237,1.44-3.36c0.494-1.182,0.69-2.555,1.199-3.6
|
||||
c0.804-1.65,2.572-2.846,2.641-4.801c-0.997,0.047-1.504,1.209-1.92,1.92c-3.448,5.897-4.921,13.352-7.44,19.681
|
||||
c0.259-2.94,0.81-5.591,1.92-7.681c0.018-3.902,2.939-6.178,2.641-9.601c-1.471,2.734-2.318,5.974-3.36,9.121
|
||||
c-1.07,3.234-1.825,6.58-3.12,9.601c0.832-8.607,3.912-14.969,6.48-21.84c0.947-1.293,2.015-2.466,2.159-4.562
|
||||
c0.559-0.647,1.829-1.842,1.44-2.64C47.226,150.467,45.48,154.437,43.741,158.151z M61.26,149.032
|
||||
c0.413,0.013,0.615-0.186,0.72-0.479C61.568,148.539,61.365,148.735,61.26,149.032z M32.46,149.752
|
||||
c0.601,0.041,0.751-0.369,0.96-0.72C32.82,148.991,32.669,149.399,32.46,149.752z M49.5,150.712
|
||||
C48.405,152.59,50.104,150.498,49.5,150.712L49.5,150.712z M52.14,154.553c0.787-1.134,1.448-2.393,2.16-3.601
|
||||
C52.851,151.267,52.217,153.665,52.14,154.553z M135.18,164.633c-0.4-0.397-0.189-1.409-0.24-2.159
|
||||
c0.188-0.054,0.279-0.201,0.24-0.48c0.703,0.062,0.737-0.543,1.44-0.48c0.905-0.104,1.127,0.475,1.68,0.721
|
||||
c0.244-1.283-0.329-1.75-0.96-2.159c-1.496,0.184-2.255,1.104-3.6,1.438C133.689,162.928,134.73,164.515,135.18,164.633z
|
||||
M139.5,161.992c0.439,0.462,0.812-0.205,1.2-0.479c-0.343-0.058-0.405-0.396-0.722-0.479
|
||||
C139.977,161.509,139.326,161.338,139.5,161.992z M136.141,163.672c1.604-0.376,1.142,0.785,0.959,1.681
|
||||
c0.455-0.054,0.529,0.271,0.961,0.239c-0.129-1.552-0.433-2.928-1.92-3.118C136.141,162.872,136.141,163.271,136.141,163.672z
|
||||
M140.46,163.672c0.478,0.004,0.306,0.655,0.96,0.479c0.075-0.323,0.342-0.459,0.24-0.959
|
||||
C141.232,163.324,140.664,163.315,140.46,163.672z M30.06,165.353c0.139-0.146,0.562-1.139,0-1.199
|
||||
C30.373,164.748,29.356,165.147,30.06,165.353z M136.141,169.433c-0.297-1.623-1.065-2.773-1.92-3.841
|
||||
C134.659,167.074,135.413,168.24,136.141,169.433z M29.821,166.553c0.064-0.327-0.149-0.938-0.24-0.479
|
||||
C29.515,166.397,29.729,167.009,29.821,166.553z M27.661,168.474c0.73-0.082,0.964-1.9,0.479-2.4
|
||||
C27.971,166.863,27.754,167.606,27.661,168.474z M141.9,167.512c0,0.24,0,0.48,0,0.722c0.324,0.326,1.676,0.457,1.681-0.24
|
||||
C143.09,167.731,142.6,166.842,141.9,167.512z M19.02,168.712c0.07-0.146,0.598-0.924,0-0.96
|
||||
C18.951,167.899,18.423,168.676,19.02,168.712z M31.26,172.553c-0.155-1.84,1.534-3.102,0.72-4.801
|
||||
C31.852,168.927,30.532,171.974,31.26,172.553z M143.34,170.633c0.68,0.197,0.622-0.339,1.2-0.239
|
||||
c0.076-0.558-0.039-0.922-0.24-1.201C143.473,169.164,143.184,169.676,143.34,170.633z M27.18,172.072
|
||||
c0.136-0.677,0.835-2.029,0.24-2.642C27.301,170.125,26.843,171.719,27.18,172.072z M137.82,172.792
|
||||
c-0.106-0.613-0.336-1.104-0.721-1.439C137.223,171.95,137.092,172.8,137.82,172.792z M25.5,173.271
|
||||
c-0.171,4.112,0.753-0.395,0.24-0.719C25.703,172.834,25.782,173.233,25.5,173.271z M148.38,175.192
|
||||
c1.005,0.115,1.526,0.713,1.44,1.92c1.74-0.732-0.082-3.19-1.2-3.36C148.344,174.035,148.35,174.601,148.38,175.192z M21.42,175.433
|
||||
c0.038-0.166-0.084-1.396-0.24-0.721C20.969,175.626,21.309,175.914,21.42,175.433z M16.14,178.553c0.035-0.284,0.428-0.545,0-0.721
|
||||
C16.106,178.115,15.713,178.376,16.14,178.553z M128.46,178.312c-0.315-0.244-0.885-0.235-1.2-0.479
|
||||
C126.975,178.545,128.271,178.863,128.46,178.312z M22.861,179.032c0.064-0.327-0.149-0.938-0.24-0.479
|
||||
C22.555,178.879,22.769,179.489,22.861,179.032z M130.861,179.992c0.285-0.714-1.012-1.031-1.2-0.479
|
||||
C130.4,179.332,129.953,180.34,130.861,179.992z M22.621,181.433c0.038-0.166-0.084-1.396-0.24-0.721
|
||||
C22.169,181.626,22.509,181.914,22.621,181.433z M138.3,182.872c-0.282-0.034-0.544-0.428-0.72,0
|
||||
C137.863,182.906,138.125,183.299,138.3,182.872z M148.38,188.633c0.229-1.432-0.444-1.955-0.96-2.641
|
||||
C141.995,184.959,145.773,191.787,148.38,188.633z M32.22,206.633c-0.457-0.104-0.603-0.518-1.2-0.479
|
||||
C30.852,206.748,32.046,207.168,32.22,206.633z M32.94,208.072c0.286-0.714-1.011-1.031-1.199-0.48
|
||||
C32.298,207.697,32.383,208.651,32.94,208.072z M35.821,214.553c0.402,1.761,1.146,5.343,2.64,5.039
|
||||
c0.277-3.799-2.039-7.08-2.4-11.039c-0.57-0.086-1.38-1.157-1.92-0.479C35.256,209.742,35.267,212.132,35.821,214.553z
|
||||
M33.18,209.512c-0.148,1.339,1.039,2.537,0.24,4.08c0.282,0.357,0.603,0.679,0.96,0.961c-1.081,0.765,1.131,2.709,0,3.601
|
||||
c0.576,0.223,0.636,0.965,1.68,0.72c-0.979-3.261-1.55-6.931-2.64-10.08c-0.85-0.21-0.59-0.03-1.44-0.239
|
||||
C31.747,209.506,33.322,208.649,33.18,209.512z M39.18,210.231c0.174-0.733-0.414-0.705-0.24-1.439c-0.399,0-0.8,0-1.199,0
|
||||
C38.161,209.332,38.641,209.812,39.18,210.231z M38.22,211.433c0.68,0.041,1,0.439,1.681,0.479
|
||||
C39.902,210.8,38.848,210.937,38.22,211.433z M38.701,213.112c1.781-0.084-0.676,0.596,0.239,0.96
|
||||
c0.066-0.566,0.895,0.021,0.961,0.239c0.459,1.181-0.798,0.644-0.721,1.439c0.596-0.035,1.165-0.045,1.2,0.48
|
||||
c0.206,0.766-0.378,0.742-0.479,1.199c0.728-0.089,0.498,0.781,0.72,1.2c-0.617,1.144-1.351,2.169-3.12,2.159
|
||||
c-0.871-0.169-0.853-1.229-1.68-1.439c-0.346,0.135-0.586,0.375-0.721,0.721c1.11-0.18,0.98,0.271,0.721,0.96
|
||||
c2.038,0.269,3.538,1.712,5.52,0.72c1.29-3.207-0.678-6.458-1.439-9.119C39.305,212.597,38.736,212.588,38.701,213.112z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 30 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
<h3>About</h3>
|
||||
<p>
|
||||
Flask-Security is an opinionated Flask extension which adds basic
|
||||
security and authentication features to your Flask apps quickly
|
||||
and easily. Flask-Social can also be used to add "social" or OAuth
|
||||
login and connection management.
|
||||
</p>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
|
||||
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
|
||||
<li><a href="http://github.com/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>
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
<p class="logo"><a href="{{ pathto(master_doc) }}">
|
||||
<img class="logo" src="{{ pathto('_static/logo-helmet.png', 1) }}" alt="Logo"/>
|
||||
</a></p>
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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``.
|
||||
======================================= ========================================
|
||||
@@ -0,0 +1,13 @@
|
||||
Contents
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
features
|
||||
configuration
|
||||
quickstart
|
||||
models
|
||||
customizing
|
||||
api
|
||||
changelog
|
||||
@@ -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")
|
||||
@@ -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
@@ -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
|
||||
@@ -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``
|
||||
@@ -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()
|
||||
-128
@@ -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()
|
||||
@@ -1,21 +0,0 @@
|
||||
# a little trick so you can run:
|
||||
# $ python example/app.py
|
||||
# from the root of the security project
|
||||
import sys, os
|
||||
sys.path.pop(0)
|
||||
sys.path.insert(0, os.getcwd())
|
||||
|
||||
from example import app
|
||||
from flask.ext.script import Manager
|
||||
from flask.ext.security.script import (CreateUserCommand , AddRoleCommand,
|
||||
RemoveRoleCommand, ActivateUserCommand, DeactivateUserCommand)
|
||||
|
||||
manager = Manager(app.create_sqlalchemy_app())
|
||||
manager.add_command('create_user', CreateUserCommand())
|
||||
manager.add_command('add_role', AddRoleCommand())
|
||||
manager.add_command('remove_role', RemoveRoleCommand())
|
||||
manager.add_command('deactivate_user', DeactivateUserCommand())
|
||||
manager.add_command('activate_user', ActivateUserCommand())
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager.run()
|
||||
@@ -1,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
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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')
|
||||
@@ -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())
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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" %}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Executable
+189
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
make-release
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Helper script that performs a release. Does pretty much everything
|
||||
automatically for us.
|
||||
|
||||
:copyright: (c) 2011 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, date
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
_date_clean_re = re.compile(r'(\d+)(st|nd|rd|th)')
|
||||
|
||||
|
||||
def installed_libraries():
|
||||
return Popen(['pip', 'freeze'], stdout=PIPE).communicate()[0]
|
||||
|
||||
|
||||
def has_library_installed(library):
|
||||
return library + '==' in installed_libraries()
|
||||
|
||||
|
||||
def parse_changelog():
|
||||
with open('CHANGES') as f:
|
||||
lineiter = iter(f)
|
||||
for line in lineiter:
|
||||
match = re.search('^Version\s+(.*)', line.strip())
|
||||
|
||||
if match is None:
|
||||
continue
|
||||
|
||||
version = match.group(1).strip()
|
||||
|
||||
if lineiter.next().count('-') != len(line.strip()):
|
||||
fail('Invalid hyphen count below version line: %s', line.strip())
|
||||
|
||||
while 1:
|
||||
released = lineiter.next().strip()
|
||||
if released:
|
||||
break
|
||||
|
||||
match = re.search(r'Released (\w+\s+\d+\w+\s+\d+)', released)
|
||||
|
||||
if match is None:
|
||||
fail('Could not find release date in version %s' % version)
|
||||
|
||||
datestr = parse_date(match.group(1).strip())
|
||||
|
||||
return version, datestr
|
||||
|
||||
|
||||
def bump_version(version):
|
||||
try:
|
||||
parts = map(int, version.split('.'))
|
||||
except ValueError:
|
||||
fail('Current version is not numeric')
|
||||
parts[-1] += 1
|
||||
return '.'.join(map(str, parts))
|
||||
|
||||
|
||||
def parse_date(string):
|
||||
string = _date_clean_re.sub(r'\1', string)
|
||||
return datetime.strptime(string, '%B %d %Y')
|
||||
|
||||
|
||||
def set_filename_version(filename, version_number, pattern):
|
||||
changed = []
|
||||
|
||||
def inject_version(match):
|
||||
before, old, after = match.groups()
|
||||
changed.append(True)
|
||||
return before + version_number + after
|
||||
|
||||
with open(filename) as f:
|
||||
contents = re.sub(r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern,
|
||||
inject_version, f.read())
|
||||
|
||||
if not changed:
|
||||
fail('Could not find %s in %s', pattern, filename)
|
||||
|
||||
with open(filename, 'w') as f:
|
||||
f.write(contents)
|
||||
|
||||
|
||||
def set_init_version(version):
|
||||
info('Setting __init__.py version to %s', version)
|
||||
set_filename_version('flask_security/__init__.py', version, '__version__')
|
||||
|
||||
|
||||
def set_setup_version(version):
|
||||
info('Setting setup.py version to %s', version)
|
||||
set_filename_version('setup.py', version, 'version')
|
||||
|
||||
|
||||
def set_docs_version(version):
|
||||
info('Setting docs/conf.py version to %s', version)
|
||||
set_filename_version('docs/conf.py', version, 'version')
|
||||
|
||||
|
||||
def build_and_upload():
|
||||
Popen([sys.executable, 'setup.py', 'sdist', 'build_sphinx', 'upload', 'upload_sphinx']).wait()
|
||||
|
||||
|
||||
def fail(message, *args):
|
||||
print >> sys.stderr, 'Error:', message % args
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def info(message, *args):
|
||||
print >> sys.stderr, message % args
|
||||
|
||||
|
||||
def get_git_tags():
|
||||
return set(Popen(['git', 'tag'], stdout=PIPE).communicate()[0].splitlines())
|
||||
|
||||
|
||||
def git_is_clean():
|
||||
return Popen(['git', 'diff', '--quiet']).wait() == 0
|
||||
|
||||
|
||||
def make_git_commit(message, *args):
|
||||
message = message % args
|
||||
Popen(['git', 'commit', '-am', message]).wait()
|
||||
|
||||
|
||||
def make_git_tag(tag):
|
||||
info('Tagging "%s"', tag)
|
||||
Popen(['git', 'tag', '-a', tag, '-m', '%s release' % tag]).wait()
|
||||
Popen(['git', 'push', '--tags']).wait()
|
||||
|
||||
|
||||
def update_version(version):
|
||||
for f in [set_init_version, set_setup_version, set_docs_version]:
|
||||
f(version)
|
||||
|
||||
|
||||
def get_branches():
|
||||
return set(Popen(['git', 'branch'], stdout=PIPE).communicate()[0].splitlines())
|
||||
|
||||
|
||||
def branch_is(branch):
|
||||
return '* ' + branch in get_branches()
|
||||
|
||||
|
||||
def main():
|
||||
os.chdir(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
rv = parse_changelog()
|
||||
|
||||
if rv is None:
|
||||
fail('Could not parse changelog')
|
||||
|
||||
version, release_date = rv
|
||||
|
||||
tags = get_git_tags()
|
||||
|
||||
for lib in ['Sphinx', 'Sphinx-PyPI-upload']:
|
||||
if not has_library_installed(lib):
|
||||
fail('Build requires that %s be installed', lib)
|
||||
|
||||
if version in tags:
|
||||
fail('Version "%s" is already tagged', version)
|
||||
if release_date.date() != date.today():
|
||||
fail('Release date is not today')
|
||||
|
||||
if not branch_is('master'):
|
||||
fail('You are not on the master branch')
|
||||
|
||||
if not git_is_clean():
|
||||
fail('You have uncommitted changes in git')
|
||||
|
||||
info('Releasing %s (release date %s)',
|
||||
version, release_date.strftime('%d/%m/%Y'))
|
||||
|
||||
update_version(version)
|
||||
make_git_commit('Bump version number to %s', version)
|
||||
make_git_tag(version)
|
||||
build_and_upload()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
[build_sphinx]
|
||||
source-dir = docs/
|
||||
build-dir = docs/_build
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = docs/_build
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user