diff --git a/.gitignore b/.gitignore
index daf2545..23d1773 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..42c3b47
--- /dev/null
+++ b/.travis.yml
@@ -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
diff --git a/CHANGES b/CHANGES
index 7117a94..34d405f 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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.
diff --git a/LICENSE b/LICENSE
index bbf6b57..2b1a4f0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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
diff --git a/MANIFEST.in b/MANIFEST.in
index 3f4ce4f..5538354 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1 +1,2 @@
-include tests/*.py
\ No newline at end of file
+recursive-include tests *.py
+recursive-include flask_security/templates *.*
\ No newline at end of file
diff --git a/README b/README
deleted file mode 100644
index 5f8ac9c..0000000
--- a/README
+++ /dev/null
@@ -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/
\ No newline at end of file
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..d6a46a1
--- /dev/null
+++ b/README.rst
@@ -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 `_
+- `Issue Tracker `_
+- `Code `_
+- `Development Version
+ `_
\ No newline at end of file
diff --git a/artwork/logo-helmet.svg b/artwork/logo-helmet.svg
new file mode 100644
index 0000000..2799f1f
--- /dev/null
+++ b/artwork/logo-helmet.svg
@@ -0,0 +1,297 @@
+
+
+
+
diff --git a/docs/_static/logo-full.png b/docs/_static/logo-full.png
new file mode 100644
index 0000000..724fd06
Binary files /dev/null and b/docs/_static/logo-full.png differ
diff --git a/docs/_static/logo-helmet.png b/docs/_static/logo-helmet.png
new file mode 100644
index 0000000..b490418
Binary files /dev/null and b/docs/_static/logo-helmet.png differ
diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html
new file mode 100644
index 0000000..f65d1c7
--- /dev/null
+++ b/docs/_templates/sidebarintro.html
@@ -0,0 +1,17 @@
+
About
+
+ 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.
+
\ No newline at end of file
diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html
new file mode 100644
index 0000000..2686677
--- /dev/null
+++ b/docs/_templates/sidebarlogo.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 0000000..efad2ef
--- /dev/null
+++ b/docs/api.rst
@@ -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.
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
index 1d9c6ec..4432360 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -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.
diff --git a/docs/configuration.rst b/docs/configuration.rst
new file mode 100644
index 0000000..ab28904
--- /dev/null
+++ b/docs/configuration.rst
@@ -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``.
+======================================= ========================================
diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc
new file mode 100644
index 0000000..c905e06
--- /dev/null
+++ b/docs/contents.rst.inc
@@ -0,0 +1,13 @@
+Contents
+--------
+
+.. toctree::
+ :maxdepth: 1
+
+ features
+ configuration
+ quickstart
+ models
+ customizing
+ api
+ changelog
\ No newline at end of file
diff --git a/docs/customizing.rst b/docs/customizing.rst
new file mode 100644
index 0000000..d10a522
--- /dev/null
+++ b/docs/customizing.rst
@@ -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:
+
+* ``_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")
\ No newline at end of file
diff --git a/docs/features.rst b/docs/features.rst
new file mode 100644
index 0000000..e870ba2
--- /dev/null
+++ b/docs/features.rst
@@ -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/
diff --git a/docs/index.rst b/docs/index.rst
index fd587fa..97713c1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -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 `_,
-`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 on your own. Additionally, you may need to install an encryption
-library such as `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 `
-
-
-.. _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::
-
-
-
-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 `_
-to login.
-
-The last thing you'll want to do is add a logout link to your templates. This
-can be achieved with::
-
- Logout
-
-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') %}
- Admin Panel
- {$ 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
\ No newline at end of file
+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 `_
+2. `Flask-Mail `_
+3. `Flask-Principal `_
+4. `Flask-Script `_
+5. `Flask-WTF `_
+6. `itsdangerous `_
+7. `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 `_
+2. `Flask-MongoEngine `_
+
+
+.. include:: contents.rst.inc
\ No newline at end of file
diff --git a/docs/models.rst b/docs/models.rst
new file mode 100644
index 0000000..6ea6b15
--- /dev/null
+++ b/docs/models.rst
@@ -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``
\ No newline at end of file
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
new file mode 100644
index 0000000..ab42032
--- /dev/null
+++ b/docs/quickstart.rst
@@ -0,0 +1,70 @@
+Quick Start
+===========
+
+
+Installation
+------------
+
+Install requirements:
+
+ $ mkvirtualenv
+ $ 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()
diff --git a/example/__init__.py b/example/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/example/app.py b/example/app.py
deleted file mode 100644
index 3f86072..0000000
--- a/example/app.py
+++ /dev/null
@@ -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()
\ No newline at end of file
diff --git a/example/manage.py b/example/manage.py
deleted file mode 100644
index 15977bb..0000000
--- a/example/manage.py
+++ /dev/null
@@ -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()
\ No newline at end of file
diff --git a/example/templates/login.html b/example/templates/login.html
deleted file mode 100644
index d368e37..0000000
--- a/example/templates/login.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% include "_messages.html" %}
-{% include "_nav.html" %}
-
-
{{ content }}
diff --git a/flask_security/__init__.py b/flask_security/__init__.py
index b0be50d..756d89a 100644
--- a/flask_security/__init__.py
+++ b/flask_security/__init__.py
@@ -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 '' % (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 '' % 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
diff --git a/flask_security/confirmable.py b/flask_security/confirmable.py
new file mode 100644
index 0000000..d53c0a3
--- /dev/null
+++ b/flask_security/confirmable.py
@@ -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())
diff --git a/flask_security/core.py b/flask_security/core.py
new file mode 100644
index 0000000..8d449cd
--- /dev/null
+++ b/flask_security/core.py
@@ -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)
diff --git a/flask_security/datastore.py b/flask_security/datastore.py
new file mode 100644
index 0000000..975c0f0
--- /dev/null
+++ b/flask_security/datastore.py
@@ -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()
diff --git a/flask_security/datastore/__init__.py b/flask_security/datastore/__init__.py
deleted file mode 100644
index c0383be..0000000
--- a/flask_security/datastore/__init__.py
+++ /dev/null
@@ -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))
diff --git a/flask_security/datastore/mongoengine.py b/flask_security/datastore/mongoengine.py
deleted file mode 100644
index 37bdde5..0000000
--- a/flask_security/datastore/mongoengine.py
+++ /dev/null
@@ -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()
diff --git a/flask_security/datastore/sqlalchemy.py b/flask_security/datastore/sqlalchemy.py
deleted file mode 100644
index b42dd84..0000000
--- a/flask_security/datastore/sqlalchemy.py
+++ /dev/null
@@ -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()
diff --git a/flask_security/decorators.py b/flask_security/decorators.py
new file mode 100644
index 0000000..32e3409
--- /dev/null
+++ b/flask_security/decorators.py
@@ -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 = """
+
Unauthorized
+
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.
+ """
+
+
+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
diff --git a/flask_security/forms.py b/flask_security/forms.py
new file mode 100644
index 0000000..373debc
--- /dev/null
+++ b/flask_security/forms.py
@@ -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")
diff --git a/flask_security/passwordless.py b/flask_security/passwordless.py
new file mode 100644
index 0000000..7a9e001
--- /dev/null
+++ b/flask_security/passwordless.py
@@ -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')
diff --git a/flask_security/recoverable.py b/flask_security/recoverable.py
new file mode 100644
index 0000000..5138680
--- /dev/null
+++ b/flask_security/recoverable.py
@@ -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())
diff --git a/flask_security/registerable.py b/flask_security/registerable.py
new file mode 100644
index 0000000..2e29af1
--- /dev/null
+++ b/flask_security/registerable.py
@@ -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
diff --git a/flask_security/script.py b/flask_security/script.py
index 521ceb9..9c9a246 100644
--- a/flask_security/script.py
+++ b/flask_security/script.py
@@ -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
diff --git a/flask_security/signals.py b/flask_security/signals.py
new file mode 100644
index 0000000..17aac64
--- /dev/null
+++ b/flask_security/signals.py
@@ -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")
diff --git a/flask_security/templates/security/_macros.html b/flask_security/templates/security/_macros.html
new file mode 100644
index 0000000..8575f3d
--- /dev/null
+++ b/flask_security/templates/security/_macros.html
@@ -0,0 +1,16 @@
+{% macro render_field_with_errors(field) %}
+
+{% endmacro %}
\ No newline at end of file
diff --git a/flask_security/templates/security/_menu.html b/flask_security/templates/security/_menu.html
new file mode 100644
index 0000000..d50b5a4
--- /dev/null
+++ b/flask_security/templates/security/_menu.html
@@ -0,0 +1,15 @@
+{% if security.registerable or security.recoverable or security.confirmabled %}
+
+{% endif %}
\ No newline at end of file
diff --git a/flask_security/templates/security/_messages.html b/flask_security/templates/security/_messages.html
new file mode 100644
index 0000000..179d063
--- /dev/null
+++ b/flask_security/templates/security/_messages.html
@@ -0,0 +1,9 @@
+{%- with messages = get_flashed_messages(with_categories=true) -%}
+ {% if messages %}
+
+ {% for category, message in messages %}
+
{{ message }}
+ {% endfor %}
+
+ {% endif %}
+{%- endwith %}
\ No newline at end of file
diff --git a/flask_security/templates/security/email/confirmation_instructions.html b/flask_security/templates/security/email/confirmation_instructions.html
new file mode 100644
index 0000000..5082a9a
--- /dev/null
+++ b/flask_security/templates/security/email/confirmation_instructions.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/flask_security/templates/security/email/confirmation_instructions.txt b/flask_security/templates/security/email/confirmation_instructions.txt
new file mode 100644
index 0000000..fb435b5
--- /dev/null
+++ b/flask_security/templates/security/email/confirmation_instructions.txt
@@ -0,0 +1,3 @@
+Please confirm your email through the link below:
+
+{{ confirmation_link }}
\ No newline at end of file
diff --git a/flask_security/templates/security/email/login_instructions.html b/flask_security/templates/security/email/login_instructions.html
new file mode 100644
index 0000000..45a7cb5
--- /dev/null
+++ b/flask_security/templates/security/email/login_instructions.html
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/flask_security/templates/security/email/login_instructions.txt b/flask_security/templates/security/email/login_instructions.txt
new file mode 100644
index 0000000..1364ed6
--- /dev/null
+++ b/flask_security/templates/security/email/login_instructions.txt
@@ -0,0 +1,5 @@
+Welcome {{ user.email }}!
+
+You can log into your through the link below:
+
+{{ login_link }}
\ No newline at end of file
diff --git a/flask_security/templates/security/email/reset_instructions.html b/flask_security/templates/security/email/reset_instructions.html
new file mode 100644
index 0000000..fd0b48d
--- /dev/null
+++ b/flask_security/templates/security/email/reset_instructions.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flask_security/templates/security/email/reset_instructions.txt b/flask_security/templates/security/email/reset_instructions.txt
new file mode 100644
index 0000000..91ac288
--- /dev/null
+++ b/flask_security/templates/security/email/reset_instructions.txt
@@ -0,0 +1,3 @@
+Click the link below to reset your password:
+
+{{ reset_link }}
\ No newline at end of file
diff --git a/flask_security/templates/security/email/reset_notice.html b/flask_security/templates/security/email/reset_notice.html
new file mode 100644
index 0000000..536e296
--- /dev/null
+++ b/flask_security/templates/security/email/reset_notice.html
@@ -0,0 +1 @@
+
Your password has been reset
\ No newline at end of file
diff --git a/flask_security/templates/security/email/reset_notice.txt b/flask_security/templates/security/email/reset_notice.txt
new file mode 100644
index 0000000..a3fa0b4
--- /dev/null
+++ b/flask_security/templates/security/email/reset_notice.txt
@@ -0,0 +1 @@
+Your password has been reset
\ No newline at end of file
diff --git a/flask_security/templates/security/email/welcome.html b/flask_security/templates/security/email/welcome.html
new file mode 100644
index 0000000..55eaed6
--- /dev/null
+++ b/flask_security/templates/security/email/welcome.html
@@ -0,0 +1,7 @@
+
Welcome {{ user.email }}!
+
+{% if security.confirmable %}
+
You can confirm your email through the link below:
+{% endif %}
\ No newline at end of file
diff --git a/flask_security/templates/security/email/welcome.txt b/flask_security/templates/security/email/welcome.txt
new file mode 100644
index 0000000..fb6ee5b
--- /dev/null
+++ b/flask_security/templates/security/email/welcome.txt
@@ -0,0 +1,7 @@
+Welcome {{ user.email }}!
+
+{% if security.confirmable %}
+You can confirm your email through the link below:
+
+{{ confirmation_link }}
+{% endif %}
\ No newline at end of file
diff --git a/flask_security/templates/security/forgot_password.html b/flask_security/templates/security/forgot_password.html
new file mode 100644
index 0000000..90fcaf6
--- /dev/null
+++ b/flask_security/templates/security/forgot_password.html
@@ -0,0 +1,9 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+
Send password reset instructions
+
+{% include "security/_menu.html" %}
\ No newline at end of file
diff --git a/flask_security/templates/security/login_user.html b/flask_security/templates/security/login_user.html
new file mode 100644
index 0000000..d781ce0
--- /dev/null
+++ b/flask_security/templates/security/login_user.html
@@ -0,0 +1,12 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+
Login
+
+{% include "security/_menu.html" %}
\ No newline at end of file
diff --git a/flask_security/templates/security/register_user.html b/flask_security/templates/security/register_user.html
new file mode 100644
index 0000000..87cf9b1
--- /dev/null
+++ b/flask_security/templates/security/register_user.html
@@ -0,0 +1,13 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+
Register
+
+{% include "security/_menu.html" %}
\ No newline at end of file
diff --git a/flask_security/templates/security/reset_password.html b/flask_security/templates/security/reset_password.html
new file mode 100644
index 0000000..e6fc3f5
--- /dev/null
+++ b/flask_security/templates/security/reset_password.html
@@ -0,0 +1,10 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+
Reset password
+
+{% include "security/_menu.html" %}
\ No newline at end of file
diff --git a/flask_security/templates/security/send_confirmation.html b/flask_security/templates/security/send_confirmation.html
new file mode 100644
index 0000000..3e82840
--- /dev/null
+++ b/flask_security/templates/security/send_confirmation.html
@@ -0,0 +1,9 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+
Resend confirmation instructions
+
+{% include "security/_menu.html" %}
\ No newline at end of file
diff --git a/flask_security/templates/security/send_login.html b/flask_security/templates/security/send_login.html
new file mode 100644
index 0000000..15611c5
--- /dev/null
+++ b/flask_security/templates/security/send_login.html
@@ -0,0 +1,9 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+
Login
+
+{% include "security/_menu.html" %}
\ No newline at end of file
diff --git a/flask_security/utils.py b/flask_security/utils.py
new file mode 100644
index 0000000..9b44408
--- /dev/null
+++ b/flask_security/utils.py
@@ -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::
+
+
+
+ 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)
diff --git a/flask_security/views.py b/flask_security/views.py
new file mode 100644
index 0000000..89bb0dc
--- /dev/null
+++ b/flask_security/views.py
@@ -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 + '/',
+ 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 + '/',
+ 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 + '/',
+ methods=['GET', 'POST'],
+ endpoint='confirm_email')(confirm_email)
+
+ return bp
diff --git a/scripts/release.py b/scripts/release.py
new file mode 100755
index 0000000..1f222e5
--- /dev/null
+++ b/scripts/release.py
@@ -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()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..4a53949
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,6 @@
+[build_sphinx]
+source-dir = docs/
+build-dir = docs/_build
+
+[upload_sphinx]
+upload-dir = docs/_build
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 258b1ac..ae2636b 100644
--- a/setup.py
+++ b/setup.py
@@ -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 `_
+* `Issue Tracker `_
+* `Source `_
+* `Development Version
`_
"""
@@ -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',
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..6d1f451
--- /dev/null
+++ b/tests/__init__.py
@@ -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
diff --git a/tests/configured_tests.py b/tests/configured_tests.py
new file mode 100644
index 0000000..b2e75bc
--- /dev/null
+++ b/tests/configured_tests.py
@@ -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("
{%- if current_user.is_authenticated() -%}
- Log out
+ Log out
{%- else -%}
- Log in
+ Log in
{%- endif -%}
\ No newline at end of file
diff --git a/example/templates/index.html b/tests/test_app/templates/index.html
similarity index 100%
rename from example/templates/index.html
rename to tests/test_app/templates/index.html
diff --git a/tests/test_app/templates/register.html b/tests/test_app/templates/register.html
new file mode 100644
index 0000000..7f4e27a
--- /dev/null
+++ b/tests/test_app/templates/register.html
@@ -0,0 +1,11 @@
+{% include "_messages.html" %}
+{% include "_nav.html" %}
+
Register
+
+
{{ content }}
diff --git a/tests/test_app/templates/unauthorized.html b/tests/test_app/templates/unauthorized.html
new file mode 100644
index 0000000..10e9c2b
--- /dev/null
+++ b/tests/test_app/templates/unauthorized.html
@@ -0,0 +1,3 @@
+{% include "_messages.html" %}
+{% include "_nav.html" %}
+
You are not allowed to access the requested resouce