Merge branch 'develop' of git://github.com/mattupstate/flask-security into template_list

Conflicts:
	requirements.txt
This commit is contained in:
Chris Haines
2013-02-01 19:40:01 -05:00
30 changed files with 740 additions and 126 deletions
+6
View File
@@ -25,3 +25,9 @@ pip-log.txt
#Mr Developer
.mr.developer.cfg
#Virtualenv
env/
#Editor temporaries
*~
+1 -1
View File
@@ -7,7 +7,7 @@ python:
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
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors
before_script:
- mysql -e 'create database flask_security_test;'
+13
View File
@@ -4,6 +4,19 @@ Flask-Security Changelog
Here you can see the full list of changes between each Flask-Security release.
Version 1.6.0
-------------
Not yet released
- Password hashing is now more flexible and can be changed
- Flask-Login messages are configurable
- AJAX requests must now send a CSRF token for security reasons
- Login form messages are now configurable
- Forms can now be extended with more fields
- Added change password endpoint
Version 1.5.4
-------------
+13 -2
View File
@@ -49,6 +49,10 @@ Datastores
:members:
:inherited-members:
.. autoclass:: flask_security.datastore.PeeweeUserDatastore
:members:
:inherited-members:
Signals
-------
@@ -83,7 +87,13 @@ sends the following signals.
.. data:: password_reset
Sent when a user completes a password. It is passed the `user`.
Sent when a user completes a password reset. It is passed the
`user`.
.. data:: password_changed
Sent when a user completes a password change. It is passed the
`user`.
.. data:: reset_password_instructions_sent
@@ -91,6 +101,7 @@ sends the following signals.
with the `user` and `token`, the user being logged in and
the (if so configured) the reset token issued.
All signals are also passed a `app` keyword argument, which is the current application.
All signals are also passed a `app` keyword argument, which is the
current application.
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
+17
View File
@@ -18,6 +18,7 @@ following is a list of view templates:
* `security/login_user.html`
* `security/register_user.html`
* `security/reset_password.html`
* `security/change_password.html`
* `security/send_confirmation.html`
* `security/send_login.html`
@@ -57,6 +58,7 @@ The following is a list of all the available context processor decorators:
* ``login_context_processor``: Login view
* ``register_context_processor``: Register view
* ``reset_password_context_processor``: Reset password view
* ``change_password_context_processor``: Reset password view
* ``send_confirmation_context_processor``: Send confirmation view
* ``send_login_context_processor``: Send login view
@@ -77,6 +79,18 @@ register form or override validators::
security = Security(app, user_datastore,
register_form=ExtendedRegisterForm)
For the ``register_form`` and ``confirm_register_form``, each field is
passed to the user model (as kwargs) when a user is created. In the
above case, the ``first_name`` and ``last_name`` fields are passed
directly to the model, so the model should look like::
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
first_name = db.Column(db.String(255))
last_name = db.Column(db.String(255))
The following is a list of all the available form overrides:
* ``login_form``: Login form
@@ -84,6 +98,7 @@ The following is a list of all the available form overrides:
* ``register_form``: Register form
* ``forgot_password_form``: Forgot password form
* ``reset_password_form``: Reset password form
* ``change_password_form``: Reset password form
* ``send_confirmation_form``: Send confirmation form
* ``passwordless_login_form``: Passwordless login form
@@ -102,6 +117,8 @@ The following is a list of email templates:
* `security/mail/reset_instructions.html`
* `security/mail/reset_instructions.txt`
* `security/mail/reset_notice.html`
* `security/mail/change_notice.txt`
* `security/mail/change_notice.html`
* `security/mail/reset_notice.txt`
* `security/mail/welcome.html`
* `security/mail/welcome.txt`
+6 -5
View File
@@ -10,7 +10,7 @@ Flask application. They include:
4. Basic HTTP authentication
5. Token based authentication
6. Token based account activation (optional)
7. Token based password recovery/resetting (optional)
7. Token based password recovery / resetting (optional)
8. User registration (optional)
9. Login tracking (optional)
@@ -27,10 +27,11 @@ and libraries. They include:
Additionally, it assumes you'll be using a common library for your database
connections and model definitions. Flask-Security supports the following Flask
extensions out of the box for data persistance:
extensions out of the box for data persistence:
1. `Flask-SQLAlchemy <http://packages.python.org/Flask-SQLAlchemy/>`_
2. `Flask-MongoEngine <http://packages.python.org/Flask-MongoEngine/>`_
1. `Flask-SQLAlchemy <http://pypi.python.org/pypi/flask-sqlalchemy/>`_
2. `Flask-MongoEngine <http://pypi.python.org/pypi/flask-mongoengine/>`_
3. `Flask-Peewee <http://pypi.python.org/pypi/flask-peewee/>`_
.. include:: contents.rst.inc
.. include:: contents.rst.inc
+7 -7
View File
@@ -1,12 +1,12 @@
Models
======
Flask-Security assumes you'll be using libraries such as SQLAlchemy or
MongoEngine to define a data model that includes a `User` and `Role` model. The
fields on your models must follow a particular convention depending on the
functionality your app requires. Aside from this, you're free to add any
additional fields to your model(s) if you want. At the bear minimum your `User`
and `Role` model should include the following fields:
Flask-Security assumes you'll be using libraries such as SQLAlchemy,
MongoEngine or Peewee to define a data model that includes a `User` and
`Role` model. The fields on your models must follow a particular convention
depending on the functionality your app requires. Aside from this, you're
free to add any additional fields to your model(s) if you want. At the bear
minimum your `User` and `Role` model should include the following fields:
**User**
@@ -48,4 +48,4 @@ additional fields:
* ``current_login_at``
* ``last_login_ip``
* ``current_login_ip``
* ``login_count``
* ``login_count``
+84 -3
View File
@@ -3,6 +3,7 @@ Quick Start
- `Basic SQLAlchemy Application <#basic-sqlalchemy-application>`_
- `Basic MongoEngine Application <#basic-mongoengine-application>`_
- `Basic Peewee Application <#basic-peewee-application>`_
- `Mail Configuration <#mail-configuration>`_
Basic SQLAlchemy Application
@@ -21,7 +22,7 @@ SQLAlchemy Application
~~~~~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as
possible using SQLAlchemy.
possible using SQLAlchemy:
::
@@ -94,7 +95,9 @@ MongoEngine Application
~~~~~~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as
possible using MongoEngine::
possible using MongoEngine:
::
from flask import Flask, render_template
from flask.ext.mongoengine import MongoEngine
@@ -144,6 +147,84 @@ possible using MongoEngine::
app.run()
Basic Peewee Application
========================
Peewee Install requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
$ mkvirtualenv <your-app-name>
$ pip install flask-security flask-peewee
Peewee Application
~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as
possible using Peewee:
::
from flask import Flask, render_template
from flask_peewee.db import Database
from peewee import *
from flask.ext.security import Security, PeeweeUserDatastore, \
UserMixin, RoleMixin, login_required
# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['DATABASE'] = {
'name': 'example.db',
'engine': 'peewee.SqliteDatabase',
}
# Create database connection object
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
description = TextField(null=True)
class User(db.Model, UserMixin):
email = TextField()
password = TextField()
active = BooleanField(default=True)
confirmed_at = DateTimeField(null=True)
class UserRoles(db.Model):
# Because peewee does not come with built-in many-to-many
# relationships, we need this intermediary class to link
# user to roles.
user = ForeignKeyField(User, related_name='roles')
role = ForeignKeyField(Role, related_name='users')
name = property(lambda self: self.role.name)
description = property(lambda self: self.role.description)
# Setup Flask-Security
user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles)
security = Security(app, user_datastore)
# Create a user to test with
@app.before_first_request
def create_user():
for Model in (Role, User, UserRoles):
Model.drop_table(fail_silently=True)
Model.create_table(fail_silently=True)
user_datastore.create_user(email='matt@nobien.net', password='password')
# Views
@app.route('/')
@login_required
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run()
Mail Configuration
===================
@@ -168,4 +249,4 @@ the basic application code in the previous section::
To learn more about the various Flask-Mail settings to configure it to
work with your particular email server configuration, please see the
`Flask-Mail documentation <http://packages.python.org/Flask-Mail/>`_.
`Flask-Mail documentation <http://packages.python.org/Flask-Mail/>`_.
+1 -1
View File
@@ -13,7 +13,7 @@
__version__ = '1.5.4'
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
from .decorators import auth_token_required, http_auth_required, \
login_required, roles_accepted, roles_required
from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
+45
View File
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.changeable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security recoverable module
:copyright: (c) 2012 by Matt Wright.
:author: Eskil Heyn Olsen
:license: MIT, see LICENSE for more details.
"""
from flask import current_app as app, request
from werkzeug.local import LocalProxy
from .signals import password_changed
from .utils import send_mail, encrypt_password, url_for_security, \
config_value
# Convenient references
_security = LocalProxy(lambda: app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
def send_password_changed_notice(user):
"""Sends the password changed notice email for the specified user.
:param user: The user to send the notice to
"""
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE'), user.email,
'change_notice', user=user)
def change_user_password(user, password):
"""Change the specified user's password
:param user: The user to change_password
:param password: The unencrypted new password
"""
user.password = encrypt_password(password)
_datastore.put(user)
send_password_changed_notice(user)
password_changed.send(user, app=app._get_current_object())
+21 -5
View File
@@ -22,8 +22,8 @@ from werkzeug.local import LocalProxy
from .utils import config_value as cv, get_config, md5, url_for_security
from .views import create_blueprint
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \
SendConfirmationForm, PasswordlessLoginForm
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
@@ -40,6 +40,7 @@ _default_config = {
'LOGOUT_URL': '/logout',
'REGISTER_URL': '/register',
'RESET_URL': '/reset',
'CHANGE_URL': '/change',
'CONFIRM_URL': '/confirm',
'POST_LOGIN_VIEW': '/',
'POST_LOGOUT_VIEW': '/',
@@ -47,6 +48,7 @@ _default_config = {
'POST_REGISTER_VIEW': None,
'POST_CONFIRM_VIEW': None,
'POST_RESET_VIEW': None,
'POST_CHANGE_VIEW': None,
'UNAUTHORIZED_VIEW': None,
'FORGOT_PASSWORD_TEMPLATE': 'security/forgot_password.html',
'LOGIN_USER_TEMPLATE': 'security/login_user.html',
@@ -59,6 +61,7 @@ _default_config = {
'RECOVERABLE': False,
'TRACKABLE': False,
'PASSWORDLESS': False,
'CHANGEABLE': False,
'LOGIN_WITHIN': '1 days',
'CONFIRM_EMAIL_WITHIN': '5 days',
'RESET_PASSWORD_WITHIN': '5 days',
@@ -69,12 +72,14 @@ _default_config = {
'CONFIRM_SALT': 'confirm-salt',
'RESET_SALT': 'reset-salt',
'LOGIN_SALT': 'login-salt',
'CHANGE_SALT': 'change-salt',
'REMEMBER_SALT': 'remember-salt',
'DEFAULT_HTTP_AUTH_REALM': 'Login Required',
'EMAIL_SUBJECT_REGISTER': 'Welcome',
'EMAIL_SUBJECT_CONFIRM': 'Please confirm your email',
'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions',
'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset',
'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed',
'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions'
}
@@ -97,10 +102,16 @@ _default_messages = {
'LOGIN_EMAIL_SENT': ('Instructions to login have been sent to %(email)s.', 'success'),
'INVALID_LOGIN_TOKEN': ('Invalid login token.', 'error'),
'DISABLED_ACCOUNT': ('Account is disabled.', 'error'),
'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'),
'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'),
'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'),
'INVALID_PASSWORD': ('Invalid password', 'error'),
'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'),
'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'),
'PASSWORD_CHANGE': ('You successfully changed your password.', 'success'),
'INVALID_PASSWORD': ('Invalid password', 'error'),
'LOGIN': ('Please log in to access this page.', 'info'),
'REFRESH': ('Please reauthenticate to access this page.', 'info')
'REFRESH': ('Please reauthenticate to access this page.', 'info'),
}
_allowed_password_hash_schemes = [
@@ -120,6 +131,7 @@ _default_forms = {
'register_form': RegisterForm,
'forgot_password_form': ForgotPasswordForm,
'reset_password_form': ResetPasswordForm,
'change_password_form': ChangePasswordForm,
'send_confirmation_form': SendConfirmationForm,
'passwordless_login_form': PasswordlessLoginForm,
}
@@ -292,6 +304,9 @@ class _SecurityState(object):
def reset_password_context_processor(self, fn):
self._add_ctx_processor('reset_password', fn)
def change_password_context_processor(self, fn):
self._add_ctx_processor('change_password', fn)
def send_confirmation_context_processor(self, fn):
self._add_ctx_processor('send_confirmation', fn)
@@ -321,8 +336,8 @@ class Security(object):
def init_app(self, app, datastore=None, register_blueprint=True,
login_form=None, confirm_register_form=None,
register_form=None, forgot_password_form=None,
reset_password_form=None, send_confirmation_form=None,
passwordless_login_form=None):
reset_password_form=None, change_password_form=None,
send_confirmation_form=None, passwordless_login_form=None):
"""Initializes the Flask-Security extension for the specified
application and datastore implentation.
@@ -346,6 +361,7 @@ class Security(object):
register_form=register_form,
forgot_password_form=forgot_password_form,
reset_password_form=reset_password_form,
change_password_form=change_password_form,
send_confirmation_form=send_confirmation_form,
passwordless_login_form=passwordless_login_form)
+83 -4
View File
@@ -44,6 +44,15 @@ class MongoEngineDatastore(Datastore):
model.delete()
class PeeweeDatastore(Datastore):
def put(self, model):
model.save()
return model
def delete(self, model):
model.delete_instance()
class UserDatastore(object):
"""Abstracted user datastore.
@@ -72,12 +81,12 @@ class UserDatastore(object):
kwargs['roles'] = roles
return kwargs
def find_user(self, **kwargs):
"""Returns a user matching the provided paramters."""
def find_user(self, *args, **kwargs):
"""Returns a user matching the provided parameters."""
raise NotImplementedError
def find_role(self, **kwargs):
"""Returns a role matching the provided paramters."""
def find_role(self, *args, **kwargs):
"""Returns a role matching the provided name."""
raise NotImplementedError
def add_role_to_user(self, user, role):
@@ -137,6 +146,13 @@ class UserDatastore(object):
role = self.role_model(**kwargs)
return self.put(role)
def find_or_create_role(self, name, **kwargs):
"""Returns a role matching the given name or creates it with any
additionally provided parameters
"""
kwargs["name"] = name
return self.find_role(name) or self.create_role(**kwargs)
def create_user(self, **kwargs):
"""Creates and returns a new user from the given parameters."""
@@ -179,3 +195,66 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
def find_role(self, role):
return self.role_model.objects(name=role).first()
class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
"""A PeeweeD datastore implementation for Flask-Security that assumes
the use of the Flask-Peewee extension.
:param user_model: A user model class definition
:param role_model: A role model class definition
:param role_link: A model implementing the many-to-many user-role relation
"""
def __init__(self, db, user_model, role_model, role_link):
PeeweeDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
self.UserRole = role_link
def find_user(self, **kwargs):
try:
return self.user_model.filter(**kwargs).get()
except self.user_model.DoesNotExist:
return None
def find_role(self, role):
try:
return self.role_model.filter(name=role).get()
except self.role_model.DoesNotExist:
return None
def create_user(self, **kwargs):
"""Creates and returns a new user from the given parameters."""
roles = kwargs.pop('roles', [])
user = self.user_model(**self._prepare_create_user_args(**kwargs))
user = self.put(user)
for role in roles:
self.add_role_to_user(user, role)
return user
def add_role_to_user(self, user, role):
"""Adds a role tp a user
:param user: The user to manipulate
:param role: The role to add to the user
"""
user, role = self._prepare_role_modify_args(user, role)
if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count():
return False
else:
self.UserRole.create(user=user, role=role)
return True
def remove_role_from_user(self, user, role):
"""Removes a role from a user
:param user: The user to manipulate
:param role: The role to remove from the user
"""
user, role = self._prepare_role_modify_args(user, role)
if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count():
self.UserRole.delete().where(self.UserRole.user==user, self.UserRole.role==role)
return True
else:
return False
+32
View File
@@ -48,6 +48,8 @@ def _check_token():
args_key = _security.token_authentication_key
header_token = request.headers.get(header_key, None)
token = request.args.get(args_key, header_token)
if request.json:
token = request.json.get(args_key, token)
serializer = _security.remember_token_serializer
try:
@@ -113,6 +115,36 @@ def auth_token_required(fn):
return decorated
def auth_required(*auth_methods):
"""
Decorator that protects enpoints through multiple mechanisms
Example::
@app.route('/dashboard')
@auth_required('token', 'session')
def dashboard():
return 'Dashboard'
:param auth_methods: Specified mechanisms.
"""
login_mechanisms = {
'token': lambda: _check_token(),
'basic': lambda: _check_http_auth(),
'session': lambda: current_user.is_authenticated()
}
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
mechanisms = [login_mechanisms.get(method) for method in auth_methods]
for mechanism in mechanisms:
if mechanism and mechanism():
return fn(*args, **kwargs)
return _get_unauthorized_response()
return decorated_view
return wrapper
def roles_required(*roles):
"""Decorator which specifies that a user must have all the specified roles.
Example::
+39 -11
View File
@@ -15,10 +15,11 @@ from flask import request, current_app
from flask.ext.wtf import Form as BaseForm, TextField, PasswordField, \
SubmitField, HiddenField, Required, BooleanField, EqualTo, Email, \
ValidationError, Length, Field
from flask.ext.login import current_user
from werkzeug.local import LocalProxy
from .confirmable import requires_confirmation
from .utils import verify_and_update_password, get_message
from .utils import verify_password, verify_and_update_password, get_message
# Convenient reference
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
@@ -45,10 +46,7 @@ def valid_user_email(form, field):
class Form(BaseForm):
def __init__(self, *args, **kwargs):
if current_app.testing:
csrf_enabled = False
else:
csrf_enabled = request.json is None
kwargs.setdefault('csrf_enabled', csrf_enabled)
self.TIME_LIMIT = None
super(Form, self).__init__(*args, **kwargs)
@@ -83,6 +81,7 @@ class NewPasswordFormMixin():
validators=[password_required,
Length(min=6, max=128)])
class PasswordConfirmFormMixin():
password_confirm = PasswordField("Retype Password",
validators=[EqualTo('password', message="Passwords do not match")])
@@ -97,7 +96,8 @@ class RegisterFormMixin():
def to_dict(form):
def is_field_and_user_attr(member):
return isinstance(member, Field) and hasattr(_datastore.user_model, member.name)
return isinstance(member, Field) and \
hasattr(_datastore.user_model, member.name)
fields = inspect.getmembers(form, is_field_and_user_attr)
return dict((key, value.data) for key, value in fields)
@@ -147,6 +147,7 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
class LoginForm(Form, NextFormMixin):
"""The default login form"""
email = TextField('Email Address')
password = PasswordField('Password')
remember = BooleanField("Remember Me")
@@ -156,23 +157,24 @@ class LoginForm(Form, NextFormMixin):
super(LoginForm, self).__init__(*args, **kwargs)
def validate(self):
super(LoginForm, self).validate()
if not super(LoginForm, self).validate():
return False
if self.email.data.strip() == '':
self.email.errors.append('Email not provided')
self.email.errors.append(get_message('EMAIL_NOT_PROVIDED')[0])
return False
if self.password.data.strip() == '':
self.password.errors.append('Password not provided')
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
return False
self.user = _datastore.find_user(email=self.email.data)
if self.user is None:
self.email.errors.append('Specified user does not exist')
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
return False
if not verify_and_update_password(self.password.data, self.user):
self.password.errors.append('Invalid password')
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False
if requires_confirmation(self.user):
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
@@ -187,6 +189,7 @@ class ConfirmRegisterForm(Form, RegisterFormMixin,
UniqueEmailFormMixin, NewPasswordFormMixin):
pass
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
pass
@@ -195,3 +198,28 @@ class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
"""The default reset password form"""
submit = SubmitField("Reset Password")
class ChangePasswordForm(Form, PasswordFormMixin):
"""The default change password form"""
new_password = PasswordField("New Password",
validators=[password_required,
Length(min=6, max=128)])
new_password_confirm = PasswordField("Retype Password",
validators=[EqualTo('new_password', message="Passwords do not match")])
submit = SubmitField("Change Password")
def validate(self):
if not super(ChangePasswordForm, self).validate():
return False
if self.password.data.strip() == '':
self.password.errors.append('Password not provided')
return False
if not verify_and_update_password(self.password.data, current_user):
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False
return True
+2
View File
@@ -24,4 +24,6 @@ login_instructions_sent = signals.signal("login-instructions-sent")
password_reset = signals.signal("password-reset")
password_changed = signals.signal("password-changed")
reset_password_instructions_sent = signals.signal("password-reset-instructions-sent")
+1 -1
View File
@@ -12,4 +12,4 @@
<li><a href="{{ url_for_security('send_confirmation') }}">Confirm account</a></li>
{% endif %}
</ul>
{% endif %}
{% endif %}
@@ -0,0 +1,11 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% include "security/_messages.html" %}
<h1>Change password</h1>
<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
{{ change_password_form.hidden_tag() }}
{{ render_field_with_errors(change_password_form.password) }}
{{ render_field_with_errors(change_password_form.new_password) }}
{{ render_field_with_errors(change_password_form.new_password_confirm) }}
{{ render_field(change_password_form.submit) }}
</form>
@@ -0,0 +1,4 @@
<p>Your password has been changed.</p>
{% if security.recoverable %}
<p>If you did not change your password, <a href="{{ url_for_security('forgot_password', _external=True) }}">click here to reset it</a>.</p>
{% endif %}
@@ -0,0 +1,5 @@
Your password has been changed
{% if security.recoverable %}
If you did not change your password, click the link below to reset it.
{{ url_for_security('forgot_password', _external=True) }}
{% endif %}
+3 -2
View File
@@ -27,7 +27,7 @@ from werkzeug.local import LocalProxy
from .signals import user_registered, user_confirmed, \
confirm_instructions_sent, login_instructions_sent, \
password_reset, reset_password_instructions_sent
password_reset, password_changed, reset_password_instructions_sent
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
@@ -373,6 +373,7 @@ def capture_signals():
"""Factory method that creates a `CaptureSignals` with all the flask_security signals."""
return CaptureSignals([user_registered, user_confirmed,
confirm_instructions_sent, login_instructions_sent,
password_reset, reset_password_instructions_sent])
password_reset, password_changed,
reset_password_instructions_sent])
+34
View File
@@ -11,6 +11,7 @@
from flask import current_app, redirect, request, render_template, jsonify, \
after_this_request, Blueprint
from flask_login import current_user
from werkzeug.datastructures import MultiDict
from werkzeug.local import LocalProxy
@@ -21,6 +22,7 @@ from .passwordless import send_login_instructions, \
login_token_status
from .recoverable import reset_password_token_status, \
send_reset_password_instructions, update_password
from .changeable import change_user_password
from .registerable import register_user
from .utils import get_url, get_post_login_redirect, do_flash, \
get_message, login_user, logout_user, url_for_security as url_for, \
@@ -279,6 +281,33 @@ def reset_password(token):
**_ctx('reset_password'))
@login_required
def change_password():
"""View function which handles a change password request."""
form_class = _security.change_password_form
if request.json:
form = form_class(MultiDict(request.json))
else:
form = form_class()
if form.validate_on_submit():
after_this_request(_commit)
change_user_password(current_user, form.new_password.data)
if request.json is None:
do_flash(*get_message('PASSWORD_CHANGE'))
return redirect(get_url(_security.post_change_view) or
get_url(_security.post_login_view))
if request.json:
return _render_json(form)
return render_template('security/change_password.html',
change_password_form=form,
**_ctx('change_password'))
def create_blueprint(state, import_name):
"""Creates the security extension blueprint"""
@@ -312,6 +341,11 @@ def create_blueprint(state, import_name):
methods=['GET', 'POST'],
endpoint='reset_password')(reset_password)
if state.changeable:
bp.route(state.change_url,
methods=['GET', 'POST'],
endpoint='change_password')(change_password)
if state.confirmable:
bp.route(state.confirm_url,
methods=['GET', 'POST'],
+2 -3
View File
@@ -1,8 +1,7 @@
Flask>=0.9
Flask-Login==0.1.3
Flask-Login>=0.1.3
Flask-Mail>=0.7.3
Flask-Principal>=0.3.3
Flask-Script==0.5.3
Flask-WTF>=0.8
itsdangerous>=0.17
passlib==1.6.1
passlib>=1.6.1
+1
View File
@@ -48,6 +48,7 @@ setup(
'nose',
'Flask-SQLAlchemy',
'Flask-MongoEngine',
'Flask-Peewee',
'py-bcrypt',
'simplejson'
],
+24 -12
View File
@@ -1,8 +1,13 @@
# -*- coding: utf-8 -*-
import hmac
from hashlib import sha1
from unittest import TestCase
from tests.test_app.sqlalchemy import create_app
class SecurityTest(TestCase):
APP_KWARGS = {
@@ -21,6 +26,13 @@ class SecurityTest(TestCase):
self.app = app
self.client = app.test_client()
with self.client.session_transaction() as session:
session['csrf'] = 'csrf_token'
csrf_hmac = hmac.new(self.app.config['SECRET_KEY'],
'csrf_token'.encode('utf8'), digestmod=sha1)
self.csrf_token = '##' + csrf_hmac.hexdigest()
def _create_app(self, auth_config, **kwargs):
return create_app(auth_config, **kwargs)
@@ -30,30 +42,30 @@ class SecurityTest(TestCase):
headers=headers)
def _post(self, route, data=None, content_type=None, follow_redirects=True, headers=None):
if isinstance(data, dict):
data['csrf_token'] = self.csrf_token
return self.client.post(route, data=data,
follow_redirects=follow_redirects,
content_type=content_type or 'application/x-www-form-urlencoded',
headers=headers)
def register(self, email, password='password'):
data = dict(email=email, password=password)
data = dict(email=email, password=password, csrf_token=self.csrf_token)
return self.client.post('/register', data=data, follow_redirects=True)
def authenticate(self, email="matt@lp.com", password="password", endpoint=None, **kwargs):
data = dict(email=email, password=password, remember='y')
r = self._post(endpoint or '/login', data=data, **kwargs)
return r
return self._post(endpoint or '/login', data=data, **kwargs)
def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None):
data = """
{
"email": "%s",
"password": "%s"
}
"""
return self._post(endpoint or '/login',
content_type="application/json",
data=data % (email, password))
data = """{
"email": "%s",
"password": "%s",
"csrf_token": "%s"
}"""
return self._post(endpoint or '/login', content_type="application/json",
data=data % (email, password, self.csrf_token))
def logout(self, endpoint=None):
return self._get(endpoint or '/logout', follow_redirects=True)
+116 -33
View File
@@ -67,7 +67,7 @@ class ConfiguredSecurityTests(SecurityTest):
self.assertIn('Post Register', r.data)
def test_register_json(self):
data = '{ "email": "dude@lp.com", "password": "password" }'
data = '{ "email": "dude@lp.com", "password": "password", "csrf_token":"%s" }' % self.csrf_token
r = self._post('/register', data=data, content_type='application/json')
data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200)
@@ -145,9 +145,10 @@ class RecoverableTemplatePathTests(SecurityTest):
def test_reset_password_template(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
r = self._post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._get('/reset/' + t)
@@ -189,7 +190,7 @@ class RegisterableTests(SecurityTest):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
self.client.post('/register', data=data, follow_redirects=True)
self._post('/register', data=data, follow_redirects=True)
r = self.authenticate('dude@lp.com')
self.assertIn('Hello dude@lp.com', r.data)
@@ -217,7 +218,7 @@ class ConfirmableTests(SecurityTest):
self.client.get('/confirm/' + token, follow_redirects=True)
self.logout()
r = self.client.post('/confirm', data=dict(email=e))
r = self._post('/confirm', data=dict(email=e))
self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data)
def test_register_sends_confirmation_email(self):
@@ -303,7 +304,7 @@ class LoginWithoutImmediateConfirmTests(SecurityTest):
e = 'dude@lp.com'
p = 'password'
data = dict(email=e, password=p, password_confirm=p)
r = self.client.post('/register', data=data, follow_redirects=True)
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn(e, r.data)
@@ -317,7 +318,7 @@ class RecoverableTests(SecurityTest):
def test_reset_view(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
r = self._post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
@@ -327,23 +328,23 @@ class RecoverableTests(SecurityTest):
def test_forgot_post_sends_email(self):
with capture_reset_password_requests():
with self.app.extensions['mail'].record_messages() as outbox:
self.client.post('/reset', data=dict(email='joe@lp.com'))
self._post('/reset', data=dict(email='joe@lp.com'))
self.assertEqual(len(outbox), 1)
def test_forgot_password_json(self):
r = self.client.post('/reset', data='{"email": "matt@lp.com"}',
r = self._post('/reset', data='{"email": "matt@lp.com"}',
content_type="application/json")
self.assertEquals(r.status_code, 200)
def test_forgot_password_invalid_email(self):
r = self.client.post('/reset',
r = self._post('/reset',
data=dict(email='larry@lp.com'),
follow_redirects=True)
self.assertIn("Specified user does not exist", r.data)
def test_reset_password_with_valid_token(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
r = self._post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
@@ -375,14 +376,13 @@ class ExpiredResetPasswordTest(SecurityTest):
def test_reset_password_with_expired_token(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
r = self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
time.sleep(1)
r = self.client.post('/reset/' + t, data={
r = self._post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
@@ -390,6 +390,95 @@ class ExpiredResetPasswordTest(SecurityTest):
self.assertIn('You did not reset your password within', r.data)
class ChangePasswordTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_CHANGEABLE': True,
}
def test_change_password(self):
self.authenticate()
r = self.client.get('/change', follow_redirects=True)
self.assertIn('Change password', r.data)
def test_change_password_invalid(self):
self.authenticate()
r = self._post('/change', data={
'password': 'notpassword',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertNotIn('You successfully changed your password', r.data)
self.assertIn('Invalid password', r.data)
def test_change_password_mismatch(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'notnewpassword'
}, follow_redirects=True)
self.assertNotIn('You successfully changed your password', r.data)
self.assertIn('Passwords do not match', r.data)
def test_change_password_bad_password(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'a',
'new_password_confirm': 'a'
}, follow_redirects=True)
self.assertNotIn('You successfully changed your password', r.data)
self.assertIn('Field must be between', r.data)
def test_change_password_success(self):
self.authenticate()
with self.app.extensions['mail'].record_messages() as outbox:
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('You successfully changed your password', r.data)
self.assertIn('Home Page', r.data)
self.assertEqual(len(outbox), 1)
self.assertIn("Your password has been changed", outbox[0].html)
self.assertIn("/reset", outbox[0].html)
class ChangePasswordPostViewTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': True,
'SECURITY_POST_CHANGE_VIEW': '/profile',
}
def test_change_password_success(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('Profile Page', r.data)
class ChangePasswordDisabledTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': False,
}
def test_change_password_endpoint_is_404(self):
self.authenticate()
r = self.client.get('/change', follow_redirects=True)
self.assertEqual(404, r.status_code)
class TrackableTests(SecurityTest):
AUTH_CONFIG = {
@@ -420,20 +509,19 @@ class PasswordlessTests(SecurityTest):
def test_login_request_for_inactive_user(self):
msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0]
r = self.client.post('/login',
data=dict(email='tiya@lp.com'),
follow_redirects=True)
r = self._post('/login', data=dict(email='tiya@lp.com'),
follow_redirects=True)
self.assertIn(msg, r.data)
def test_request_login_token_with_json_and_valid_email(self):
data = '{"email": "matt@lp.com", "password": "password"}'
r = self.client.post('/login', data=data, content_type='application/json')
data = '{"email": "matt@lp.com", "password": "password", "csrf_token":"%s"}' % self.csrf_token
r = self._post('/login', data=data, content_type='application/json')
self.assertEquals(r.status_code, 200)
self.assertNotIn('error', r.data)
def test_request_login_token_with_json_and_invalid_email(self):
data = '{"email": "nobody@lp.com", "password": "password"}'
r = self.client.post('/login', data=data, content_type='application/json')
r = self._post('/login', data=data, content_type='application/json')
self.assertIn('errors', r.data)
def test_request_login_token_sends_email_and_can_login(self):
@@ -442,9 +530,8 @@ class PasswordlessTests(SecurityTest):
with capture_passwordless_login_requests() as requests:
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.post('/login',
data=dict(email=e),
follow_redirects=True)
r = self._post('/login', data=dict(email=e),
follow_redirects=True)
self.assertEqual(len(outbox), 1)
@@ -473,9 +560,8 @@ class PasswordlessTests(SecurityTest):
def test_token_login_when_already_authenticated(self):
with capture_passwordless_login_requests() as requests:
self.client.post('/login',
data=dict(email='matt@lp.com'),
follow_redirects=True)
self._post('/login', data=dict(email='matt@lp.com'),
follow_redirects=True)
token = requests[0]['login_token']
r = self.client.get('/login/' + token, follow_redirects=True)
@@ -503,9 +589,7 @@ class ExpiredLoginTokenTests(SecurityTest):
e = 'matt@lp.com'
with capture_passwordless_login_requests() as requests:
self.client.post('/login',
data=dict(email=e),
follow_redirects=True)
self._post('/login', data=dict(email=e), follow_redirects=True)
token = requests[0]['login_token']
time.sleep(1.25)
@@ -539,7 +623,7 @@ class AsyncMailTaskTests(SecurityTest):
def send_email(msg):
self.mail_sent = True
self.client.post('/reset', data=dict(email='matt@lp.com'))
self._post('/reset', data=dict(email='matt@lp.com'))
self.assertTrue(self.mail_sent)
@@ -614,9 +698,8 @@ class RecoverableExtendFormsTest(SecurityTest):
def test_reset_password(self):
with capture_reset_password_requests() as requests:
self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
token = requests[0]['token']
r = self._get('/reset/' + token)
self.assertIn("My Reset Password Submit Field", r.data)
+30 -5
View File
@@ -35,23 +35,23 @@ class DefaultSecurityTests(SecurityTest):
def test_unprovided_username(self):
r = self.authenticate("")
self.assertIn("Email not provided", r.data)
self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data)
def test_unprovided_password(self):
r = self.authenticate(password="")
self.assertIn("Password not provided", r.data)
self.assertIn(self.get_message('PASSWORD_NOT_PROVIDED'), r.data)
def test_invalid_user(self):
r = self.authenticate(email="bogus@bogus.com")
self.assertIn("Specified user does not exist", r.data)
self.assertIn(self.get_message('USER_DOES_NOT_EXIST'), r.data)
def test_bad_password(self):
r = self.authenticate(password="bogus")
self.assertIn("Invalid password", r.data)
self.assertIn(self.get_message('INVALID_PASSWORD'), r.data)
def test_inactive_user(self):
r = self.authenticate("tiya@lp.com", "password")
self.assertIn("Account is disabled", r.data)
self.assertIn(self.get_message('DISABLED_ACCOUNT'), r.data)
def test_logout(self):
self.authenticate()
@@ -169,6 +169,24 @@ class DefaultSecurityTests(SecurityTest):
self.assertEquals('Basic realm="My Realm"',
r.headers['WWW-Authenticate'])
def test_multi_auth_basic(self):
r = self._get('/multi_auth', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password")
})
self.assertIn('Basic', r.data)
def test_multi_auth_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
r = self._get('/multi_auth?auth_token=' + token)
self.assertIn('Token', r.data)
def test_multi_auth_session(self):
self.authenticate()
r = self._get('/multi_auth')
self.assertIn('Session', r.data)
def test_user_deleted_during_session_reverts_to_anonymous_user(self):
self.authenticate()
@@ -206,6 +224,13 @@ class MongoEngineSecurityTests(DefaultSecurityTests):
return create_app(auth_config, **kwargs)
# class PeeweeSecurityTests(DefaultSecurityTests):
# def _create_app(self, auth_config, **kwargs):
# from tests.test_app.peewee_app import create_app
# return create_app(auth_config, **kwargs)
class DefaultDatastoreTests(SecurityTest):
def test_add_role_to_user(self):
+69 -28
View File
@@ -3,7 +3,8 @@ from __future__ import with_statement
from flask_security.utils import (capture_registrations, capture_reset_password_requests, capture_signals)
from flask_security.signals import (user_registered, user_confirmed,
confirm_instructions_sent, login_instructions_sent,
password_reset, reset_password_instructions_sent)
password_reset, password_changed,
reset_password_instructions_sent)
from tests import SecurityTest
@@ -32,7 +33,7 @@ class RegisterableSignalsTests(SecurityTest):
self.assertIn('confirm_token', args[0])
self.assertEqual(kwargs['app'], self.app)
def test_register(self):
def test_register_without_password(self):
e = 'dude@lp.com'
with capture_signals() as mocks:
self.register(e, password='')
@@ -111,9 +112,8 @@ class RecoverableSignalsTests(SecurityTest):
def test_reset_password_request(self):
with capture_signals() as mocks:
self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([reset_password_instructions_sent]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[reset_password_instructions_sent]
@@ -125,15 +125,12 @@ class RecoverableSignalsTests(SecurityTest):
def test_reset_password(self):
with capture_reset_password_requests() as requests:
self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
token = requests[0]['token']
with capture_signals() as mocks:
self.client.post('/reset/' + token,
data=dict(password='newpassword',
password_confirm='newpassword'),
follow_redirects=True)
data = dict(password='newpassword', password_confirm='newpassword')
self._post('/reset/' + token, data, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([password_reset]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[password_reset]
@@ -144,16 +141,64 @@ class RecoverableSignalsTests(SecurityTest):
def test_reset_password_invalid_emails(self):
with capture_signals() as mocks:
self.client.post('/reset',
data=dict(email='nobody@lp.com'),
follow_redirects=True)
self._post('/reset', data=dict(email='nobody@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_reset_password_invalid_token(self):
with capture_signals() as mocks:
self.client.post('/reset/bogus',
data=dict(password='newpassword',
password_confirm='newpassword'),
data = dict(password='newpassword', password_confirm='newpassword')
self._post('/reset/bogus', data, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
class ChangeableSignalsTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': True,
}
def test_change_password(self):
self.authenticate('joe@lp.com')
with capture_signals() as mocks:
with self.client as client:
client.post('/change',
data=dict(password='password',
new_password='newpassword',
new_password_confirm='newpassword',
csrf_token=self.csrf_token))
self.assertEqual(mocks.signals_sent(), set([password_changed]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[password_changed]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(args[0], user))
self.assertEqual(kwargs['app'], self.app)
def test_change_password_invalid_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='notpassword',
new_password='newpassword',
new_password_confirm='newpassword'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_change_password_bad_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='notpassword',
new_password='a',
new_password_confirm='a'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_change_password_mismatch_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='password',
new_password='newpassword',
new_password_confirm='notnewpassword'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
@@ -166,25 +211,22 @@ class PasswordlessTests(SecurityTest):
def test_login_request_for_inactive_user(self):
with capture_signals() as mocks:
self.client.post('/login',
data=dict(email='tiya@lp.com'),
follow_redirects=True)
self._post('/login', data=dict(email='tiya@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_login_request_for_invalid_email(self):
with capture_signals() as mocks:
self.client.post('/login',
data=dict(email='nobody@lp.com'),
follow_redirects=True)
self._post('/login', data=dict(email='nobody@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_request_login_token_sends_email_and_can_login(self):
e = 'matt@lp.com'
with capture_signals() as mocks:
self.client.post('/login',
data=dict(email=e),
follow_redirects=True)
self._post('/login', data=dict(email=e), follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([login_instructions_sent]))
user = self.app.security.datastore.find_user(email='matt@lp.com')
calls = mocks[login_instructions_sent]
@@ -193,4 +235,3 @@ class PasswordlessTests(SecurityTest):
self.assertTrue(compare_user(args[0]['user'], user))
self.assertIn('login_token', args[0])
self.assertEqual(kwargs['app'], self.app)
+6 -1
View File
@@ -4,7 +4,7 @@ from flask import Flask, render_template, current_app
from flask.ext.mail import Mail
from flask.ext.security import login_required, roles_required, roles_accepted
from flask.ext.security.decorators import http_auth_required, \
auth_token_required
auth_token_required, auth_required
from flask.ext.security.utils import encrypt_password
from werkzeug.local import LocalProxy
@@ -50,6 +50,11 @@ def create_app(config):
def token():
return render_template('index.html', content='Token Authentication')
@app.route('/multi_auth')
@auth_required('session', 'token', 'basic')
def multi_auth():
return render_template('index.html', content='Session, Token, Basic auth')
@app.route('/post_logout')
def post_logout():
return render_template('index.html', content='Post Logout')
+62
View File
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask_peewee.db import Database
from peewee import *
from flask.ext.security import Security, UserMixin, RoleMixin, \
PeeweeUserDatastore
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config, **kwargs):
app = create_base_app(config)
app.config['DATABASE'] = {
'name': 'example2.db',
'engine': 'peewee.SqliteDatabase',
}
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
description = TextField(null=True)
class User(db.Model, UserMixin):
email = TextField()
password = TextField()
last_login_at = DateTimeField(null=True)
current_login_at = DateTimeField(null=True)
last_login_ip = TextField(null=True)
current_login_ip = TextField(null=True)
login_count = IntegerField(null=True)
active = BooleanField(default=True)
confirmed_at = DateTimeField(null=True)
class UserRoles(db.Model):
""" Peewee does not have built-in many-to-many support, so we have to
create this mapping class to link users to roles."""
user = ForeignKeyField(User, related_name='roles')
role = ForeignKeyField(Role, related_name='users')
name = property(lambda self: self.role.name)
description = property(lambda self: self.role.description)
@app.before_first_request
def before_first_request():
for Model in (Role, User, UserRoles):
Model.drop_table(fail_silently=True)
Model.create_table(fail_silently=True)
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, datastore=PeeweeUserDatastore(db, User, Role, UserRoles), **kwargs)
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
+2 -2
View File
@@ -55,8 +55,8 @@ class DatastoreTests(unittest.TestCase):
self.assertRaises(NotImplementedError, ds.delete, None)
def test_unimplemented_user_datastore_methods(self):
self.assertRaises(NotImplementedError, self.ds.find_user)
self.assertRaises(NotImplementedError, self.ds.find_role)
self.assertRaises(NotImplementedError, self.ds.find_user, None)
self.assertRaises(NotImplementedError, self.ds.find_role, None)
def test_toggle_active(self):
user.active = True