mirror of
https://github.com/wassname/flask-security.git
synced 2026-08-07 11:22:21 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f46f35981 | ||
|
|
c0d9eecf10 | ||
|
|
04bb2c4041 | ||
|
|
8eeb832d2e | ||
|
|
8f760aadbd | ||
|
|
03d27cd600 | ||
|
|
d30a27b3bb | ||
|
|
78903fa2e5 | ||
|
|
514de64303 | ||
|
|
bad63265f8 | ||
|
|
66a9dcd2e6 | ||
|
|
df1647f1f9 | ||
|
|
89ecded480 | ||
|
|
2b35b37a66 | ||
|
|
4d8a813004 | ||
|
|
06312ef50f | ||
|
|
c3ad5b2fa6 | ||
|
|
637fc913cd | ||
|
|
e00522f331 | ||
|
|
3d34d87a97 | ||
|
|
461ace9303 | ||
|
|
d19bb98abd | ||
|
|
c24af5ca6e | ||
|
|
26045fc4dc | ||
|
|
bf260d4b7e | ||
|
|
db56ff74a9 | ||
|
|
e03efe0b34 | ||
|
|
c587988a3a | ||
|
|
f2d5245bd8 | ||
|
|
97e1960abd | ||
|
|
e749b77ca7 | ||
|
|
6f3c163ee7 | ||
|
|
3b81ec57ea | ||
|
|
38874433c7 | ||
|
|
4eda3e756f | ||
|
|
4815b1afed | ||
|
|
99ac732d10 | ||
|
|
e8b0c62818 | ||
|
|
1108f1670c | ||
|
|
3575a2df18 | ||
|
|
c84c485493 | ||
|
|
8298ac461e | ||
|
|
105d04768e | ||
|
|
f1cca43d9c | ||
|
|
e8352fa265 | ||
|
|
37d84ddd73 | ||
|
|
6f9869e9c2 | ||
|
|
abc061ba46 | ||
|
|
95c80e5677 |
+4
-3
@@ -3,11 +3,12 @@ language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "pypy"
|
||||
|
||||
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-Peewee Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors
|
||||
- pip install . --quiet
|
||||
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib --quiet --use-mirrors; fi"
|
||||
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee py-bcrypt MySQL-python --quiet
|
||||
|
||||
before_script:
|
||||
- mysql -e 'create database flask_security_test;'
|
||||
|
||||
@@ -3,6 +3,64 @@ Flask-Security Changelog
|
||||
|
||||
Here you can see the full list of changes between each Flask-Security release.
|
||||
|
||||
Version 1.6.7
|
||||
-------------
|
||||
|
||||
Released July 11th 2013
|
||||
|
||||
- Made password length form error message configurable
|
||||
- Fixed email confirmation bug that prevented logged in users from confirming their email
|
||||
|
||||
|
||||
Version 1.6.6
|
||||
-------------
|
||||
|
||||
Released June 28th 2013
|
||||
|
||||
- Fixed dependency versions
|
||||
|
||||
|
||||
Version 1.6.5
|
||||
-------------
|
||||
|
||||
Released June 20th 2013
|
||||
|
||||
- Fixed bug in `flask.ext.security.confirmable.generate_confirmation_link`
|
||||
|
||||
|
||||
Version 1.6.4
|
||||
-------------
|
||||
|
||||
Released June 18th 2013
|
||||
|
||||
- Added `SECURITY_DEFAULT_REMEMBER_ME` configuration value to unify behavior between endpoints
|
||||
- Fixed Flask-Login dependency problem
|
||||
- Added optional `next` parameter to registration endpoint, similar to that of login
|
||||
|
||||
|
||||
Version 1.6.3
|
||||
-------------
|
||||
|
||||
Released May 8th 2013
|
||||
|
||||
- Fixed bug in regards to imports with latest version of MongoEngine
|
||||
|
||||
|
||||
Version 1.6.2
|
||||
-------------
|
||||
|
||||
Released April 4th 2013
|
||||
|
||||
- Fixed bug with http basic auth
|
||||
|
||||
|
||||
Version 1.6.1
|
||||
-------------
|
||||
|
||||
Released April 3rd 2013
|
||||
|
||||
- Fixed bug with signals
|
||||
|
||||
|
||||
Version 1.6.0
|
||||
-------------
|
||||
@@ -93,10 +151,10 @@ Version 1.2.0
|
||||
|
||||
Released March 12th 2012
|
||||
|
||||
- Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a
|
||||
- Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a
|
||||
boolean value to specify if Flask-Security should flash messages or not.
|
||||
|
||||
|
||||
Version 1.1.0
|
||||
-------------
|
||||
|
||||
Initial release
|
||||
Initial release
|
||||
|
||||
Vendored
+2
-2
@@ -9,9 +9,9 @@
|
||||
<ul>
|
||||
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
|
||||
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
|
||||
<li><a href="http://github.com/jfinkels/flask-security/issues">Issue Tracker</a></li>
|
||||
<li><a href="http://github.com/mattupstate/flask-security/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="http://pypi.python.org/pypi/Flask-Social">Flask-Social</a></li>
|
||||
<li><a href="http://github.com/mattupstate/flask-social">Flask-Social @ github</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ copyright = u'2012, Matt Wright'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.6.0'
|
||||
version = '1.6.7'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
|
||||
@@ -156,6 +156,10 @@ Feature Flags
|
||||
sent an email with a login link. This feature is
|
||||
experimental and should be used with caution. Defaults
|
||||
to ``False``.
|
||||
``SECURITY_CHANGEABLE`` Specifies if Flask-Security should enable the
|
||||
change password endpoint. The URL for this endpoint is
|
||||
specified by the ``SECURITY_CHANGE_URL`` configuration
|
||||
option. Defaults to ``False``.
|
||||
========================= ======================================================
|
||||
|
||||
Email
|
||||
@@ -220,4 +224,7 @@ Miscellaneous
|
||||
remember tokens. Remember tokens are
|
||||
used instead of user ID's as it is more
|
||||
secure. Defaults to ``remember-salt``.
|
||||
``SECURITY_DEFAULT_REMEMBER_ME`` Specifies the default "remember me"
|
||||
value used when logging in a user.
|
||||
Defaults to ``False``.
|
||||
======================================= ========================================
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ and libraries. They include:
|
||||
2. `Flask-Mail <http://packages.python.org/Flask-Mail/>`_
|
||||
3. `Flask-Principal <http://packages.python.org/Flask-Principal/>`_
|
||||
4. `Flask-Script <http://packages.python.org/Flask-Script/>`_
|
||||
5. `Flask-WTF <http://packages.python.org/Flask-Mail/>`_
|
||||
5. `Flask-WTF <http://packages.python.org/Flask-WTF/>`_
|
||||
6. `itsdangerous <http://packages.python.org/itsdangerous/>`_
|
||||
7. `passlib <http://packages.python.org/passlib/>`_
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__version__ = '1.6.0'
|
||||
__version__ = '1.6.7'
|
||||
|
||||
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
|
||||
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
|
||||
from .decorators import auth_token_required, http_auth_required, \
|
||||
login_required, roles_accepted, roles_required
|
||||
login_required, roles_accepted, roles_required, auth_required
|
||||
from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
|
||||
ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
|
||||
from .signals import confirm_instructions_sent, password_reset, \
|
||||
|
||||
@@ -14,8 +14,7 @@ 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
|
||||
from .utils import send_mail, encrypt_password, config_value
|
||||
|
||||
|
||||
# Convenient references
|
||||
|
||||
@@ -15,7 +15,7 @@ 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,\
|
||||
config_value
|
||||
config_value
|
||||
from .signals import user_confirmed, confirm_instructions_sent
|
||||
|
||||
|
||||
@@ -27,8 +27,7 @@ _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
|
||||
return url_for_security('confirm_email', token=token, _external=True), token
|
||||
|
||||
|
||||
def send_confirmation_instructions(user):
|
||||
@@ -44,7 +43,7 @@ def send_confirmation_instructions(user):
|
||||
'confirmation_instructions', user=user,
|
||||
confirmation_link=confirmation_link)
|
||||
|
||||
confirm_instructions_sent.send(user, app=app._get_current_object())
|
||||
confirm_instructions_sent.send(app._get_current_object(), user=user)
|
||||
return token
|
||||
|
||||
|
||||
@@ -80,4 +79,4 @@ def confirm_user(user):
|
||||
"""
|
||||
user.confirmed_at = datetime.utcnow()
|
||||
_datastore.put(user)
|
||||
user_confirmed.send(user, app=app._get_current_object())
|
||||
user_confirmed.send(app._get_current_object(), user=user)
|
||||
|
||||
+22
-14
@@ -10,10 +10,10 @@
|
||||
"""
|
||||
|
||||
from flask import current_app
|
||||
from flask.ext.login import AnonymousUser as AnonymousUserBase, \
|
||||
UserMixin as BaseUserMixin, LoginManager, current_user
|
||||
from flask.ext.login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
|
||||
LoginManager, current_user
|
||||
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \
|
||||
identity_loaded
|
||||
identity_loaded
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from passlib.context import CryptContext
|
||||
from werkzeug.datastructures import ImmutableList
|
||||
@@ -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, ChangePasswordForm, ResetPasswordForm, \
|
||||
SendConfirmationForm, PasswordlessLoginForm
|
||||
ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \
|
||||
SendConfirmationForm, PasswordlessLoginForm
|
||||
|
||||
# Convenient references
|
||||
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||
@@ -76,6 +76,7 @@ _default_config = {
|
||||
'LOGIN_SALT': 'login-salt',
|
||||
'CHANGE_SALT': 'change-salt',
|
||||
'REMEMBER_SALT': 'remember-salt',
|
||||
'DEFAULT_REMEMBER_ME': False,
|
||||
'DEFAULT_HTTP_AUTH_REALM': 'Login Required',
|
||||
'EMAIL_SUBJECT_REGISTER': 'Welcome',
|
||||
'EMAIL_SUBJECT_CONFIRM': 'Please confirm your email',
|
||||
@@ -109,6 +110,7 @@ _default_messages = {
|
||||
'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'),
|
||||
'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'),
|
||||
'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'),
|
||||
'PASSWORD_INVALID_LENGTH': ('Password must be at least 6 characters', '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'),
|
||||
@@ -153,8 +155,7 @@ def _token_loader(token):
|
||||
return user
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
def _identity_loader():
|
||||
@@ -179,8 +180,14 @@ def _get_login_manager(app):
|
||||
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
|
||||
lm.user_loader(_user_loader)
|
||||
lm.token_loader(_token_loader)
|
||||
lm.login_message, lm.login_message_category = cv('MSG_LOGIN', app=app)
|
||||
lm.needs_refresh_message, lm.needs_refresh_message_category = cv('MSG_REFRESH', app=app)
|
||||
|
||||
if cv('FLASH_MESSAGES', app=app):
|
||||
lm.login_message, lm.login_message_category = cv('MSG_LOGIN', app=app)
|
||||
lm.needs_refresh_message, lm.needs_refresh_message_category = cv('MSG_REFRESH', app=app)
|
||||
else:
|
||||
lm.login_message = None
|
||||
lm.needs_refresh_message = None
|
||||
|
||||
lm.init_app(app)
|
||||
return lm
|
||||
|
||||
@@ -241,8 +248,7 @@ class RoleMixin(object):
|
||||
self.name == getattr(other, 'name', None))
|
||||
|
||||
def __ne__(self, other):
|
||||
return (self.name != other and
|
||||
self.name != getattr(other, 'name', None))
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
class UserMixin(BaseUserMixin):
|
||||
@@ -261,14 +267,16 @@ class UserMixin(BaseUserMixin):
|
||||
"""Returns `True` if the user identifies with the specified role.
|
||||
|
||||
:param role: A role name or `Role` instance"""
|
||||
return role in self.roles
|
||||
if isinstance(role, basestring):
|
||||
return role in (role.name for role in self.roles)
|
||||
else:
|
||||
return role in self.roles
|
||||
|
||||
|
||||
class AnonymousUser(AnonymousUserBase):
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
"""AnonymousUser definition"""
|
||||
|
||||
def __init__(self):
|
||||
super(AnonymousUser, self).__init__()
|
||||
self.roles = ImmutableList()
|
||||
|
||||
def has_role(self, *args):
|
||||
|
||||
+20
-14
@@ -95,12 +95,11 @@ class UserDatastore(object):
|
||||
: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
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_role_from_user(self, user, role):
|
||||
"""Removes a role from a user
|
||||
@@ -191,7 +190,14 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
|
||||
UserDatastore.__init__(self, user_model, role_model)
|
||||
|
||||
def find_user(self, **kwargs):
|
||||
return self.user_model.objects(**kwargs).first()
|
||||
try:
|
||||
from mongoengine.queryset import Q, QCombination
|
||||
except ImportError:
|
||||
from mongoengine.queryset.visitor import Q, QCombination
|
||||
|
||||
queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items())
|
||||
query = QCombination(QCombination.AND, queries)
|
||||
return self.user_model.objects(query).first()
|
||||
|
||||
def find_role(self, role):
|
||||
return self.role_model.objects(name=role).first()
|
||||
@@ -217,10 +223,7 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
||||
return None
|
||||
|
||||
def find_role(self, role):
|
||||
try:
|
||||
return self.role_model.filter(name=role).get()
|
||||
except self.role_model.DoesNotExist:
|
||||
return None
|
||||
return self.role_model.filter(name=role).get()
|
||||
|
||||
def create_user(self, **kwargs):
|
||||
"""Creates and returns a new user from the given parameters."""
|
||||
@@ -231,7 +234,6 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
||||
self.add_role_to_user(user, role)
|
||||
return user
|
||||
|
||||
|
||||
def add_role_to_user(self, user, role):
|
||||
"""Adds a role tp a user
|
||||
|
||||
@@ -239,10 +241,12 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
||||
: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():
|
||||
result = self.UserRole.select() \
|
||||
.where(self.UserRole.user == user.id, self.UserRole.role == role.id)
|
||||
if result.count():
|
||||
return False
|
||||
else:
|
||||
self.UserRole.create(user=user, role=role)
|
||||
self.UserRole.create(user=user.id, role=role.id)
|
||||
return True
|
||||
|
||||
def remove_role_from_user(self, user, role):
|
||||
@@ -252,9 +256,11 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
||||
: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)
|
||||
result = self.UserRole.select() \
|
||||
.where(self.UserRole.user == user, self.UserRole.role == role)
|
||||
if result.count():
|
||||
self.UserRole.delete().where(
|
||||
self.UserRole.user == user, self.UserRole.role == role)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
from functools import wraps
|
||||
|
||||
from flask import current_app, Response, request, redirect, _request_ctx_stack
|
||||
@@ -30,6 +31,8 @@ _default_unauthorized_html = """
|
||||
or your browser doesn't understand how to supply the credentials required.</p>
|
||||
"""
|
||||
|
||||
BasicAuth = namedtuple('BasicAuth', 'username, password')
|
||||
|
||||
|
||||
def _get_unauthorized_response(text=None, headers=None):
|
||||
text = text or _default_unauthorized_html
|
||||
@@ -67,7 +70,7 @@ def _check_token():
|
||||
|
||||
|
||||
def _check_http_auth():
|
||||
auth = request.authorization or dict(username=None, password=None)
|
||||
auth = request.authorization or BasicAuth(username=None, password=None)
|
||||
user = _security.datastore.find_user(email=auth.username)
|
||||
|
||||
if user and utils.verify_and_update_password(auth.password, user):
|
||||
|
||||
@@ -16,7 +16,7 @@ import flask_wtf as wtf
|
||||
|
||||
from flask import request, current_app
|
||||
from flask_wtf import Form as BaseForm, TextField, PasswordField, \
|
||||
SubmitField, HiddenField, BooleanField, ValidationError, Field
|
||||
SubmitField, HiddenField, BooleanField, ValidationError, Field
|
||||
from flask_login import current_user
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
@@ -69,6 +69,7 @@ class Length(ValidatorMixin, wtf.Length):
|
||||
email_required = Required(message='EMAIL_NOT_PROVIDED')
|
||||
email_validator = Email(message='INVALID_EMAIL_ADDRESS')
|
||||
password_required = Required(message='PASSWORD_NOT_PROVIDED')
|
||||
password_length = Length(min=6, max=128, message='PASSWORD_INVALID_LENGTH')
|
||||
|
||||
|
||||
def get_form_field_label(key):
|
||||
@@ -122,8 +123,7 @@ class PasswordFormMixin():
|
||||
|
||||
class NewPasswordFormMixin():
|
||||
password = PasswordField(get_form_field_label('password'),
|
||||
validators=[password_required,
|
||||
Length(min=6, max=128)])
|
||||
validators=[password_required, password_length])
|
||||
|
||||
|
||||
class PasswordConfirmFormMixin():
|
||||
@@ -256,8 +256,7 @@ class ChangePasswordForm(Form, PasswordFormMixin):
|
||||
"""The default change password form"""
|
||||
|
||||
new_password = PasswordField(get_form_field_label('new_password'),
|
||||
validators=[password_required,
|
||||
Length(min=6, max=128)])
|
||||
validators=[password_required, password_length])
|
||||
|
||||
new_password_confirm = PasswordField(get_form_field_label('retype_password'),
|
||||
validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
|
||||
|
||||
@@ -14,7 +14,7 @@ from werkzeug.local import LocalProxy
|
||||
|
||||
from .signals import login_instructions_sent
|
||||
from .utils import send_mail, url_for_security, get_token_status, \
|
||||
config_value
|
||||
config_value
|
||||
|
||||
|
||||
# Convenient references
|
||||
@@ -30,14 +30,13 @@ def send_login_instructions(user):
|
||||
: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
|
||||
login_link = url_for_security('token_login', token=token, _external=True)
|
||||
|
||||
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), 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())
|
||||
login_instructions_sent.send(app._get_current_object(),
|
||||
user=user, login_token=token)
|
||||
|
||||
|
||||
def generate_login_token(user):
|
||||
|
||||
@@ -14,7 +14,7 @@ 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, config_value
|
||||
get_token_status, config_value
|
||||
|
||||
|
||||
# Convenient references
|
||||
@@ -29,15 +29,14 @@ def send_reset_password_instructions(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
|
||||
reset_link = url_for_security('reset_password', token=token, _external=True)
|
||||
|
||||
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), 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())
|
||||
reset_password_instructions_sent.send(app._get_current_object(),
|
||||
user=user, token=token)
|
||||
|
||||
|
||||
def send_password_reset_notice(user):
|
||||
@@ -77,4 +76,4 @@ def update_password(user, password):
|
||||
user.password = encrypt_password(password)
|
||||
_datastore.put(user)
|
||||
send_password_reset_notice(user)
|
||||
password_reset.send(user, app=app._get_current_object())
|
||||
password_reset.send(app._get_current_object(), user=user)
|
||||
|
||||
@@ -15,7 +15,7 @@ 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, \
|
||||
config_value
|
||||
config_value
|
||||
|
||||
# Convenient references
|
||||
_security = LocalProxy(lambda: app.extensions['security'])
|
||||
@@ -33,8 +33,8 @@ def register_user(**kwargs):
|
||||
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())
|
||||
user_registered.send(app._get_current_object(),
|
||||
user=user, confirm_token=token)
|
||||
|
||||
if config_value('SEND_REGISTER_EMAIL'):
|
||||
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
|
||||
|
||||
+20
-9
@@ -14,12 +14,13 @@ import blinker
|
||||
import functools
|
||||
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
|
||||
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
|
||||
@@ -37,9 +38,12 @@ _datastore = LocalProxy(lambda: _security.datastore)
|
||||
_pwd_context = LocalProxy(lambda: _security.pwd_context)
|
||||
|
||||
|
||||
def login_user(user, remember=True):
|
||||
def login_user(user, remember=None):
|
||||
"""Performs the login and sends the appropriate signal."""
|
||||
|
||||
if remember is None:
|
||||
remember = config_value('DEFAULT_REMEMBER_ME')
|
||||
|
||||
if not _login_user(user, remember):
|
||||
return False
|
||||
|
||||
@@ -144,11 +148,18 @@ def url_for_security(endpoint, **values):
|
||||
return url_for(endpoint, **values)
|
||||
|
||||
|
||||
def get_post_login_redirect():
|
||||
"""Returns the URL to redirect to after a user logs in successfully."""
|
||||
def get_post_action_redirect(config_key):
|
||||
return (get_url(request.args.get('next')) or
|
||||
get_url(request.form.get('next')) or
|
||||
find_redirect('SECURITY_POST_LOGIN_VIEW'))
|
||||
find_redirect(config_key))
|
||||
|
||||
|
||||
def get_post_login_redirect():
|
||||
return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW')
|
||||
|
||||
|
||||
def get_post_register_redirect():
|
||||
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW')
|
||||
|
||||
|
||||
def find_redirect(key):
|
||||
@@ -273,7 +284,7 @@ def get_token_status(token, serializer, max_age=None):
|
||||
def capture_passwordless_login_requests():
|
||||
login_requests = []
|
||||
|
||||
def _on(data, app):
|
||||
def _on(app, **data):
|
||||
login_requests.append(data)
|
||||
|
||||
login_instructions_sent.connect(_on)
|
||||
@@ -293,7 +304,7 @@ def capture_registrations():
|
||||
"""
|
||||
registrations = []
|
||||
|
||||
def _on(data, app):
|
||||
def _on(app, **data):
|
||||
registrations.append(data)
|
||||
|
||||
user_registered.connect(_on)
|
||||
@@ -313,8 +324,8 @@ def capture_reset_password_requests(reset_password_sent_at=None):
|
||||
"""
|
||||
reset_requests = []
|
||||
|
||||
def _on(request, app):
|
||||
reset_requests.append(request)
|
||||
def _on(app, **data):
|
||||
reset_requests.append(data)
|
||||
|
||||
reset_password_instructions_sent.connect(_on)
|
||||
|
||||
|
||||
+14
-15
@@ -10,24 +10,23 @@
|
||||
"""
|
||||
|
||||
from flask import current_app, redirect, request, render_template, jsonify, \
|
||||
after_this_request, Blueprint
|
||||
after_this_request, Blueprint
|
||||
from flask_login import current_user
|
||||
from werkzeug.datastructures import MultiDict
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .confirmable import send_confirmation_instructions, \
|
||||
confirm_user, confirm_email_token_status
|
||||
confirm_user, confirm_email_token_status
|
||||
from .decorators import login_required, anonymous_user_required
|
||||
from .passwordless import send_login_instructions, \
|
||||
login_token_status
|
||||
login_token_status
|
||||
from .recoverable import reset_password_token_status, \
|
||||
send_reset_password_instructions, update_password
|
||||
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, \
|
||||
config_value
|
||||
|
||||
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
|
||||
|
||||
# Convenient references
|
||||
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||
@@ -123,9 +122,7 @@ def register():
|
||||
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)
|
||||
return redirect(get_post_register_redirect())
|
||||
|
||||
if request.json:
|
||||
return _render_json(form)
|
||||
@@ -173,7 +170,7 @@ def token_login(token):
|
||||
if invalid or expired:
|
||||
return redirect(url_for('login'))
|
||||
|
||||
login_user(user, True)
|
||||
login_user(user)
|
||||
after_this_request(_commit)
|
||||
do_flash(*get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'))
|
||||
|
||||
@@ -203,7 +200,6 @@ def send_confirmation():
|
||||
**_ctx('send_confirmation'))
|
||||
|
||||
|
||||
@anonymous_user_required
|
||||
def confirm_email(token):
|
||||
"""View function which handles a email confirmation request."""
|
||||
|
||||
@@ -220,8 +216,11 @@ def confirm_email(token):
|
||||
return redirect(get_url(_security.confirm_error_view) or
|
||||
url_for('send_confirmation'))
|
||||
|
||||
if user != current_user:
|
||||
logout_user()
|
||||
login_user(user)
|
||||
|
||||
confirm_user(user)
|
||||
login_user(user, True)
|
||||
after_this_request(_commit)
|
||||
do_flash(*get_message('EMAIL_CONFIRMED'))
|
||||
|
||||
@@ -272,7 +271,7 @@ def reset_password(token):
|
||||
after_this_request(_commit)
|
||||
update_password(user, form.password.data)
|
||||
do_flash(*get_message('PASSWORD_RESET'))
|
||||
login_user(user, True)
|
||||
login_user(user)
|
||||
return redirect(get_url(_security.post_reset_view) or
|
||||
get_url(_security.post_login_view))
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='Flask-Security',
|
||||
version='1.6.0',
|
||||
version='1.6.7',
|
||||
url='https://github.com/mattupstate/flask-security',
|
||||
license='MIT',
|
||||
author='Matt Wright',
|
||||
@@ -35,7 +35,7 @@ setup(
|
||||
platforms='any',
|
||||
install_requires=[
|
||||
'Flask>=0.9',
|
||||
'Flask-Login>=0.1.3',
|
||||
'Flask-Login>=0.2.3',
|
||||
'Flask-Mail>=0.7.3',
|
||||
'Flask-Principal>=0.3.3',
|
||||
'Flask-WTF>=0.8',
|
||||
|
||||
@@ -68,6 +68,14 @@ class ConfiguredSecurityTests(SecurityTest):
|
||||
r = self._post('/register', data=data, follow_redirects=True)
|
||||
self.assertIn('Post Register', r.data)
|
||||
|
||||
def test_register_with_next_querystring_argument(self):
|
||||
data = dict(email='dude@lp.com',
|
||||
password='password',
|
||||
password_confirm='password')
|
||||
|
||||
r = self._post('/register?next=/page1', data=data, follow_redirects=True)
|
||||
self.assertIn('Page 1', r.data)
|
||||
|
||||
def test_register_json(self):
|
||||
data = '{ "email": "dude@lp.com", "password": "password", "csrf_token":"%s" }' % self.csrf_token
|
||||
r = self._post('/register', data=data, content_type='application/json')
|
||||
@@ -328,6 +336,24 @@ class LoginWithoutImmediateConfirmTests(SecurityTest):
|
||||
r = self._post('/register', data=data, follow_redirects=True)
|
||||
self.assertIn(e, r.data)
|
||||
|
||||
def test_confirm_email_of_user_different_than_current_user(self):
|
||||
e1 = 'dude@lp.com'
|
||||
e2 = 'lady@lp.com'
|
||||
|
||||
with capture_registrations() as registrations:
|
||||
self.register(e1)
|
||||
self.register(e2)
|
||||
token1 = registrations[0]['confirm_token']
|
||||
token2 = registrations[1]['confirm_token']
|
||||
|
||||
self.client.get('/confirm/' + token1, follow_redirects=True)
|
||||
self.client.get('/logout')
|
||||
self.authenticate(email=e1)
|
||||
r = self.client.get('/confirm/' + token2, follow_redirects=True)
|
||||
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
|
||||
self.assertIn(msg, r.data)
|
||||
self.assertIn('Hello %s' % e2, r.data)
|
||||
|
||||
|
||||
class RecoverableTests(SecurityTest):
|
||||
|
||||
@@ -451,7 +477,7 @@ class ChangePasswordTest(SecurityTest):
|
||||
'new_password_confirm': 'a'
|
||||
}, follow_redirects=True)
|
||||
self.assertNotIn('You successfully changed your password', r.data)
|
||||
self.assertIn('Field must be between', r.data)
|
||||
self.assertIn('Password must be at least 6 characters', r.data)
|
||||
|
||||
def test_change_password_success(self):
|
||||
self.authenticate()
|
||||
|
||||
@@ -60,7 +60,7 @@ class DefaultSecurityTests(SecurityTest):
|
||||
|
||||
def test_unauthorized_access(self):
|
||||
r = self._get('/profile', follow_redirects=True)
|
||||
self.assertIn('<li class="message">Please log in to access this page.</li>', r.data)
|
||||
self.assertIn('<li class="info">Please log in to access this page.</li>', r.data)
|
||||
|
||||
def test_authorized_access(self):
|
||||
self.authenticate()
|
||||
@@ -142,6 +142,13 @@ class DefaultSecurityTests(SecurityTest):
|
||||
})
|
||||
self.assertIn('HTTP Authentication', r.data)
|
||||
|
||||
def test_http_auth_no_authorization(self):
|
||||
r = self._get('/http', headers={})
|
||||
self.assertIn('<h1>Unauthorized</h1>', r.data)
|
||||
self.assertIn('WWW-Authenticate', r.headers)
|
||||
self.assertEquals('Basic realm="Login Required"',
|
||||
r.headers['WWW-Authenticate'])
|
||||
|
||||
def test_invalid_http_auth_invalid_username(self):
|
||||
r = self._get('/http', headers={
|
||||
'Authorization': 'Basic ' + base64.b64encode("bogus:bogus")
|
||||
@@ -224,11 +231,11 @@ class MongoEngineSecurityTests(DefaultSecurityTests):
|
||||
return create_app(auth_config, **kwargs)
|
||||
|
||||
|
||||
# class PeeweeSecurityTests(DefaultSecurityTests):
|
||||
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)
|
||||
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):
|
||||
|
||||
+27
-20
@@ -15,7 +15,14 @@ def compare_user(a, b):
|
||||
return a.id == b.id and a.email == b.email and a.password == b.password
|
||||
|
||||
|
||||
class RegisterableSignalsTests(SecurityTest):
|
||||
class SignalTest(SecurityTest):
|
||||
|
||||
def _create_app(self, auth_config, **kwargs):
|
||||
from tests.test_app.mongoengine import create_app
|
||||
return create_app(auth_config, **kwargs)
|
||||
|
||||
|
||||
class RegisterableSignalsTests(SignalTest):
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_CONFIRMABLE': True,
|
||||
@@ -31,9 +38,9 @@ class RegisterableSignalsTests(SecurityTest):
|
||||
calls = mocks[user_registered]
|
||||
self.assertEqual(len(calls), 1)
|
||||
args, kwargs = calls[0]
|
||||
self.assertTrue(compare_user(args[0]['user'], user))
|
||||
self.assertIn('confirm_token', args[0])
|
||||
self.assertEqual(kwargs['app'], self.app)
|
||||
self.assertTrue(compare_user(kwargs['user'], user))
|
||||
self.assertIn('confirm_token', kwargs)
|
||||
self.assertEqual(args[0], self.app)
|
||||
|
||||
def test_register_without_password(self):
|
||||
e = 'dude@lp.com'
|
||||
@@ -42,7 +49,7 @@ class RegisterableSignalsTests(SecurityTest):
|
||||
self.assertEqual(mocks.signals_sent(), set())
|
||||
|
||||
|
||||
class ConfirmableSignalsTests(SecurityTest):
|
||||
class ConfirmableSignalsTests(SignalTest):
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_CONFIRMABLE': True,
|
||||
@@ -61,8 +68,8 @@ class ConfirmableSignalsTests(SecurityTest):
|
||||
calls = mocks[user_confirmed]
|
||||
self.assertEqual(len(calls), 1)
|
||||
args, kwargs = calls[0]
|
||||
self.assertEqual(args[0].id, user.id)
|
||||
self.assertEqual(kwargs['app'], self.app)
|
||||
self.assertEqual(args[0], self.app)
|
||||
self.assertTrue(compare_user(kwargs['user'], user))
|
||||
|
||||
def test_confirm_bad_token(self):
|
||||
e = 'dude@lp.com'
|
||||
@@ -94,8 +101,8 @@ class ConfirmableSignalsTests(SecurityTest):
|
||||
calls = mocks[confirm_instructions_sent]
|
||||
self.assertEqual(len(calls), 1)
|
||||
args, kwargs = calls[0]
|
||||
self.assertTrue(compare_user(args[0], user))
|
||||
self.assertEqual(kwargs['app'], self.app)
|
||||
self.assertTrue(compare_user(kwargs['user'], user))
|
||||
self.assertEqual(args[0], self.app)
|
||||
|
||||
def test_send_confirmation_bad_email(self):
|
||||
with capture_signals() as mocks:
|
||||
@@ -103,7 +110,7 @@ class ConfirmableSignalsTests(SecurityTest):
|
||||
self.assertEqual(mocks.signals_sent(), set())
|
||||
|
||||
|
||||
class RecoverableSignalsTests(SecurityTest):
|
||||
class RecoverableSignalsTests(SignalTest):
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_RECOVERABLE': True,
|
||||
@@ -120,9 +127,9 @@ class RecoverableSignalsTests(SecurityTest):
|
||||
calls = mocks[reset_password_instructions_sent]
|
||||
self.assertEqual(len(calls), 1)
|
||||
args, kwargs = calls[0]
|
||||
self.assertTrue(compare_user(args[0]['user'], user))
|
||||
self.assertIn('token', args[0])
|
||||
self.assertEqual(kwargs['app'], self.app)
|
||||
self.assertTrue(compare_user(kwargs['user'], user))
|
||||
self.assertIn('token', kwargs)
|
||||
self.assertEqual(args[0], self.app)
|
||||
|
||||
def test_reset_password(self):
|
||||
with capture_reset_password_requests() as requests:
|
||||
@@ -137,8 +144,8 @@ class RecoverableSignalsTests(SecurityTest):
|
||||
calls = mocks[password_reset]
|
||||
self.assertEqual(len(calls), 1)
|
||||
args, kwargs = calls[0]
|
||||
self.assertTrue(compare_user(args[0], user))
|
||||
self.assertEqual(kwargs['app'], self.app)
|
||||
self.assertTrue(compare_user(kwargs['user'], user))
|
||||
self.assertEqual(args[0], self.app)
|
||||
|
||||
def test_reset_password_invalid_emails(self):
|
||||
with capture_signals() as mocks:
|
||||
@@ -153,7 +160,7 @@ class RecoverableSignalsTests(SecurityTest):
|
||||
self.assertEqual(mocks.signals_sent(), set())
|
||||
|
||||
|
||||
class ChangeableSignalsTests(SecurityTest):
|
||||
class ChangeableSignalsTests(SignalTest):
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_CHANGEABLE': True,
|
||||
@@ -204,7 +211,7 @@ class ChangeableSignalsTests(SecurityTest):
|
||||
self.assertEqual(mocks.signals_sent(), set())
|
||||
|
||||
|
||||
class PasswordlessTests(SecurityTest):
|
||||
class PasswordlessTests(SignalTest):
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_PASSWORDLESS': True
|
||||
@@ -233,6 +240,6 @@ class PasswordlessTests(SecurityTest):
|
||||
calls = mocks[login_instructions_sent]
|
||||
self.assertEqual(len(calls), 1)
|
||||
args, kwargs = calls[0]
|
||||
self.assertTrue(compare_user(args[0]['user'], user))
|
||||
self.assertIn('login_token', args[0])
|
||||
self.assertEqual(kwargs['app'], self.app)
|
||||
self.assertTrue(compare_user(kwargs['user'], user))
|
||||
self.assertIn('login_token', kwargs)
|
||||
self.assertEqual(args[0], self.app)
|
||||
|
||||
@@ -113,6 +113,10 @@ def create_app(config):
|
||||
def invalid_role():
|
||||
return 'success' if ds.find_role('bogus') is None else 'failure'
|
||||
|
||||
@app.route('/page1')
|
||||
def page_1():
|
||||
return 'Page 1'
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
@@ -14,11 +14,12 @@ from flask.ext.security import Security, UserMixin, RoleMixin, \
|
||||
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',
|
||||
'engine': 'peewee.SqliteDatabase'
|
||||
}
|
||||
db = Database(app)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user