Compare commits

...
26 Commits
Author SHA1 Message Date
Matt Wright 07bb00f75c Bump version number to 1.6.9 2013-08-20 12:35:29 -04:00
Matt Wright 9a10be5c90 Merge branch 'develop' 2013-08-20 12:35:12 -04:00
Matt Wright 829bb135d7 Update CHANGES 2013-08-20 12:35:08 -04:00
Matt Wright ab64406b6c Merge pull request #152 from AleshGood/develop
Fix imports from Flask-WTF
2013-08-20 09:31:32 -07:00
Alexey Poryadin 6f68f1d540 Fix imports from Flask-WTF 2013-08-15 12:46:09 +04:00
Matt Wright 510c7bfa2a Merge pull request #151 from lupiter/fix-peewee-delete-role
Commit deleting role from user (peewee).
2013-08-12 06:58:47 -07:00
Matt Wright 67d8134e3a Merge pull request #150 from vmalloc/fix-147
#147: only get by email_or_id if it is numeric
2013-08-12 06:58:35 -07:00
Wise, Catherine (ICT Centre, Acton) 7bef96c261 Commit deleting role from user (peewee). 2013-08-12 15:07:13 +10:00
Rotem YaariandRotem Yaari 7d6b42e960 #147: only get by email_or_id if it is numeric 2013-08-11 20:15:06 +03:00
Matt Wright e773232e5a Merge branch 'develop' 2013-08-01 16:28:18 -04:00
Matt Wright f401eac496 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2013-08-01 16:28:11 -04:00
Matt Wright f6b532103d Bump version number to 1.6.8 2013-08-01 16:27:32 -04:00
Matt Wright 27a4bbac6a Update CHANGES 2013-08-01 16:27:25 -04:00
Matt Wright 0cf7d563dc Update changes 2013-08-01 16:24:46 -04:00
Matt Wright 7c906de427 When logging in user lookups on email should be case insensitive 2013-08-01 16:22:28 -04:00
Matt Wright 9c03884441 Merge pull request #140 from joeyespo/ignore-validation-errors
Ignore validation errors in find_user
2013-07-25 14:28:01 -07:00
Joe Esposito 4ed4579738 Ignore validation errors in find_user 2013-07-24 16:08:19 -04:00
Matt Wright 156ccaecc1 Attempting a fix to get tests to pass with travis. Very odd... 2013-07-24 11:35:04 -04:00
Matt Wright a5482922f3 Merge pull request #139 from codebutler/develop
Use token_callback for checking tokens.
2013-07-24 07:01:29 -07:00
Eric Butler d3cfddfcac Use token_callback for checking tokens.
Fixes error if user is not found.
2013-07-23 15:37:28 -07:00
Matt Wright 312a527498 PEP8 and general clean up on tests 2013-07-22 12:42:20 -04:00
Matt Wright d87765fc3b PEP8 polish 2013-07-22 12:37:44 -04:00
Matt Wright d05e699a15 Clean up 2013-07-22 12:19:29 -04:00
Matt Wright e7855488df Docs polish 2013-07-22 11:44:19 -04:00
Matt Wright 01bdc0d356 A little more documentation. Fixes #131 2013-07-22 11:35:45 -04:00
Matt Wright 5e2a016bcd Update docs. Fixes #137 2013-07-22 11:26:11 -04:00
20 changed files with 195 additions and 101 deletions
+20
View File
@@ -3,6 +3,26 @@ Flask-Security Changelog
Here you can see the full list of changes between each Flask-Security release.
Version 1.6.9
-------------
Released August 20th 2013
- Fix bug in SQLAlchemy datastore's `get_user` function
- Fix bug in PeeWee datastore's `remove_role_from_user` function
- Fixed import error caused by new Flask-WTF release
Version 1.6.8
-------------
Released August 1st 2013
- Fixed bug with case sensitivity of email address during login
- Code cleanup regarding token_callback
- Ignore validation errors in find_user function for MongoEngineUserDatastore
Version 1.6.7
-------------
+1 -1
View File
@@ -49,7 +49,7 @@ copyright = u'2012, Matt Wright'
# built documents.
#
# The short X.Y version.
version = '1.6.7'
version = '1.6.9'
# The full version, including alpha/beta/rc tags.
release = version
+33 -19
View File
@@ -56,6 +56,8 @@ URLs and Views
``/register``.
``SECURITY_RESET_URL`` Specifies the password reset URL. Defaults to
``/reset``.
``SECURITY_CHANGE_URL`` Specifies the password change URL. Defaults to
``/change``.
``SECURITY_CONFIRM_URL`` Specifies the email confirmation URL. Defaults
to ``/confirm``.
``SECURITY_POST_LOGIN_VIEW`` Specifies the default view to redirect to after
@@ -86,8 +88,14 @@ URLs and Views
successfully resets their password. This value
can be set to a URL or an endpoint name. If this
value is ``None`` the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults to
``None``.
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``.
``SECURITY_POST_CHANGE_VIEW`` Specifies the view to redirect to after a user
successfully changes their password. This value
can be set to a URL or an endpoint name. If this
value is ``None`` the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``.
``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user
attempts to access a URL/endpoint that they do
not have permission to access. If this value is
@@ -167,23 +175,29 @@ Email
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
=========================================== ====================================
``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the
confirmation email. Defaults to
``Welcome``
``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the
passwordless feature. Defaults to
``Login instructions``
``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password
notice. Defaults to
``Your password has been reset``
``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the password
reset. Defaults to
``Password reset instructions``
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
confirmation message. Defaults to
``Please confirm your email``
=========================================== ====================================
================================================= ==============================
``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the
confirmation email. Defaults
to ``Welcome``
``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the
passwordless feature. Defaults
to ``Login instructions``
``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password
notice. Defaults to ``Your
password has been reset``
``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the
password reset email. Defaults
to ``Password reset
instructions``
``SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE`` Sets the subject for the
password change notice.
Defaults to ``Your password
has been changed``
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
confirmation message. Defaults
to ``Please confirm your
email``
================================================= ==============================
Miscellaneous
-------------
+16 -1
View File
@@ -34,7 +34,8 @@ Password encryption is enabled with `passlib`_. Passwords are stored in plain
text by default but you can easily configure the encryption algorithm. You
should **always use an encryption algorithm** in your production environment.
You may also specify to use HMAC with a configured salt value in addition to the
algorithm chosen. Bear in mind passlib does not assume which algorithm you will choose and may require additional libraries to be installed.
algorithm chosen. Bear in mind passlib does not assume which algorithm you will
choose and may require additional libraries to be installed.
Basic HTTP Authentication
@@ -104,6 +105,20 @@ statistics. They include:
* Total login count
JSON/Ajax Support
-----------------
Flask-Security supports JSON/Ajax requests where appropriate. Just remember that
all endpoints require a CSRF token just like HTML views. More specifically
JSON is supported for the following operations:
* Login requests
* Registration requests
* Change password requests
* Confirmation requests
* Forgot password requests
* Passwordless login requests
.. _Flask-Login: http://packages.python.org/Flask-Login/
.. _alternative token: http://packages.python.org/Flask-Login/#alternative-tokens
+1
View File
@@ -13,6 +13,7 @@ Flask application. They include:
7. Token based password recovery / resetting (optional)
8. User registration (optional)
9. Login tracking (optional)
10. JSON/Ajax Support
Many of these features are made possible by integrating various Flask extensions
and libraries. They include:
+1 -1
View File
@@ -10,7 +10,7 @@
:license: MIT, see LICENSE for more details.
"""
__version__ = '1.6.7'
__version__ = '1.6.9'
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
+1 -1
View File
@@ -10,7 +10,7 @@
:license: MIT, see LICENSE for more details.
"""
from flask import current_app as app, request
from flask import current_app as app
from werkzeug.local import LocalProxy
from .signals import password_changed
+1 -1
View File
@@ -11,7 +11,7 @@
from datetime import datetime
from flask import current_app as app, request
from flask import current_app as app
from werkzeug.local import LocalProxy
from .utils import send_mail, md5, url_for_security, get_token_status,\
+5 -5
View File
@@ -244,7 +244,7 @@ def _context_processor():
class RoleMixin(object):
"""Mixin for `Role` model definitions"""
def __eq__(self, other):
return (self.name == other or \
return (self.name == other or
self.name == getattr(other, 'name', None))
def __ne__(self, other):
@@ -346,10 +346,10 @@ class Security(object):
self._state = self.init_app(app, datastore, **kwargs)
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, change_password_form=None,
send_confirmation_form=None, passwordless_login_form=None):
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):
"""Initializes the Flask-Security extension for the specified
application and datastore implentation.
+46 -2
View File
@@ -9,6 +9,7 @@
:license: MIT, see LICENSE for more details.
"""
class Datastore(object):
def __init__(self, db):
self.db = db
@@ -81,6 +82,10 @@ class UserDatastore(object):
kwargs['roles'] = roles
return kwargs
def get_user(self, id_or_email):
"""Returns a user matching the specified ID or email address"""
raise NotImplementedError
def find_user(self, *args, **kwargs):
"""Returns a user matching the provided parameters."""
raise NotImplementedError
@@ -174,6 +179,22 @@ class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
SQLAlchemyDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
def get_user(self, id_or_email):
returned = None
if self._is_numeric(id_or_email):
returned = self.user_model.query.get(id_or_email)
if not returned:
returned = self.user_model.query.filter(
self.user_model.email.ilike(id_or_email)).first()
return returned
def _is_numeric(self, value):
try:
int(value)
except ValueError:
return False
return True
def find_user(self, **kwargs):
return self.user_model.query.filter_by(**kwargs).first()
@@ -189,15 +210,26 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
MongoEngineDatastore.__init__(self, db)
UserDatastore.__init__(self, user_model, role_model)
def get_user(self, id_or_email):
from mongoengine import ValidationError
try:
return self.user_model.objects(id=id_or_email).first()
except ValidationError:
return self.user_model.objects(email__iexact=id_or_email).first()
def find_user(self, **kwargs):
try:
from mongoengine.queryset import Q, QCombination
except ImportError:
from mongoengine.queryset.visitor import Q, QCombination
from mongoengine.errors import ValidationError
queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items())
query = QCombination(QCombination.AND, queries)
return self.user_model.objects(query).first()
try:
return self.user_model.objects(query).first()
except ValidationError:
return None
def find_role(self, role):
return self.role_model.objects(name=role).first()
@@ -216,6 +248,17 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
UserDatastore.__init__(self, user_model, role_model)
self.UserRole = role_link
def get_user(self, id_or_email):
try:
return self.user_model.get(self.user_model.id == id_or_email)
except ValueError:
pass
try:
return self.user_model.get(self.user_model.email ** id_or_email)
except self.user_model.DoesNotExist:
pass
return None
def find_user(self, **kwargs):
try:
return self.user_model.filter(**kwargs).get()
@@ -259,8 +302,9 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
result = self.UserRole.select() \
.where(self.UserRole.user == user, self.UserRole.role == role)
if result.count():
self.UserRole.delete().where(
query = self.UserRole.delete().where(
self.UserRole.user == user, self.UserRole.role == role)
query.execute()
return True
else:
return False
+4 -8
View File
@@ -53,21 +53,17 @@ def _check_token():
token = request.args.get(args_key, header_token)
if request.json:
token = request.json.get(args_key, token)
serializer = _security.remember_token_serializer
try:
data = serializer.loads(token)
except:
return False
user = _security.login_manager.token_callback(token)
user = _security.datastore.find_user(id=data[0])
if utils.md5(user.password) == data[1]:
if user and user.is_authenticated():
app = current_app._get_current_object()
_request_ctx_stack.top.user = user
identity_changed.send(app, identity=Identity(user.id))
return True
return False
def _check_http_auth():
auth = request.authorization or BasicAuth(username=None, password=None)
+24 -22
View File
@@ -15,7 +15,8 @@ import urlparse
import flask_wtf as wtf
from flask import request, current_app
from flask_wtf import Form as BaseForm, TextField, PasswordField, \
from flask_wtf import Form as BaseForm
from wtforms import TextField, PasswordField, validators, \
SubmitField, HiddenField, BooleanField, ValidationError, Field
from flask_login import current_user
from werkzeug.local import LocalProxy
@@ -50,19 +51,19 @@ class ValidatorMixin(object):
return super(ValidatorMixin, self).__call__(form, field)
class EqualTo(ValidatorMixin, wtf.EqualTo):
class EqualTo(ValidatorMixin, validators.EqualTo):
pass
class Required(ValidatorMixin, wtf.Required):
class Required(ValidatorMixin, validators.Required):
pass
class Email(ValidatorMixin, wtf.Email):
class Email(ValidatorMixin, validators.Email):
pass
class Length(ValidatorMixin, wtf.Length):
class Length(ValidatorMixin, validators.Length):
pass
@@ -96,33 +97,32 @@ class Form(BaseForm):
class EmailFormMixin():
email = TextField(get_form_field_label('email'),
validators=[email_required,
email_validator])
email = TextField(
get_form_field_label('email'),
validators=[email_required, email_validator])
class UserEmailFormMixin():
user = None
email = TextField(get_form_field_label('email'),
validators=[email_required,
email_validator,
valid_user_email])
email = TextField(
get_form_field_label('email'),
validators=[email_required, email_validator, valid_user_email])
class UniqueEmailFormMixin():
email = TextField(get_form_field_label('email'),
validators=[email_required,
email_validator,
unique_user_email])
email = TextField(
get_form_field_label('email'),
validators=[email_required, email_validator, unique_user_email])
class PasswordFormMixin():
password = PasswordField(get_form_field_label('password'),
validators=[password_required])
password = PasswordField(
get_form_field_label('password'), validators=[password_required])
class NewPasswordFormMixin():
password = PasswordField(get_form_field_label('password'),
password = PasswordField(
get_form_field_label('password'),
validators=[password_required, password_length])
@@ -220,7 +220,7 @@ class LoginForm(Form, NextFormMixin):
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
return False
self.user = _datastore.find_user(email=self.email.data)
self.user = _datastore.get_user(self.email.data)
if self.user is None:
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
@@ -255,10 +255,12 @@ class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
class ChangePasswordForm(Form, PasswordFormMixin):
"""The default change password form"""
new_password = PasswordField(get_form_field_label('new_password'),
new_password = PasswordField(
get_form_field_label('new_password'),
validators=[password_required, password_length])
new_password_confirm = PasswordField(get_form_field_label('retype_password'),
new_password_confirm = PasswordField(
get_form_field_label('retype_password'),
validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
submit = SubmitField(get_form_field_label('change_password'))
+1 -1
View File
@@ -9,7 +9,7 @@
:license: MIT, see LICENSE for more details.
"""
from flask import request, current_app as app
from flask import current_app as app
from werkzeug.local import LocalProxy
from .signals import login_instructions_sent
+2 -1
View File
@@ -9,7 +9,7 @@
:license: MIT, see LICENSE for more details.
"""
from flask import current_app as app, request
from flask import current_app as app
from werkzeug.local import LocalProxy
from .signals import password_reset, reset_password_instructions_sent
@@ -67,6 +67,7 @@ def reset_password_token_status(token):
"""
return get_token_status(token, 'reset', 'RESET_PASSWORD')
def update_password(user, password):
"""Update the specified user's password
+1 -1
View File
@@ -38,6 +38,6 @@ def register_user(**kwargs):
if config_value('SEND_REGISTER_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
user=user, confirmation_link=confirmation_link)
user=user, confirmation_link=confirmation_link)
return user
+3 -4
View File
@@ -78,8 +78,9 @@ def get_hmac(password):
return password
if _security.password_salt is None:
raise RuntimeError('The configuration value `SECURITY_PASSWORD_SALT` '
'must not be None when the value of `SECURITY_PASSWORD_HASH` is '
raise RuntimeError(
'The configuration value `SECURITY_PASSWORD_SALT` must '
'not be None when the value of `SECURITY_PASSWORD_HASH` is '
'set to "%s"' % _security.password_hash)
h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512)
@@ -386,5 +387,3 @@ def capture_signals():
confirm_instructions_sent, login_instructions_sent,
password_reset, password_changed,
reset_password_instructions_sent])
+1 -1
View File
@@ -20,7 +20,7 @@ from setuptools import setup
setup(
name='Flask-Security',
version='1.6.7',
version='1.6.9',
url='https://github.com/mattupstate/flask-security',
license='MIT',
author='Matt Wright',
+7 -6
View File
@@ -30,7 +30,8 @@ class SecurityTest(TestCase):
session['csrf'] = 'csrf_token'
csrf_hmac = hmac.new(self.app.config['SECRET_KEY'],
'csrf_token'.encode('utf8'), digestmod=sha1)
'csrf_token'.encode('utf8'),
digestmod=sha1)
self.csrf_token = '##' + csrf_hmac.hexdigest()
def _create_app(self, auth_config, **kwargs):
@@ -38,17 +39,17 @@ class SecurityTest(TestCase):
def _get(self, route, content_type=None, follow_redirects=None, headers=None):
return self.client.get(route, follow_redirects=follow_redirects,
content_type=content_type or 'text/html',
headers=headers)
content_type=content_type or 'text/html',
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
content_type = content_type or 'application/x-www-form-urlencoded'
return self.client.post(route, data=data,
follow_redirects=follow_redirects,
content_type=content_type or 'application/x-www-form-urlencoded',
headers=headers)
follow_redirects=follow_redirects,
content_type=content_type, headers=headers)
def register(self, email, password='password'):
data = dict(email=email, password=password, csrf_token=self.csrf_token)
+22 -26
View File
@@ -122,7 +122,6 @@ class DefaultTemplatePathTests(SecurityTest):
'SECURITY_LOGIN_USER_TEMPLATE': 'custom_security/login_user.html',
}
def test_login_user_template(self):
r = self._get('/login')
@@ -155,9 +154,8 @@ class RecoverableTemplatePathTests(SecurityTest):
def test_reset_password_template(self):
with capture_reset_password_requests() as requests:
r = self._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']
@@ -365,9 +363,8 @@ class RecoverableTests(SecurityTest):
def test_reset_view(self):
with capture_reset_password_requests() as requests:
r = self._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']
r = self._get('/reset/' + t)
self.assertIn('<h1>Reset password</h1>', r.data)
@@ -380,20 +377,18 @@ class RecoverableTests(SecurityTest):
def test_forgot_password_json(self):
r = self._post('/reset', data='{"email": "matt@lp.com"}',
content_type="application/json")
content_type="application/json")
self.assertEquals(r.status_code, 200)
def test_forgot_password_invalid_email(self):
r = self._post('/reset',
data=dict(email='larry@lp.com'),
follow_redirects=True)
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._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']
r = self._post('/reset/' + t, data={
@@ -480,13 +475,15 @@ class ChangePasswordTest(SecurityTest):
self.assertIn('Password must be at least 6 characters', r.data)
def test_change_password_success(self):
data = {
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}
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)
r = self._post('/change', data=data, follow_redirects=True)
self.assertIn('You successfully changed your password', r.data)
self.assertIn('Home Page', r.data)
@@ -504,12 +501,13 @@ class ChangePasswordPostViewTest(SecurityTest):
}
def test_change_password_success(self):
data = {
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
r = self._post('/change', data=data, follow_redirects=True)
self.assertIn('Profile Page', r.data)
@@ -792,8 +790,6 @@ class ConfirmableExtendFormsTest(SecurityTest):
r = self._get('/register', follow_redirects=True)
self.assertIn("My Confirm Register Email Address Field", r.data)
def test_send_confirmation(self):
r = self._get('/confirm', follow_redirects=True)
self.assertIn("My Send Confirmation Email Address Field", r.data)
+5
View File
@@ -33,6 +33,10 @@ class DefaultSecurityTests(SecurityTest):
r = self.authenticate()
self.assertIn('Hello matt@lp.com', r.data)
def test_authenticate_case_insensitive_email(self):
r = self.authenticate(email='MATT@lp.com')
self.assertIn('Hello matt@lp.com', r.data)
def test_unprovided_username(self):
r = self.authenticate("")
self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data)
@@ -59,6 +63,7 @@ class DefaultSecurityTests(SecurityTest):
self.assertIsHomePage(r.data)
def test_unauthorized_access(self):
self.logout()
r = self._get('/profile', follow_redirects=True)
self.assertIn('<li class="info">Please log in to access this page.</li>', r.data)