mirror of
https://github.com/wassname/flask-security.git
synced 2026-06-27 16:10:11 +08:00
Merge branch 'develop' of https://github.com/mattupstate/flask-security into develop
This commit is contained in:
+10
@@ -34,3 +34,13 @@ env/
|
||||
|
||||
*.db
|
||||
*cache*
|
||||
|
||||
# vim
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
*.un~
|
||||
Session.vim
|
||||
.netrwhist
|
||||
*~
|
||||
|
||||
.eggs/README.txt
|
||||
|
||||
@@ -36,3 +36,4 @@ Rotem Yaari
|
||||
Srijan Choudhary
|
||||
Tristan Escalada
|
||||
Vadim Kotov
|
||||
Walt Askew
|
||||
|
||||
+14
-19
@@ -87,43 +87,38 @@ sends the following signals.
|
||||
|
||||
.. data:: user_registered
|
||||
|
||||
Sent when a user registers on the site. It is passed a dict with
|
||||
the `user` and `confirm_token`, the user being logged in and the
|
||||
(if so configured) the confirmation token issued.
|
||||
Sent when a user registers on the site. In addition to the app (which is the
|
||||
sender), it is passed `user` and `confirm_token` arguments.
|
||||
|
||||
.. data:: user_confirmed
|
||||
|
||||
Sent when a user is confirmed. It is passed `user`, which is the
|
||||
user being confirmed.
|
||||
Sent when a user is confirmed. In addition to the app (which is the
|
||||
sender), it is passed a `user` argument.
|
||||
|
||||
.. data:: confirm_instructions_sent
|
||||
|
||||
Sent when a user requests confirmation instructions. It is passed
|
||||
the `user`.
|
||||
Sent when a user requests confirmation instructions. In addition to the app
|
||||
(which is the sender), it is passed a `user` argument.
|
||||
|
||||
.. data:: login_instructions_sent
|
||||
|
||||
Sent when passwordless login is used and user logs in. It is passed
|
||||
a dict with the `user` and `login_token`, the user being logged in
|
||||
and the (if so configured) the login token issued.
|
||||
Sent when passwordless login is used and user logs in. In addition to the app
|
||||
(which is the sender), it is passed `user` and `login_token` arguments.
|
||||
|
||||
.. data:: password_reset
|
||||
|
||||
Sent when a user completes a password reset. It is passed the
|
||||
`user`.
|
||||
Sent when a user completes a password reset. In addition to the app (which is
|
||||
the sender), it is passed a `user` argument.
|
||||
|
||||
.. data:: password_changed
|
||||
|
||||
Sent when a user completes a password change. It is passed the
|
||||
`user`.
|
||||
Sent when a user completes a password change. In addition to the app (which is
|
||||
the sender), it is passed a `user` argument.
|
||||
|
||||
.. data:: reset_password_instructions_sent
|
||||
|
||||
Sent when a user requests a password reset. It is passed a dict
|
||||
with the `user` and `token`, the user being logged in and
|
||||
the (if so configured) the reset token issued.
|
||||
Sent when a user requests a password reset. In addition to the app (which is
|
||||
the sender), it is passed `user` and `token` arguments.
|
||||
|
||||
All signals are also passed a `app` keyword argument, which is the
|
||||
current application.
|
||||
|
||||
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
|
||||
|
||||
+11
-3
@@ -23,7 +23,10 @@ Core
|
||||
passwords. Recommended values for
|
||||
production systems are ``bcrypt``,
|
||||
``sha512_crypt``, or ``pbkdf2_sha512``.
|
||||
Defaults to ``plaintext``.
|
||||
Defaults to ``plaintext``. Note:
|
||||
``bcrypt>=2.0.0`` is not currently
|
||||
supported. If ``bcrypt`` is preferred,
|
||||
please use ``bcrypt<2.0``.
|
||||
``SECURITY_PASSWORD_SALT`` Specifies the HMAC salt. This is only
|
||||
used if the password hash type is set
|
||||
to something other than plain text.
|
||||
@@ -31,12 +34,16 @@ Core
|
||||
``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
|
||||
``SECURITY_TOKEN_AUTHENTICATION_KEY`` Specifies the query string 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_TOKEN_MAX_AGE`` Specifies the number of seconds before
|
||||
an authentication token expires.
|
||||
Defaults to None, meaning the token
|
||||
never expires.
|
||||
``SECURITY_DEFAULT_HTTP_AUTH_REALM`` Specifies the default authentication
|
||||
realm when using basic HTTP auth.
|
||||
Defaults to ``Login Required``
|
||||
@@ -159,7 +166,8 @@ Feature Flags
|
||||
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
|
||||
models have the required fields/attribues. Be sure to
|
||||
use `ProxyFix <http://flask.pocoo.org/docs/0.10/deploying/wsgi-standalone/#proxy-setups>` if you are using a proxy. Defaults to
|
||||
``False``
|
||||
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the
|
||||
passwordless login feature. If set to ``True``, users
|
||||
|
||||
@@ -73,8 +73,8 @@ register form or override validators::
|
||||
from flask_security.forms import RegisterForm
|
||||
|
||||
class ExtendedRegisterForm(RegisterForm):
|
||||
first_name = TextField('First Name', [Required()])
|
||||
last_name = TextField('Last Name', [Required()])
|
||||
first_name = StringField('First Name', [Required()])
|
||||
last_name = StringField('Last Name', [Required()])
|
||||
|
||||
security = Security(app, user_datastore,
|
||||
register_form=ExtendedRegisterForm)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
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.
|
||||
@@ -10,8 +10,6 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__version__ = '1.7.4'
|
||||
|
||||
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
|
||||
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
|
||||
from .decorators import auth_token_required, http_auth_required, \
|
||||
@@ -21,3 +19,5 @@ from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
|
||||
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
|
||||
|
||||
__version__ = '1.7.4'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.changeable
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.changeable
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security recoverable module
|
||||
|
||||
@@ -42,4 +42,5 @@ def change_user_password(user, password):
|
||||
user.password = encrypt_password(password)
|
||||
_datastore.put(user)
|
||||
send_password_changed_notice(user)
|
||||
password_changed.send(app._get_current_object(), user=user)
|
||||
password_changed.send(app._get_current_object(),
|
||||
user=user._get_current_object())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.confirmable
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.confirmable
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security confirmable module
|
||||
|
||||
|
||||
+21
-14
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.core
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.core
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security core module
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
"""
|
||||
|
||||
from flask import current_app, render_template
|
||||
from flask.ext.login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
|
||||
from flask_login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
|
||||
LoginManager, current_user
|
||||
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \
|
||||
from flask_principal import Principal, RoleNeed, UserNeed, Identity, \
|
||||
identity_loaded
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from passlib.context import CryptContext
|
||||
@@ -75,6 +75,7 @@ _default_config = {
|
||||
'EMAIL_SENDER': 'no-reply@localhost',
|
||||
'TOKEN_AUTHENTICATION_KEY': 'auth_token',
|
||||
'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token',
|
||||
'TOKEN_MAX_AGE': None,
|
||||
'CONFIRM_SALT': 'confirm-salt',
|
||||
'RESET_SALT': 'reset-salt',
|
||||
'LOGIN_SALT': 'login-salt',
|
||||
@@ -192,17 +193,17 @@ def _user_loader(user_id):
|
||||
|
||||
def _token_loader(token):
|
||||
try:
|
||||
data = _security.remember_token_serializer.loads(token)
|
||||
data = _security.remember_token_serializer.loads(token, max_age=_security.token_max_age)
|
||||
user = _security.datastore.find_user(id=data[0])
|
||||
if user and safe_str_cmp(md5(user.password), data[1]):
|
||||
return user
|
||||
except:
|
||||
pass
|
||||
return AnonymousUser()
|
||||
return _security.login_manager.anonymous_user()
|
||||
|
||||
|
||||
def _identity_loader():
|
||||
if not isinstance(current_user._get_current_object(), AnonymousUser):
|
||||
if not isinstance(current_user._get_current_object(), AnonymousUserMixin):
|
||||
identity = Identity(current_user.id)
|
||||
return identity
|
||||
|
||||
@@ -217,9 +218,9 @@ def _on_identity_loaded(sender, identity):
|
||||
identity.user = current_user
|
||||
|
||||
|
||||
def _get_login_manager(app):
|
||||
def _get_login_manager(app, anonymous_user):
|
||||
lm = LoginManager()
|
||||
lm.anonymous_user = AnonymousUser
|
||||
lm.anonymous_user = anonymous_user or AnonymousUser
|
||||
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
|
||||
lm.user_loader(_user_loader)
|
||||
lm.token_loader(_token_loader)
|
||||
@@ -257,14 +258,14 @@ def _get_serializer(app, name):
|
||||
return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)
|
||||
|
||||
|
||||
def _get_state(app, datastore, **kwargs):
|
||||
def _get_state(app, datastore, anonymous_user=None, **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),
|
||||
login_manager=_get_login_manager(app, anonymous_user),
|
||||
principal=_get_principal(app),
|
||||
pwd_context=_get_pwd_context(app),
|
||||
remember_token_serializer=_get_serializer(app, 'remember'),
|
||||
@@ -272,7 +273,8 @@ def _get_state(app, datastore, **kwargs):
|
||||
reset_serializer=_get_serializer(app, 'reset'),
|
||||
confirm_serializer=_get_serializer(app, 'confirm'),
|
||||
_context_processors={},
|
||||
_send_mail_task=None
|
||||
_send_mail_task=None,
|
||||
_unauthorized_callback=None
|
||||
))
|
||||
|
||||
for key, value in _default_forms.items():
|
||||
@@ -380,6 +382,9 @@ class _SecurityState(object):
|
||||
def send_mail_task(self, fn):
|
||||
self._send_mail_task = fn
|
||||
|
||||
def unauthorized_handler(self, fn):
|
||||
self._unauthorized_callback = fn
|
||||
|
||||
|
||||
class Security(object):
|
||||
"""The :class:`Security` class initializes the Flask-Security extension.
|
||||
@@ -398,7 +403,8 @@ class Security(object):
|
||||
login_form=None, confirm_register_form=None,
|
||||
register_form=None, forgot_password_form=None,
|
||||
reset_password_form=None, change_password_form=None,
|
||||
send_confirmation_form=None, passwordless_login_form=None):
|
||||
send_confirmation_form=None, passwordless_login_form=None,
|
||||
anonymous_user=None):
|
||||
"""Initializes the Flask-Security extension for the specified
|
||||
application and datastore implentation.
|
||||
|
||||
@@ -424,7 +430,8 @@ class Security(object):
|
||||
reset_password_form=reset_password_form,
|
||||
change_password_form=change_password_form,
|
||||
send_confirmation_form=send_confirmation_form,
|
||||
passwordless_login_form=passwordless_login_form)
|
||||
passwordless_login_form=passwordless_login_form,
|
||||
anonymous_user=anonymous_user)
|
||||
|
||||
if register_blueprint:
|
||||
app.register_blueprint(create_blueprint(state, __name__))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.datastore
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.datastore
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains an user datastore classes.
|
||||
|
||||
@@ -195,7 +195,7 @@ class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
|
||||
def _is_numeric(self, value):
|
||||
try:
|
||||
int(value)
|
||||
except ValueError:
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.decorators
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.decorators
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security decorators module
|
||||
|
||||
@@ -13,8 +13,8 @@ from collections import namedtuple
|
||||
from functools import wraps
|
||||
|
||||
from flask import current_app, Response, request, redirect, _request_ctx_stack
|
||||
from flask.ext.login import current_user, login_required # pragma: no flakes
|
||||
from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed
|
||||
from flask_login import current_user, login_required # pragma: no flakes
|
||||
from flask_principal import RoleNeed, Permission, Identity, identity_changed
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from . import utils
|
||||
@@ -91,9 +91,12 @@ def http_auth_required(realm):
|
||||
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)
|
||||
if _security._unauthorized_callback:
|
||||
return _security._unauthorized_callback()
|
||||
else:
|
||||
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):
|
||||
@@ -113,7 +116,10 @@ def auth_token_required(fn):
|
||||
def decorated(*args, **kwargs):
|
||||
if _check_token():
|
||||
return fn(*args, **kwargs)
|
||||
return _get_unauthorized_response()
|
||||
if _security._unauthorized_callback:
|
||||
return _security._unauthorized_callback()
|
||||
else:
|
||||
return _get_unauthorized_response()
|
||||
return decorated
|
||||
|
||||
|
||||
@@ -138,11 +144,18 @@ def auth_required(*auth_methods):
|
||||
def wrapper(fn):
|
||||
@wraps(fn)
|
||||
def decorated_view(*args, **kwargs):
|
||||
mechanisms = [login_mechanisms.get(method) for method in auth_methods]
|
||||
for mechanism in mechanisms:
|
||||
h = {}
|
||||
mechanisms = [(method, login_mechanisms.get(method)) for method in auth_methods]
|
||||
for method, mechanism in mechanisms:
|
||||
if mechanism and mechanism():
|
||||
return fn(*args, **kwargs)
|
||||
return _get_unauthorized_response()
|
||||
elif method == 'basic':
|
||||
r = _security.default_http_auth_realm
|
||||
h['WWW-Authenticate'] = 'Basic realm="%s"' % r
|
||||
if _security._unauthorized_callback:
|
||||
return _security._unauthorized_callback()
|
||||
else:
|
||||
return _get_unauthorized_response(headers=h)
|
||||
return decorated_view
|
||||
return wrapper
|
||||
|
||||
@@ -167,7 +180,10 @@ def roles_required(*roles):
|
||||
perms = [Permission(RoleNeed(role)) for role in roles]
|
||||
for perm in perms:
|
||||
if not perm.can():
|
||||
return _get_unauthorized_view()
|
||||
if _security._unauthorized_callback:
|
||||
return _security._unauthorized_callback()
|
||||
else:
|
||||
return _get_unauthorized_view()
|
||||
return fn(*args, **kwargs)
|
||||
return decorated_view
|
||||
return wrapper
|
||||
@@ -193,7 +209,10 @@ def roles_accepted(*roles):
|
||||
perm = Permission(*[RoleNeed(role) for role in roles])
|
||||
if perm.can():
|
||||
return fn(*args, **kwargs)
|
||||
return _get_unauthorized_view()
|
||||
if _security._unauthorized_callback:
|
||||
return _security._unauthorized_callback()
|
||||
else:
|
||||
return _get_unauthorized_view()
|
||||
return decorated_view
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.forms
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.forms
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security forms module
|
||||
|
||||
@@ -13,7 +13,7 @@ import inspect
|
||||
|
||||
from flask import request, current_app, flash
|
||||
from flask_wtf import Form as BaseForm
|
||||
from wtforms import TextField, PasswordField, validators, \
|
||||
from wtforms import StringField, PasswordField, validators, \
|
||||
SubmitField, HiddenField, BooleanField, ValidationError, Field
|
||||
from flask_login import current_user
|
||||
from werkzeug.local import LocalProxy
|
||||
@@ -94,20 +94,20 @@ class Form(BaseForm):
|
||||
|
||||
|
||||
class EmailFormMixin():
|
||||
email = TextField(
|
||||
email = StringField(
|
||||
get_form_field_label('email'),
|
||||
validators=[email_required, email_validator])
|
||||
|
||||
|
||||
class UserEmailFormMixin():
|
||||
user = None
|
||||
email = TextField(
|
||||
email = StringField(
|
||||
get_form_field_label('email'),
|
||||
validators=[email_required, email_validator, valid_user_email])
|
||||
|
||||
|
||||
class UniqueEmailFormMixin():
|
||||
email = TextField(
|
||||
email = StringField(
|
||||
get_form_field_label('email'),
|
||||
validators=[email_required, email_validator, unique_user_email])
|
||||
|
||||
@@ -204,7 +204,7 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
|
||||
class LoginForm(Form, NextFormMixin):
|
||||
"""The default login form"""
|
||||
|
||||
email = TextField(get_form_field_label('email'))
|
||||
email = StringField(get_form_field_label('email'))
|
||||
password = PasswordField(get_form_field_label('password'))
|
||||
remember = BooleanField(get_form_field_label('remember_me'))
|
||||
submit = SubmitField(get_form_field_label('login'))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.passwordless
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.passwordless
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security passwordless module
|
||||
|
||||
@@ -35,8 +35,7 @@ def send_login_instructions(user):
|
||||
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
|
||||
'login_instructions', user=user, login_link=login_link)
|
||||
|
||||
login_instructions_sent.send(app._get_current_object(),
|
||||
user=user, login_token=token)
|
||||
login_instructions_sent.send(app._get_current_object(), user=user, login_token=token)
|
||||
|
||||
|
||||
def generate_login_token(user):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.recoverable
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.recoverable
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security recoverable module
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
from flask import current_app as app
|
||||
from werkzeug.local import LocalProxy
|
||||
from werkzeug.security import safe_str_cmp
|
||||
|
||||
from .signals import password_reset, reset_password_instructions_sent
|
||||
from .utils import send_mail, md5, encrypt_password, url_for_security, \
|
||||
@@ -35,8 +36,7 @@ def send_reset_password_instructions(user):
|
||||
'reset_instructions',
|
||||
user=user, reset_link=reset_link)
|
||||
|
||||
reset_password_instructions_sent.send(app._get_current_object(),
|
||||
user=user, token=token)
|
||||
reset_password_instructions_sent.send(app._get_current_object(), user=user, token=token)
|
||||
|
||||
|
||||
def send_password_reset_notice(user):
|
||||
@@ -54,7 +54,8 @@ def generate_reset_password_token(user):
|
||||
|
||||
:param user: The user to work with
|
||||
"""
|
||||
data = [str(user.id), md5(user.password)]
|
||||
password_hash = md5(user.password) if user.password else None
|
||||
data = [str(user.id), password_hash]
|
||||
return _security.reset_serializer.dumps(data)
|
||||
|
||||
|
||||
@@ -62,11 +63,19 @@ 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('...')
|
||||
expired, invalid, user, data = reset_password_token_status('...')
|
||||
|
||||
:param token: The password reset token
|
||||
"""
|
||||
return get_token_status(token, 'reset', 'RESET_PASSWORD')
|
||||
expired, invalid, user, data = get_token_status(token, 'reset', 'RESET_PASSWORD',
|
||||
return_data=True)
|
||||
if not invalid:
|
||||
if user.password:
|
||||
password_hash = md5(user.password)
|
||||
if not safe_str_cmp(password_hash, data[1]):
|
||||
invalid = True
|
||||
|
||||
return expired, invalid, user
|
||||
|
||||
|
||||
def update_password(user, password):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.registerable
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.registerable
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security registerable module
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.script
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.script
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security script module
|
||||
|
||||
@@ -18,7 +18,7 @@ except ImportError:
|
||||
import re
|
||||
|
||||
from flask import current_app
|
||||
from flask.ext.script import Command, Option
|
||||
from flask_script import Command, Option
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .utils import encrypt_password
|
||||
@@ -100,7 +100,7 @@ class AddRoleCommand(_RoleCommand):
|
||||
|
||||
|
||||
class RemoveRoleCommand(_RoleCommand):
|
||||
"""Add a role to a user"""
|
||||
"""Remove a role from a user"""
|
||||
|
||||
@commit
|
||||
def run(self, user_identifier, role_name):
|
||||
@@ -124,7 +124,7 @@ class DeactivateUserCommand(_ToggleActiveCommand):
|
||||
|
||||
|
||||
class ActivateUserCommand(_ToggleActiveCommand):
|
||||
"""Deactive a user"""
|
||||
"""Activate a user"""
|
||||
|
||||
@commit
|
||||
def run(self, user_identifier):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.signals
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.signals
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security signals module
|
||||
|
||||
|
||||
+22
-10
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.utils
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.utils
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security utils module
|
||||
|
||||
@@ -23,9 +23,9 @@ 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 flask_login import login_user as _login_user, logout_user as _logout_user
|
||||
from flask_mail import Message
|
||||
from flask_principal import Identity, AnonymousIdentity, identity_changed
|
||||
from itsdangerous import BadSignature, SignatureExpired
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
@@ -62,10 +62,10 @@ def login_user(user, remember=None):
|
||||
return False
|
||||
|
||||
if _security.trackable:
|
||||
if 'X-Forwarded-For' not in request.headers:
|
||||
remote_addr = request.remote_addr or 'untrackable'
|
||||
if 'X-Forwarded-For' in request.headers:
|
||||
remote_addr = request.headers.getlist("X-Forwarded-For")[0].rpartition(' ')[-1]
|
||||
else:
|
||||
remote_addr = request.headers.getlist("X-Forwarded-For")[0]
|
||||
remote_addr = request.remote_addr or 'untrackable'
|
||||
|
||||
old_current_login, new_current_login = user.current_login_at, datetime.utcnow()
|
||||
old_current_ip, new_current_ip = user.current_login_ip, remote_addr
|
||||
@@ -188,6 +188,14 @@ def get_url(endpoint_or_url):
|
||||
return endpoint_or_url
|
||||
|
||||
|
||||
def slash_url_suffix(url, suffix):
|
||||
"""Adds a slash either to the beginning or the end of a suffix
|
||||
(which is to be appended to a URL), depending on whether or not
|
||||
the URL ends with a slash."""
|
||||
|
||||
return url.endswith('/') and ('%s/' % suffix) or ('/%s' % suffix)
|
||||
|
||||
|
||||
def get_security_endpoint_name(endpoint):
|
||||
return '%s.%s' % (_security.blueprint_name, endpoint)
|
||||
|
||||
@@ -333,7 +341,7 @@ def send_mail(subject, recipient, template, **context):
|
||||
mail.send(msg)
|
||||
|
||||
|
||||
def get_token_status(token, serializer, max_age=None):
|
||||
def get_token_status(token, serializer, max_age=None, return_data=False):
|
||||
"""Get the status of a token.
|
||||
|
||||
:param token: The token to check
|
||||
@@ -359,7 +367,11 @@ def get_token_status(token, serializer, max_age=None):
|
||||
user = _datastore.find_user(id=data[0])
|
||||
|
||||
expired = expired and (user is not None)
|
||||
return expired, invalid, user
|
||||
|
||||
if return_data:
|
||||
return expired, invalid, user, data
|
||||
else:
|
||||
return expired, invalid, user
|
||||
|
||||
|
||||
def get_identity_attributes(app=None):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ext.security.views
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
flask_security.views
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Flask-Security views module
|
||||
|
||||
@@ -26,7 +26,7 @@ from .changeable import change_user_password
|
||||
from .registerable import register_user
|
||||
from .utils import config_value, do_flash, get_url, get_post_login_redirect, \
|
||||
get_post_register_redirect, get_message, login_user, logout_user, \
|
||||
url_for_security as url_for
|
||||
url_for_security as url_for, slash_url_suffix
|
||||
|
||||
# Convenient references
|
||||
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||
@@ -238,6 +238,7 @@ def confirm_email(token):
|
||||
get_url(_security.post_login_view))
|
||||
|
||||
|
||||
@anonymous_user_required
|
||||
def forgot_password():
|
||||
"""View function that handles a forgotten password request."""
|
||||
|
||||
@@ -333,7 +334,7 @@ def create_blueprint(state, import_name):
|
||||
bp.route(state.login_url,
|
||||
methods=['GET', 'POST'],
|
||||
endpoint='login')(send_login)
|
||||
bp.route(state.login_url + '/<token>',
|
||||
bp.route(state.login_url + slash_url_suffix(state.login_url, '<token>'),
|
||||
endpoint='token_login')(token_login)
|
||||
else:
|
||||
bp.route(state.login_url,
|
||||
@@ -349,7 +350,7 @@ def create_blueprint(state, import_name):
|
||||
bp.route(state.reset_url,
|
||||
methods=['GET', 'POST'],
|
||||
endpoint='forgot_password')(forgot_password)
|
||||
bp.route(state.reset_url + '/<token>',
|
||||
bp.route(state.reset_url + slash_url_suffix(state.reset_url, '<token>'),
|
||||
methods=['GET', 'POST'],
|
||||
endpoint='reset_password')(reset_password)
|
||||
|
||||
@@ -362,7 +363,7 @@ def create_blueprint(state, import_name):
|
||||
bp.route(state.confirm_url,
|
||||
methods=['GET', 'POST'],
|
||||
endpoint='send_confirmation')(send_confirmation)
|
||||
bp.route(state.confirm_url + '/<token>',
|
||||
bp.route(state.confirm_url + slash_url_suffix(state.confirm_url, '<token>'),
|
||||
methods=['GET', 'POST'],
|
||||
endpoint='confirm_email')(confirm_email)
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
Flask-SQLAlchemy>=1.0
|
||||
bcrypt>=1.0.2
|
||||
bcrypt>=1.0.2,<2.0.0
|
||||
flask-mongoengine>=0.7.0
|
||||
flask-peewee>=0.6.5
|
||||
pymongo==2.8
|
||||
pytest>=2.5.2
|
||||
pytest-cache>=1.0
|
||||
pytest-cov>=1.6
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from flask import Flask
|
||||
from flask_security.core import UserMixin
|
||||
from flask_security.signals import password_changed
|
||||
|
||||
from utils import authenticate
|
||||
@@ -20,6 +22,8 @@ def test_recoverable_flag(app, client, get_message):
|
||||
|
||||
@password_changed.connect_via(app)
|
||||
def on_password_changed(app, user):
|
||||
assert isinstance(app, Flask)
|
||||
assert isinstance(user, UserMixin)
|
||||
recorded.append(user)
|
||||
|
||||
authenticate(client)
|
||||
|
||||
@@ -226,6 +226,19 @@ def test_multi_auth_basic(client):
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_multi_auth_basic_invalid(client):
|
||||
response = client.get('/multi_auth', headers={
|
||||
'Authorization': 'Basic %s' % base64.b64encode(b"bogus:bogus").decode('utf-8')
|
||||
})
|
||||
assert b'<h1>Unauthorized</h1>' in response.data
|
||||
assert 'WWW-Authenticate' in response.headers
|
||||
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
|
||||
|
||||
response = client.get('/multi_auth')
|
||||
print(response.headers)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_multi_auth_token(client):
|
||||
response = json_authenticate(client)
|
||||
token = response.jdata['response']['user']['authentication_token']
|
||||
|
||||
@@ -10,6 +10,8 @@ import time
|
||||
|
||||
import pytest
|
||||
|
||||
from flask import Flask
|
||||
from flask_security.core import UserMixin
|
||||
from flask_security.signals import user_confirmed, confirm_instructions_sent
|
||||
from flask_security.utils import capture_registrations
|
||||
|
||||
@@ -25,10 +27,14 @@ def test_confirmable_flag(app, client, sqlalchemy_datastore, get_message):
|
||||
|
||||
@user_confirmed.connect_via(app)
|
||||
def on_confirmed(app, user):
|
||||
assert isinstance(app, Flask)
|
||||
assert isinstance(user, UserMixin)
|
||||
recorded_confirms.append(user)
|
||||
|
||||
@confirm_instructions_sent.connect_via(app)
|
||||
def on_instructions_sent(app, user):
|
||||
assert isinstance(app, Flask)
|
||||
assert isinstance(user, UserMixin)
|
||||
recorded_instructions_sent.append(user)
|
||||
|
||||
# Test login before confirmation
|
||||
|
||||
@@ -81,6 +81,8 @@ def test_context_processors(client, app):
|
||||
def mail():
|
||||
return {'foo': 'bar'}
|
||||
|
||||
client.get('/logout')
|
||||
|
||||
with app.mail.record_messages() as outbox:
|
||||
client.post('/reset', data=dict(email='matt@lp.com'))
|
||||
|
||||
|
||||
+26
-9
@@ -11,7 +11,8 @@ import pytest
|
||||
from flask_security import Security
|
||||
from flask_security.forms import LoginForm, RegisterForm, ConfirmRegisterForm, \
|
||||
SendConfirmationForm, PasswordlessLoginForm, ForgotPasswordForm, ResetPasswordForm, \
|
||||
ChangePasswordForm, TextField, PasswordField, email_required, email_validator, valid_user_email
|
||||
ChangePasswordForm, StringField, PasswordField, email_required, email_validator, \
|
||||
valid_user_email
|
||||
from flask_security.utils import capture_reset_password_requests, md5, string_types
|
||||
|
||||
from utils import authenticate, init_app_with_options, populate_data
|
||||
@@ -41,17 +42,17 @@ def test_register_blueprint_flag(app, sqlalchemy_datastore):
|
||||
@pytest.mark.changeable()
|
||||
def test_basic_custom_forms(app, sqlalchemy_datastore):
|
||||
class MyLoginForm(LoginForm):
|
||||
email = TextField('My Login Email Address Field')
|
||||
email = StringField('My Login Email Address Field')
|
||||
|
||||
class MyRegisterForm(RegisterForm):
|
||||
email = TextField('My Register Email Address Field')
|
||||
email = StringField('My Register Email Address Field')
|
||||
|
||||
class MyForgotPasswordForm(ForgotPasswordForm):
|
||||
email = TextField('My Forgot Email Address Field',
|
||||
validators=[email_required, email_validator, valid_user_email])
|
||||
email = StringField('My Forgot Email Address Field',
|
||||
validators=[email_required, email_validator, valid_user_email])
|
||||
|
||||
class MyResetPasswordForm(ResetPasswordForm):
|
||||
password = TextField('My Reset Password Field')
|
||||
password = StringField('My Reset Password Field')
|
||||
|
||||
class MyChangePasswordForm(ChangePasswordForm):
|
||||
password = PasswordField('My Change Password Field')
|
||||
@@ -96,10 +97,10 @@ def test_confirmable_custom_form(app, sqlalchemy_datastore):
|
||||
app.config['SECURITY_CONFIRMABLE'] = True
|
||||
|
||||
class MyRegisterForm(ConfirmRegisterForm):
|
||||
email = TextField('My Register Email Address Field')
|
||||
email = StringField('My Register Email Address Field')
|
||||
|
||||
class MySendConfirmationForm(SendConfirmationForm):
|
||||
email = TextField('My Send Confirmation Email Address Field')
|
||||
email = StringField('My Send Confirmation Email Address Field')
|
||||
|
||||
app.security = Security(app,
|
||||
datastore=sqlalchemy_datastore,
|
||||
@@ -119,7 +120,7 @@ def test_passwordless_custom_form(app, sqlalchemy_datastore):
|
||||
app.config['SECURITY_PASSWORDLESS'] = True
|
||||
|
||||
class MyPasswordlessLoginForm(PasswordlessLoginForm):
|
||||
email = TextField('My Passwordless Email Address Field')
|
||||
email = StringField('My Passwordless Email Address Field')
|
||||
|
||||
app.security = Security(app,
|
||||
datastore=sqlalchemy_datastore,
|
||||
@@ -191,3 +192,19 @@ def test_password_unicode_password_salt(client):
|
||||
assert response.status_code == 302
|
||||
response = authenticate(client, follow_redirects=True)
|
||||
assert b'Hello matt@lp.com' in response.data
|
||||
|
||||
|
||||
def test_set_unauthorized_handler(app, client):
|
||||
@app.security.unauthorized_handler
|
||||
def unauthorized():
|
||||
app.unauthorized_handler_set = True
|
||||
return 'unauthorized-handler-set', 401
|
||||
|
||||
app.unauthorized_handler_set = False
|
||||
|
||||
authenticate(client, "joe@lp.com")
|
||||
response = client.get("/admin", follow_redirects=True)
|
||||
|
||||
assert app.unauthorized_handler_set is True
|
||||
assert b'unauthorized-handler-set' in response.data
|
||||
assert response.status_code == 401
|
||||
|
||||
@@ -10,8 +10,10 @@ import time
|
||||
|
||||
import pytest
|
||||
|
||||
from flask import Flask
|
||||
from flask_security.core import UserMixin
|
||||
from flask_security.signals import login_instructions_sent
|
||||
from flask_security.utils import capture_passwordless_login_requests
|
||||
from flask_security.utils import capture_passwordless_login_requests, string_types
|
||||
|
||||
from utils import logout
|
||||
|
||||
@@ -23,6 +25,9 @@ def test_trackable_flag(app, client, get_message):
|
||||
|
||||
@login_instructions_sent.connect_via(app)
|
||||
def on_instructions_sent(app, user, login_token):
|
||||
assert isinstance(app, Flask)
|
||||
assert isinstance(user, UserMixin)
|
||||
assert isinstance(login_token, string_types)
|
||||
recorded.append(user)
|
||||
|
||||
# Test disabled account
|
||||
|
||||
@@ -10,8 +10,10 @@ import time
|
||||
|
||||
import pytest
|
||||
|
||||
from flask import Flask
|
||||
from flask_security.core import UserMixin
|
||||
from flask_security.signals import reset_password_instructions_sent, password_reset
|
||||
from flask_security.utils import capture_reset_password_requests
|
||||
from flask_security.utils import capture_reset_password_requests, string_types
|
||||
|
||||
from utils import authenticate, logout
|
||||
|
||||
@@ -28,6 +30,9 @@ def test_recoverable_flag(app, client, get_message):
|
||||
|
||||
@reset_password_instructions_sent.connect_via(app)
|
||||
def on_instructions_sent(app, user, token):
|
||||
assert isinstance(app, Flask)
|
||||
assert isinstance(user, UserMixin)
|
||||
assert isinstance(token, string_types)
|
||||
recorded_instructions_sent.append(user)
|
||||
|
||||
# Test the reset view
|
||||
@@ -117,6 +122,47 @@ def test_expired_reset_token(client, get_message):
|
||||
assert msg in response.data
|
||||
|
||||
|
||||
def test_used_reset_token(client, get_message):
|
||||
with capture_reset_password_requests() as requests:
|
||||
client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
|
||||
|
||||
token = requests[0]['token']
|
||||
|
||||
# use the token
|
||||
response = client.post('/reset/' + token, data={
|
||||
'password': 'newpassword',
|
||||
'password_confirm': 'newpassword'
|
||||
}, follow_redirects=True)
|
||||
|
||||
assert get_message('PASSWORD_RESET') in response.data
|
||||
|
||||
logout(client)
|
||||
|
||||
# attempt to use it a second time
|
||||
response2 = client.post('/reset/' + token, data={
|
||||
'password': 'otherpassword',
|
||||
'password_confirm': 'otherpassword'
|
||||
}, follow_redirects=True)
|
||||
|
||||
msg = get_message('INVALID_RESET_PASSWORD_TOKEN')
|
||||
assert msg in response2.data
|
||||
|
||||
|
||||
def test_reset_passwordless_user(client, get_message):
|
||||
with capture_reset_password_requests() as requests:
|
||||
client.post('/reset', data=dict(email='jess@lp.com'), follow_redirects=True)
|
||||
|
||||
token = requests[0]['token']
|
||||
|
||||
# use the token
|
||||
response = client.post('/reset/' + token, data={
|
||||
'password': 'newpassword',
|
||||
'password_confirm': 'newpassword'
|
||||
}, follow_redirects=True)
|
||||
|
||||
assert get_message('PASSWORD_RESET') in response.data
|
||||
|
||||
|
||||
@pytest.mark.settings(reset_url='/custom_reset')
|
||||
def test_custom_reset_url(client):
|
||||
response = client.get('/custom_reset')
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from flask import Flask
|
||||
from flask_security.core import UserMixin
|
||||
from flask_security.signals import user_registered
|
||||
|
||||
from utils import authenticate, logout
|
||||
@@ -26,6 +28,9 @@ def test_registerable_flag(client, app, get_message):
|
||||
# Test registering is successful, sends email, and fires signal
|
||||
@user_registered.connect_via(app)
|
||||
def on_user_registerd(app, user, confirm_token):
|
||||
assert isinstance(app, Flask)
|
||||
assert isinstance(user, UserMixin)
|
||||
assert confirm_token is None
|
||||
recorded.append(user)
|
||||
|
||||
data = dict(
|
||||
|
||||
@@ -26,3 +26,19 @@ def test_trackable_flag(app, client):
|
||||
assert user.last_login_ip == 'untrackable'
|
||||
assert user.current_login_ip == '127.0.0.1'
|
||||
assert user.login_count == 2
|
||||
|
||||
|
||||
def test_trackable_with_multiple_ips_in_headers(app, client):
|
||||
e = 'matt@lp.com'
|
||||
authenticate(client, email=e)
|
||||
logout(client)
|
||||
authenticate(client, email=e, headers={
|
||||
'X-Forwarded-For': '99.99.99.99, 88.88.88.88'})
|
||||
|
||||
with app.app_context():
|
||||
user = app.security.datastore.find_user(email=e)
|
||||
assert user.last_login_at is not None
|
||||
assert user.current_login_at is not None
|
||||
assert user.last_login_ip == 'untrackable'
|
||||
assert user.current_login_ip == '88.88.88.88'
|
||||
assert user.login_count == 2
|
||||
|
||||
Reference in New Issue
Block a user