Compare commits

..
59 Commits
Author SHA1 Message Date
Matt Wright 8b1ab24341 Bump version number to 1.7.5 2015-12-02 17:20:46 -05:00
Matt Wright ea77940ffc Merge pull request #454 from jonafato/changelog-1.7.5
Add changelog for bugfix version 1.7.5
2015-12-02 17:19:37 -05:00
Jon Banafato 2adc572e80 Add changelog for bugfix version 1.7.5 2015-12-02 17:13:32 -05:00
Jon Banafato 2ac4f373ab Merge pull request #438 from jonafato/is_active-fix
Fix use of is_active for Flask-Login>=0.3.0
2015-12-02 16:42:06 -05:00
Jon Banafato 132ced5675 Merge pull request #442 from nfvs/migrate_travis_containers
Migrate to Travis container-based infrastructure, optimize build with pip caching.
2015-11-18 11:29:14 -05:00
Nuno Santos aeb5ffbb1d Enable pip and apt cache in Travis for faster build times. 2015-11-12 16:07:00 +01:00
Nuno Santos c2ee2e2629 Migrate to Travis container-based infrastructure.
Advantages are listed here:
http://docs.travis-ci.com/user/migrating-from-legacy
2015-11-12 14:51:07 +01:00
Jon Banafato 8005709997 Fix use of is_active for Flask-Login>=0.3.0 2015-11-11 16:02:41 -05:00
Matt Wright 2abdaa873a Merge pull request #437 from jonafato/tox-test-improvements
Fix tests, add py35
2015-11-11 15:58:29 -05:00
Jon Banafato 4508c717fb Fix tests, add py35
- Add Python 3.5 support on travis

- pytest-cache has been merged into pytest [0] and is no longer
  separately maintained, so pytest-cache-related requirements are being
  updated.

- Flask-Mongoengine latest stable is broken on Python 2.6 (but fixed in
  master [1]), so its version is being restricted for now.

[0] http://pytest.org/latest/changelog.html#id3 [1]
https://github.com/MongoEngine/flask-mongoengine/pull/174
2015-11-03 14:14:11 -05:00
Matt Wright 9583dc3e63 Merge pull request #417 from ialex/develop
Fixes #416 Changing is_authenticated from function to property & updating Flask-…
2015-11-03 10:46:57 -05:00
Alejandro Villanueva 745fae1ab1 Adding >0.4 to flask-loginrequirement 2015-09-24 10:03:46 -05:00
Alejandro Villanueva 7e8551763e Changing is_authenticated from function to property & updating Flask-Login>=0.3.0 2015-09-11 10:31:01 -05:00
Matt Wright 4049c06203 Merge pull request #366 from covertgeek/develop
Modified check_token function to handle list-type JSON post
2015-07-10 14:11:15 -04:00
Matt Wright 3b678b872f Merge pull request #400 from feliciaan/patch-1
Fixes a small spelling error
2015-07-10 14:07:15 -04:00
Derek Rushing 17a79ed6a3 Merge branch 'develop' of https://github.com/mattupstate/flask-security into develop 2015-07-06 06:51:20 -05:00
Derek Rushing d65c3cadcd Merge branch 'master' of https://github.com/mattupstate/flask-security into develop 2015-07-06 06:47:23 -05:00
Feliciaan De Palmenaer bf01ad0727 Fixes a small spelling error 2015-06-28 20:17:23 +02:00
Matt Wright cbd0db7c39 Merge pull request #398 from jonafato/bcrypt-hotfix
Restrict bcrypt to <2.0.0
2015-06-24 22:23:36 -05:00
Jon Banafato 398f5c920b Restrict bcrypt to <2.0.0
As of 2.0.0, passlib no longer correctly identifies bcrypt as bcrypt
(instead, it mistakenly applies pybcrypt logic to bcrypt). This results
in all Python 3 logic involving bcrypt failing. As a hotfix, we should
require users to be on a version of bcrypt that passlib can handle a fix
can be pushed into passlib.
2015-06-23 13:23:07 -04:00
Matt Wright 33252ae178 Merge pull request #389 from nickretallack/develop
Reset Password Fixes
2015-05-26 14:59:30 -04:00
Nick Retallack c10c9050c7 test: reset password on a user who has no password
The user may have been invited via a social network or an invitation
system.
2015-05-11 23:22:30 -07:00
Nick Retallack 5697ff80c3 ignore the eggs readme 2015-05-11 23:16:04 -07:00
Nick Retallack 4411470202 test: invalidate used password reset tokens
Also pep8 compliance and suggested changes.
2015-05-11 23:12:05 -07:00
Nick Retallack a0e2037747 invalidate password reset tokens when the passwords changes
Check that the previous password is the same as it was when this
password reset request was generated.
2015-05-11 21:52:57 -07:00
Nick Retallack 7884d637c5 prevent password reset from breaking if you have no password
If you've just been invited, or are using social auth, you have no
password set, so the reset password feature causes a crash.  This
doesn't need to happen.
2015-05-11 21:52:57 -07:00
Matt Wright 8a14abaa1e Fix failing test 2015-05-02 14:57:34 -04:00
Matt Wright 6b55e9613a Merge pull request #255 from nfvs/unauthorized_override
Allow overriding of an unauthorized callback.
2015-05-02 14:45:57 -04:00
Matt Wright d08aac6d35 Fix pymongo version issue 2015-05-02 14:34:31 -04:00
Nuno Santos 10fd1844d8 Allow overriding of unauthorized callback.
Related to issue #255.
2015-05-02 20:17:38 +02:00
Matt Wright 72d3a4b5e3 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2015-05-02 13:57:29 -04:00
Matt Wright 6541640ee8 Merge pull request #373 from lnielsen/anonymoususer
Add support for custom AnonymousUser class.
2015-05-02 13:57:20 -04:00
Matt Wright 9cda8baff3 Fix #367 2015-05-02 13:55:05 -04:00
Matt Wright d907e5b890 Merge pull request #363 from jeffwidman/patch-1
"overrided" --> "overriden"
2015-05-02 13:50:39 -04:00
Matt Wright 8a62b5f193 Merge pull request #361 from nfvs/add_headers_to_auth_required
Include WWW-Authenticate headers in @auth_required.
2015-05-02 13:50:23 -04:00
Matt Wright 2e08ec87a6 Merge pull request #352 from fuhrysteve/develop
X-Forwarded-For can contain multiple IP addresses
2015-05-02 13:46:21 -04:00
Matt Wright 79fe821c80 Merge pull request #351 from denizdogan/master
Add documentation about SECURITY_MSG configuration variables.
2015-05-02 13:45:13 -04:00
Matt Wright 4d3c1c0bdc Merge pull request #347 from Jaza/slash-url-suffix
re #343: Add slash before or after token in flask-security URLs correctly
2015-05-02 13:43:36 -04:00
Matt Wright cd8982fa65 Merge pull request #342 from alexef/patch-1
Fail silently for get_user(None)
2015-05-02 13:30:01 -04:00
Matt Wright c8a3549e2d Merge pull request #331 from Diaoul/patch-2
Fix ActivateUserCommand docstring
2015-05-02 13:29:21 -04:00
Matt Wright 03d9cf2f0d Merge pull request #330 from Diaoul/patch-1
Fix RemoveRoleCommand docstring
2015-05-02 13:29:07 -04:00
Matt Wright e4d9d3ad17 Merge pull request #322 from waltaskew/develop
Add configuration for token expiration
2015-05-02 13:27:11 -04:00
Matt Wright f2a5e4b614 Normalize import paths. Fixes #313 2015-05-02 13:25:26 -04:00
Matt Wright 916f5ee012 Use StringField instead of TextField. Fixes #312 2015-05-02 13:05:46 -04:00
Matt Wright bc1f5dd7f9 Stricter tests for signals and a small docs update. Fixes #308 2015-05-02 12:59:02 -04:00
Matt Wright 4659d10c5c forgot password endpoint should be for anonymous users only. Fixes #291 2015-05-02 12:11:05 -04:00
Lars Holm Nielsen a4581681e5 Fix PEP8 error. 2015-03-06 13:09:05 +01:00
Lars Holm Nielsen 248ea5d272 Custom AnonymousUser support.
(addresses #362)
2015-03-06 12:45:17 +01:00
Derek Rushing 4debc8d102 Modified check_token function to account for multiple objects being posted via JSON. Resolves issue with it throwing an error when it encounters a list instead of a dict type. 2015-02-18 10:20:22 -06:00
Jeff Widman fccaccd282 "overrided" --> "overriden"
Per http://english.stackexchange.com/questions/75786/overrode-vs-overridden
2015-02-02 23:34:52 -08:00
Nuno Santos 3681823fcf Include WWW-Authenticate headers in @auth_required.
When using @http_auth_required, the WWW-Authenticate header is included,
but when using @auth_required('basic'), it is not. This change includes
that header in every @auth_required call that contains the 'basic'
method.
2015-01-30 11:27:53 +01:00
Stephen J. FuhryandStephen J. Fuhry 923ad720a1 X-Forwarded-For can contain multiple IP addresses
From the nginx docs:
http://nginx.org/en/docs/http/ngx_http_proxy_module.html
> $proxy_add_x_forwarded_for
> the “X-Forwarded-For” client request header field with the $remote_addr
> variable appended to it, separated by a comma. If the “X-Forwarded-For”
> field is not present in the client request header, the
> $proxy_add_x_forwarded_for variable is equal to the $remote_addr
> variable.

Use the last IP address in X-Forwarded-For. For this to work properly
behind a trusted proxy, you must be using ProxyFix as described in the
flask & werkzeug documentation.
2014-12-29 08:31:19 -05:00
Deniz Dogan f4d758f561 Add documentation about SECURITY_MSG configuration variables. 2014-12-21 15:20:48 +01:00
Jeremy Epstein 665b164618 split docstring into multiple lines to make travis CI happy 2014-11-28 13:50:25 +11:00
Jeremy Epstein 4d70f016ad re #343: Add slash before or after token in flask-security URLs correctly 2014-11-28 10:36:31 +11:00
Alex Eftimie 7e4fc94601 Fail silently for get_user(None)
get_user(identifier) checks if the identifier is a number by trying to convert it to int. This works for strings, but in a particular case, when identifier is None, it fails. Checking for both TypeError and ValueError fixes it.
2014-11-19 14:11:58 +02:00
Antoine Bertin 6cfe662dc6 Fix ActivateUserCommand docstring 2014-10-21 11:26:17 +02:00
Antoine Bertin 8c45271bf9 Fix RemoveRoleCommand docstring 2014-10-21 10:27:17 +02:00
waltaskew 897b2fceab Add configuration for token expiration 2014-10-01 15:59:28 -04:00
37 changed files with 389 additions and 136 deletions
+10
View File
@@ -34,3 +34,13 @@ env/
*.db *.db
*cache* *cache*
# vim
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
.eggs/README.txt
+14
View File
@@ -5,13 +5,27 @@ python:
- "2.7" - "2.7"
- "3.3" - "3.3"
- "3.4" - "3.4"
- "3.5"
- "pypy" - "pypy"
sudo: false
cache:
- apt
- pip
install: install:
- pip install -r requirements.txt -r requirements-dev.txt -e . - pip install -r requirements.txt -r requirements-dev.txt -e .
- pip install coverage coveralls - pip install coverage coveralls
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib; fi" - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib; fi"
addons:
apt:
sources:
- deadsnakes
packages:
- python3.5
services: services:
- mongodb - mongodb
+1
View File
@@ -36,3 +36,4 @@ Rotem Yaari
Srijan Choudhary Srijan Choudhary
Tristan Escalada Tristan Escalada
Vadim Kotov Vadim Kotov
Walt Askew
+20
View File
@@ -3,6 +3,26 @@ Flask-Security Changelog
Here you can see the full list of changes between each Flask-Security release. Here you can see the full list of changes between each Flask-Security release.
Version 1.7.5
-------------
Released December 2nd 2015
- Added `SECURITY_TOKEN_MAX_AGE` configuration setting
- Fixed calls to `SQLAlchemyUserDatastore.get_user(None)` (this now returns `False` instead of raising a `TypeError`
- Fixed URL generation adding extra slashes in some cases (see GitHub #343)
- Fixed handling of trackable IP addresses when the `X-Forwarded-For` header contains multiple values
- Include WWW-Authenticate headers in `@auth_required` authentication checks
- Fixed error when `check_token` function is used with a json list
- Added support for custom `AnonymousUser` classes
- Restricted `forgot_password` endpoint to anonymous users
- Allowed unauthorized callback to be overridden
- Fixed issue where passwords cannot be reset if currently set to `None`
- Ensured that password reset tokens are invalidated after use
- Updated `is_authenticated` and `is_active` functions to support Flask-Login changes
- Various documentation improvements
Version 1.7.4 Version 1.7.4
------------- -------------
+14 -19
View File
@@ -87,43 +87,38 @@ sends the following signals.
.. data:: user_registered .. data:: user_registered
Sent when a user registers on the site. It is passed a dict with Sent when a user registers on the site. In addition to the app (which is the
the `user` and `confirm_token`, the user being logged in and the sender), it is passed `user` and `confirm_token` arguments.
(if so configured) the confirmation token issued.
.. data:: user_confirmed .. data:: user_confirmed
Sent when a user is confirmed. It is passed `user`, which is the Sent when a user is confirmed. In addition to the app (which is the
user being confirmed. sender), it is passed a `user` argument.
.. data:: confirm_instructions_sent .. data:: confirm_instructions_sent
Sent when a user requests confirmation instructions. It is passed Sent when a user requests confirmation instructions. In addition to the app
the `user`. (which is the sender), it is passed a `user` argument.
.. data:: login_instructions_sent .. data:: login_instructions_sent
Sent when passwordless login is used and user logs in. It is passed Sent when passwordless login is used and user logs in. In addition to the app
a dict with the `user` and `login_token`, the user being logged in (which is the sender), it is passed `user` and `login_token` arguments.
and the (if so configured) the login token issued.
.. data:: password_reset .. data:: password_reset
Sent when a user completes a password reset. It is passed the Sent when a user completes a password reset. In addition to the app (which is
`user`. the sender), it is passed a `user` argument.
.. data:: password_changed .. data:: password_changed
Sent when a user completes a password change. It is passed the Sent when a user completes a password change. In addition to the app (which is
`user`. the sender), it is passed a `user` argument.
.. data:: reset_password_instructions_sent .. data:: reset_password_instructions_sent
Sent when a user requests a password reset. It is passed a dict Sent when a user requests a password reset. In addition to the app (which is
with the `user` and `token`, the user being logged in and the sender), it is passed `user` and `token` arguments.
the (if so configured) the reset token issued.
All signals are also passed a `app` keyword argument, which is the
current application.
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/ .. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
+1 -1
View File
@@ -49,7 +49,7 @@ copyright = u'2012, Matt Wright'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.7.4' version = '1.7.5'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version
+52 -3
View File
@@ -23,7 +23,10 @@ Core
passwords. Recommended values for passwords. Recommended values for
production systems are ``bcrypt``, production systems are ``bcrypt``,
``sha512_crypt``, or ``pbkdf2_sha512``. ``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 ``SECURITY_PASSWORD_SALT`` Specifies the HMAC salt. This is only
used if the password hash type is set used if the password hash type is set
to something other than plain text. to something other than plain text.
@@ -31,12 +34,16 @@ Core
``SECURITY_EMAIL_SENDER`` Specifies the email address to send ``SECURITY_EMAIL_SENDER`` Specifies the email address to send
emails as. Defaults to emails as. Defaults to
``no-reply@localhost``. ``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. read when using token authentication.
Defaults to ``auth_token``. Defaults to ``auth_token``.
``SECURITY_TOKEN_AUTHENTICATION_HEADER`` Specifies the HTTP header to read when ``SECURITY_TOKEN_AUTHENTICATION_HEADER`` Specifies the HTTP header to read when
using token authentication. Defaults to using token authentication. Defaults to
``Authentication-Token``. ``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 ``SECURITY_DEFAULT_HTTP_AUTH_REALM`` Specifies the default authentication
realm when using basic HTTP auth. realm when using basic HTTP auth.
Defaults to ``Login Required`` Defaults to ``Login Required``
@@ -159,7 +166,8 @@ Feature Flags
option. Defaults to ``False``. option. Defaults to ``False``.
``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user ``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user
login statistics. If set to ``True``, ensure your 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`` ``False``
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the ``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the
passwordless login feature. If set to ``True``, users passwordless login feature. If set to ``True``, users
@@ -261,3 +269,44 @@ Miscellaneous
me" value used when logging in me" value used when logging in
a user. Defaults to ``False``. a user. Defaults to ``False``.
============================================= ================================== ============================================= ==================================
Messages
-------------
The following are the messages Flask-Security uses. They are tuples; the first
element is the message and the second element is the error level.
The default messages and error levels can be found in ``core.py``.
* ``SECURITY_MSG_ALREADY_CONFIRMED``
* ``SECURITY_MSG_CONFIRMATION_EXPIRED``
* ``SECURITY_MSG_CONFIRMATION_REQUEST``
* ``SECURITY_MSG_CONFIRMATION_REQUIRED``
* ``SECURITY_MSG_CONFIRM_REGISTRATION``
* ``SECURITY_MSG_DISABLED_ACCOUNT``
* ``SECURITY_MSG_EMAIL_ALREADY_ASSOCIATED``
* ``SECURITY_MSG_EMAIL_CONFIRMED``
* ``SECURITY_MSG_EMAIL_NOT_PROVIDED``
* ``SECURITY_MSG_INVALID_CONFIRMATION_TOKEN``
* ``SECURITY_MSG_INVALID_EMAIL_ADDRESS``
* ``SECURITY_MSG_INVALID_LOGIN_TOKEN``
* ``SECURITY_MSG_INVALID_PASSWORD``
* ``SECURITY_MSG_INVALID_REDIRECT``
* ``SECURITY_MSG_INVALID_RESET_PASSWORD_TOKEN``
* ``SECURITY_MSG_LOGIN``
* ``SECURITY_MSG_LOGIN_EMAIL_SENT``
* ``SECURITY_MSG_LOGIN_EXPIRED``
* ``SECURITY_MSG_PASSWORDLESS_LOGIN_SUCCESSFUL``
* ``SECURITY_MSG_PASSWORD_CHANGE``
* ``SECURITY_MSG_PASSWORD_INVALID_LENGTH``
* ``SECURITY_MSG_PASSWORD_IS_THE_SAME``
* ``SECURITY_MSG_PASSWORD_MISMATCH``
* ``SECURITY_MSG_PASSWORD_NOT_PROVIDED``
* ``SECURITY_MSG_PASSWORD_NOT_SET``
* ``SECURITY_MSG_PASSWORD_RESET``
* ``SECURITY_MSG_PASSWORD_RESET_EXPIRED``
* ``SECURITY_MSG_PASSWORD_RESET_REQUEST``
* ``SECURITY_MSG_REFRESH``
* ``SECURITY_MSG_RETYPE_PASSWORD_MISMATCH``
* ``SECURITY_MSG_UNAUTHORIZED``
* ``SECURITY_MSG_USER_DOES_NOT_EXIST``
+2 -2
View File
@@ -73,8 +73,8 @@ register form or override validators::
from flask_security.forms import RegisterForm from flask_security.forms import RegisterForm
class ExtendedRegisterForm(RegisterForm): class ExtendedRegisterForm(RegisterForm):
first_name = TextField('First Name', [Required()]) first_name = StringField('First Name', [Required()])
last_name = TextField('Last Name', [Required()]) last_name = StringField('Last Name', [Required()])
security = Security(app, user_datastore, security = Security(app, user_datastore,
register_form=ExtendedRegisterForm) register_form=ExtendedRegisterForm)
+1 -1
View File
@@ -89,7 +89,7 @@ User Registration
Flask-Security comes packaged with a basic user registration view. This view is Flask-Security comes packaged with a basic user registration view. This view is
very simple and new users need only supply an email address and their password. very simple and new users need only supply an email address and their password.
This view can be overrided if your registration process requires more fields. This view can be overridden if your registration process requires more fields.
Login Tracking Login Tracking
+4 -4
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security flask_security
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
Flask-Security is a Flask extension that aims to add quick and simple Flask-Security is a Flask extension that aims to add quick and simple
security via Flask-Login, Flask-Principal, Flask-WTF, and passlib. security via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
@@ -10,8 +10,6 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
__version__ = '1.7.4'
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
from .decorators import auth_token_required, http_auth_required, \ 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, \ from .signals import confirm_instructions_sent, password_reset, \
reset_password_instructions_sent, user_confirmed, user_registered reset_password_instructions_sent, user_confirmed, user_registered
from .utils import login_user, logout_user, url_for_security from .utils import login_user, logout_user, url_for_security
__version__ = '1.7.5'
+4 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.changeable flask_security.changeable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security recoverable module Flask-Security recoverable module
@@ -42,4 +42,5 @@ def change_user_password(user, password):
user.password = encrypt_password(password) user.password = encrypt_password(password)
_datastore.put(user) _datastore.put(user)
send_password_changed_notice(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())
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.confirmable flask_security.confirmable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security confirmable module Flask-Security confirmable module
+22 -14
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.core flask_security.core
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
Flask-Security core module Flask-Security core module
@@ -10,9 +10,9 @@
""" """
from flask import current_app, render_template 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 LoginManager, current_user
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \ from flask_principal import Principal, RoleNeed, UserNeed, Identity, \
identity_loaded identity_loaded
from itsdangerous import URLSafeTimedSerializer from itsdangerous import URLSafeTimedSerializer
from passlib.context import CryptContext from passlib.context import CryptContext
@@ -75,6 +75,7 @@ _default_config = {
'EMAIL_SENDER': 'no-reply@localhost', 'EMAIL_SENDER': 'no-reply@localhost',
'TOKEN_AUTHENTICATION_KEY': 'auth_token', 'TOKEN_AUTHENTICATION_KEY': 'auth_token',
'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token', 'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token',
'TOKEN_MAX_AGE': None,
'CONFIRM_SALT': 'confirm-salt', 'CONFIRM_SALT': 'confirm-salt',
'RESET_SALT': 'reset-salt', 'RESET_SALT': 'reset-salt',
'LOGIN_SALT': 'login-salt', 'LOGIN_SALT': 'login-salt',
@@ -192,17 +193,17 @@ def _user_loader(user_id):
def _token_loader(token): def _token_loader(token):
try: 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]) user = _security.datastore.find_user(id=data[0])
if user and safe_str_cmp(md5(user.password), data[1]): if user and safe_str_cmp(md5(user.password), data[1]):
return user return user
except: except:
pass pass
return AnonymousUser() return _security.login_manager.anonymous_user()
def _identity_loader(): 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) identity = Identity(current_user.id)
return identity return identity
@@ -217,9 +218,9 @@ def _on_identity_loaded(sender, identity):
identity.user = current_user identity.user = current_user
def _get_login_manager(app): def _get_login_manager(app, anonymous_user):
lm = LoginManager() lm = LoginManager()
lm.anonymous_user = AnonymousUser lm.anonymous_user = anonymous_user or AnonymousUser
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app) lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
lm.user_loader(_user_loader) lm.user_loader(_user_loader)
lm.token_loader(_token_loader) lm.token_loader(_token_loader)
@@ -257,14 +258,14 @@ def _get_serializer(app, name):
return URLSafeTimedSerializer(secret_key=secret_key, salt=salt) 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(): for key, value in get_config(app).items():
kwargs[key.lower()] = value kwargs[key.lower()] = value
kwargs.update(dict( kwargs.update(dict(
app=app, app=app,
datastore=datastore, datastore=datastore,
login_manager=_get_login_manager(app), login_manager=_get_login_manager(app, anonymous_user),
principal=_get_principal(app), principal=_get_principal(app),
pwd_context=_get_pwd_context(app), pwd_context=_get_pwd_context(app),
remember_token_serializer=_get_serializer(app, 'remember'), remember_token_serializer=_get_serializer(app, 'remember'),
@@ -272,7 +273,8 @@ def _get_state(app, datastore, **kwargs):
reset_serializer=_get_serializer(app, 'reset'), reset_serializer=_get_serializer(app, 'reset'),
confirm_serializer=_get_serializer(app, 'confirm'), confirm_serializer=_get_serializer(app, 'confirm'),
_context_processors={}, _context_processors={},
_send_mail_task=None _send_mail_task=None,
_unauthorized_callback=None
)) ))
for key, value in _default_forms.items(): for key, value in _default_forms.items():
@@ -303,6 +305,7 @@ class RoleMixin(object):
class UserMixin(BaseUserMixin): class UserMixin(BaseUserMixin):
"""Mixin for `User` model definitions""" """Mixin for `User` model definitions"""
@property
def is_active(self): def is_active(self):
"""Returns `True` if the user is active.""" """Returns `True` if the user is active."""
return self.active return self.active
@@ -380,6 +383,9 @@ class _SecurityState(object):
def send_mail_task(self, fn): def send_mail_task(self, fn):
self._send_mail_task = fn self._send_mail_task = fn
def unauthorized_handler(self, fn):
self._unauthorized_callback = fn
class Security(object): class Security(object):
"""The :class:`Security` class initializes the Flask-Security extension. """The :class:`Security` class initializes the Flask-Security extension.
@@ -398,7 +404,8 @@ class Security(object):
login_form=None, confirm_register_form=None, login_form=None, confirm_register_form=None,
register_form=None, forgot_password_form=None, register_form=None, forgot_password_form=None,
reset_password_form=None, change_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 """Initializes the Flask-Security extension for the specified
application and datastore implentation. application and datastore implentation.
@@ -424,7 +431,8 @@ class Security(object):
reset_password_form=reset_password_form, reset_password_form=reset_password_form,
change_password_form=change_password_form, change_password_form=change_password_form,
send_confirmation_form=send_confirmation_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: if register_blueprint:
app.register_blueprint(create_blueprint(state, __name__)) app.register_blueprint(create_blueprint(state, __name__))
+3 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.datastore flask_security.datastore
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
This module contains an user datastore classes. This module contains an user datastore classes.
@@ -195,7 +195,7 @@ class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
def _is_numeric(self, value): def _is_numeric(self, value):
try: try:
int(value) int(value)
except ValueError: except (TypeError, ValueError):
return False return False
return True return True
+37 -17
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.decorators flask_security.decorators
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security decorators module Flask-Security decorators module
@@ -13,8 +13,8 @@ from collections import namedtuple
from functools import wraps from functools import wraps
from flask import current_app, Response, request, redirect, _request_ctx_stack 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_login import current_user, login_required # pragma: no flakes
from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed from flask_principal import RoleNeed, Permission, Identity, identity_changed
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from . import utils from . import utils
@@ -52,11 +52,12 @@ def _check_token():
header_token = request.headers.get(header_key, None) header_token = request.headers.get(header_key, None)
token = request.args.get(args_key, header_token) token = request.args.get(args_key, header_token)
if request.get_json(silent=True): if request.get_json(silent=True):
token = request.json.get(args_key, token) if not isinstance(request.json, list):
token = request.json.get(args_key, token)
user = _security.login_manager.token_callback(token) user = _security.login_manager.token_callback(token)
if user and user.is_authenticated(): if user and user.is_authenticated:
app = current_app._get_current_object() app = current_app._get_current_object()
_request_ctx_stack.top.user = user _request_ctx_stack.top.user = user
identity_changed.send(app, identity=Identity(user.id)) identity_changed.send(app, identity=Identity(user.id))
@@ -90,9 +91,12 @@ def http_auth_required(realm):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if _check_http_auth(): if _check_http_auth():
return fn(*args, **kwargs) return fn(*args, **kwargs)
r = _security.default_http_auth_realm if callable(realm) else realm if _security._unauthorized_callback:
h = {'WWW-Authenticate': 'Basic realm="%s"' % r} return _security._unauthorized_callback()
return _get_unauthorized_response(headers=h) 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 return wrapper
if callable(realm): if callable(realm):
@@ -112,7 +116,10 @@ def auth_token_required(fn):
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
if _check_token(): if _check_token():
return fn(*args, **kwargs) return fn(*args, **kwargs)
return _get_unauthorized_response() if _security._unauthorized_callback:
return _security._unauthorized_callback()
else:
return _get_unauthorized_response()
return decorated return decorated
@@ -131,17 +138,24 @@ def auth_required(*auth_methods):
login_mechanisms = { login_mechanisms = {
'token': lambda: _check_token(), 'token': lambda: _check_token(),
'basic': lambda: _check_http_auth(), 'basic': lambda: _check_http_auth(),
'session': lambda: current_user.is_authenticated() 'session': lambda: current_user.is_authenticated
} }
def wrapper(fn): def wrapper(fn):
@wraps(fn) @wraps(fn)
def decorated_view(*args, **kwargs): def decorated_view(*args, **kwargs):
mechanisms = [login_mechanisms.get(method) for method in auth_methods] h = {}
for mechanism in mechanisms: mechanisms = [(method, login_mechanisms.get(method)) for method in auth_methods]
for method, mechanism in mechanisms:
if mechanism and mechanism(): if mechanism and mechanism():
return fn(*args, **kwargs) 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 decorated_view
return wrapper return wrapper
@@ -166,7 +180,10 @@ def roles_required(*roles):
perms = [Permission(RoleNeed(role)) for role in roles] perms = [Permission(RoleNeed(role)) for role in roles]
for perm in perms: for perm in perms:
if not perm.can(): 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 fn(*args, **kwargs)
return decorated_view return decorated_view
return wrapper return wrapper
@@ -192,7 +209,10 @@ def roles_accepted(*roles):
perm = Permission(*[RoleNeed(role) for role in roles]) perm = Permission(*[RoleNeed(role) for role in roles])
if perm.can(): if perm.can():
return fn(*args, **kwargs) 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 decorated_view
return wrapper return wrapper
@@ -200,7 +220,7 @@ def roles_accepted(*roles):
def anonymous_user_required(f): def anonymous_user_required(f):
@wraps(f) @wraps(f)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if current_user.is_authenticated(): if current_user.is_authenticated:
return redirect(utils.get_url(_security.post_login_view)) return redirect(utils.get_url(_security.post_login_view))
return f(*args, **kwargs) return f(*args, **kwargs)
return wrapper return wrapper
+9 -9
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.forms flask_security.forms
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
Flask-Security forms module Flask-Security forms module
@@ -13,7 +13,7 @@ import inspect
from flask import request, current_app, flash from flask import request, current_app, flash
from flask_wtf import Form as BaseForm from flask_wtf import Form as BaseForm
from wtforms import TextField, PasswordField, validators, \ from wtforms import StringField, PasswordField, validators, \
SubmitField, HiddenField, BooleanField, ValidationError, Field SubmitField, HiddenField, BooleanField, ValidationError, Field
from flask_login import current_user from flask_login import current_user
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
@@ -94,20 +94,20 @@ class Form(BaseForm):
class EmailFormMixin(): class EmailFormMixin():
email = TextField( email = StringField(
get_form_field_label('email'), get_form_field_label('email'),
validators=[email_required, email_validator]) validators=[email_required, email_validator])
class UserEmailFormMixin(): class UserEmailFormMixin():
user = None user = None
email = TextField( email = StringField(
get_form_field_label('email'), get_form_field_label('email'),
validators=[email_required, email_validator, valid_user_email]) validators=[email_required, email_validator, valid_user_email])
class UniqueEmailFormMixin(): class UniqueEmailFormMixin():
email = TextField( email = StringField(
get_form_field_label('email'), get_form_field_label('email'),
validators=[email_required, email_validator, unique_user_email]) validators=[email_required, email_validator, unique_user_email])
@@ -195,7 +195,7 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
def validate(self): def validate(self):
if not super(PasswordlessLoginForm, self).validate(): if not super(PasswordlessLoginForm, self).validate():
return False return False
if not self.user.is_active(): if not self.user.is_active:
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
return False return False
return True return True
@@ -204,7 +204,7 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
class LoginForm(Form, NextFormMixin): class LoginForm(Form, NextFormMixin):
"""The default login form""" """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')) password = PasswordField(get_form_field_label('password'))
remember = BooleanField(get_form_field_label('remember_me')) remember = BooleanField(get_form_field_label('remember_me'))
submit = SubmitField(get_form_field_label('login')) submit = SubmitField(get_form_field_label('login'))
@@ -241,7 +241,7 @@ class LoginForm(Form, NextFormMixin):
if requires_confirmation(self.user): if requires_confirmation(self.user):
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
return False return False
if not self.user.is_active(): if not self.user.is_active:
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
return False return False
return True return True
+3 -4
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.passwordless flask_security.passwordless
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security passwordless module Flask-Security passwordless module
@@ -35,8 +35,7 @@ def send_login_instructions(user):
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email, send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
'login_instructions', user=user, login_link=login_link) 'login_instructions', user=user, login_link=login_link)
login_instructions_sent.send(app._get_current_object(), login_instructions_sent.send(app._get_current_object(), user=user, login_token=token)
user=user, login_token=token)
def generate_login_token(user): def generate_login_token(user):
+16 -7
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.recoverable flask_security.recoverable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security recoverable module Flask-Security recoverable module
@@ -11,6 +11,7 @@
from flask import current_app as app from flask import current_app as app
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from werkzeug.security import safe_str_cmp
from .signals import password_reset, reset_password_instructions_sent from .signals import password_reset, reset_password_instructions_sent
from .utils import send_mail, md5, encrypt_password, url_for_security, \ from .utils import send_mail, md5, encrypt_password, url_for_security, \
@@ -35,8 +36,7 @@ def send_reset_password_instructions(user):
'reset_instructions', 'reset_instructions',
user=user, reset_link=reset_link) user=user, reset_link=reset_link)
reset_password_instructions_sent.send(app._get_current_object(), reset_password_instructions_sent.send(app._get_current_object(), user=user, token=token)
user=user, token=token)
def send_password_reset_notice(user): def send_password_reset_notice(user):
@@ -54,7 +54,8 @@ def generate_reset_password_token(user):
:param user: The user to work with :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) 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 """Returns the expired status, invalid status, and user of a password reset
token. For example:: token. For example::
expired, invalid, user = reset_password_token_status('...') expired, invalid, user, data = reset_password_token_status('...')
:param token: The password reset token :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): def update_password(user, password):
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.registerable flask_security.registerable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security registerable module Flask-Security registerable module
+6 -6
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.script flask_security.script
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
Flask-Security script module Flask-Security script module
@@ -18,7 +18,7 @@ except ImportError:
import re import re
from flask import current_app from flask import current_app
from flask.ext.script import Command, Option from flask_script import Command, Option
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from .utils import encrypt_password from .utils import encrypt_password
@@ -100,7 +100,7 @@ class AddRoleCommand(_RoleCommand):
class RemoveRoleCommand(_RoleCommand): class RemoveRoleCommand(_RoleCommand):
"""Add a role to a user""" """Remove a role from a user"""
@commit @commit
def run(self, user_identifier, role_name): def run(self, user_identifier, role_name):
@@ -115,7 +115,7 @@ class _ToggleActiveCommand(Command):
class DeactivateUserCommand(_ToggleActiveCommand): class DeactivateUserCommand(_ToggleActiveCommand):
"""Deactive a user""" """Deactivate a user"""
@commit @commit
def run(self, user_identifier): def run(self, user_identifier):
@@ -124,7 +124,7 @@ class DeactivateUserCommand(_ToggleActiveCommand):
class ActivateUserCommand(_ToggleActiveCommand): class ActivateUserCommand(_ToggleActiveCommand):
"""Deactive a user""" """Activate a user"""
@commit @commit
def run(self, user_identifier): def run(self, user_identifier):
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.signals flask_security.signals
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
Flask-Security signals module Flask-Security signals module
+22 -10
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.utils flask_security.utils
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
Flask-Security utils module Flask-Security utils module
@@ -23,9 +23,9 @@ from contextlib import contextmanager
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import url_for, flash, current_app, request, session, render_template 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_login import login_user as _login_user, logout_user as _logout_user
from flask.ext.mail import Message from flask_mail import Message
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed from flask_principal import Identity, AnonymousIdentity, identity_changed
from itsdangerous import BadSignature, SignatureExpired from itsdangerous import BadSignature, SignatureExpired
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
@@ -62,10 +62,10 @@ def login_user(user, remember=None):
return False return False
if _security.trackable: if _security.trackable:
if 'X-Forwarded-For' not in request.headers: if 'X-Forwarded-For' in request.headers:
remote_addr = request.remote_addr or 'untrackable' remote_addr = request.headers.getlist("X-Forwarded-For")[0].rpartition(' ')[-1]
else: 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_login, new_current_login = user.current_login_at, datetime.utcnow()
old_current_ip, new_current_ip = user.current_login_ip, remote_addr 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 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): def get_security_endpoint_name(endpoint):
return '%s.%s' % (_security.blueprint_name, endpoint) return '%s.%s' % (_security.blueprint_name, endpoint)
@@ -333,7 +341,7 @@ def send_mail(subject, recipient, template, **context):
mail.send(msg) 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. """Get the status of a token.
:param token: The token to check :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]) user = _datastore.find_user(id=data[0])
expired = expired and (user is not None) 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): def get_identity_attributes(app=None):
+8 -7
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.views flask_security.views
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
Flask-Security views module Flask-Security views module
@@ -26,7 +26,7 @@ from .changeable import change_user_password
from .registerable import register_user from .registerable import register_user
from .utils import config_value, do_flash, get_url, get_post_login_redirect, \ from .utils import config_value, do_flash, get_url, get_post_login_redirect, \
get_post_register_redirect, get_message, login_user, logout_user, \ 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 # Convenient references
_security = LocalProxy(lambda: current_app.extensions['security']) _security = LocalProxy(lambda: current_app.extensions['security'])
@@ -90,7 +90,7 @@ def login():
def logout(): def logout():
"""View function which handles a logout request.""" """View function which handles a logout request."""
if current_user.is_authenticated(): if current_user.is_authenticated:
logout_user() logout_user()
return redirect(request.args.get('next', None) or return redirect(request.args.get('next', None) or
@@ -238,6 +238,7 @@ def confirm_email(token):
get_url(_security.post_login_view)) get_url(_security.post_login_view))
@anonymous_user_required
def forgot_password(): def forgot_password():
"""View function that handles a forgotten password request.""" """View function that handles a forgotten password request."""
@@ -333,7 +334,7 @@ def create_blueprint(state, import_name):
bp.route(state.login_url, bp.route(state.login_url,
methods=['GET', 'POST'], methods=['GET', 'POST'],
endpoint='login')(send_login) 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) endpoint='token_login')(token_login)
else: else:
bp.route(state.login_url, bp.route(state.login_url,
@@ -349,7 +350,7 @@ def create_blueprint(state, import_name):
bp.route(state.reset_url, bp.route(state.reset_url,
methods=['GET', 'POST'], methods=['GET', 'POST'],
endpoint='forgot_password')(forgot_password) 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'], methods=['GET', 'POST'],
endpoint='reset_password')(reset_password) endpoint='reset_password')(reset_password)
@@ -362,7 +363,7 @@ def create_blueprint(state, import_name):
bp.route(state.confirm_url, bp.route(state.confirm_url,
methods=['GET', 'POST'], methods=['GET', 'POST'],
endpoint='send_confirmation')(send_confirmation) 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'], methods=['GET', 'POST'],
endpoint='confirm_email')(confirm_email) endpoint='confirm_email')(confirm_email)
+3 -3
View File
@@ -1,9 +1,9 @@
Flask-SQLAlchemy>=1.0 Flask-SQLAlchemy>=1.0
bcrypt>=1.0.2 bcrypt>=1.0.2,<2.0.0
flask-mongoengine>=0.7.0 flask-mongoengine>=0.7.0,<0.7.3
flask-peewee>=0.6.5 flask-peewee>=0.6.5
pymongo==2.8
pytest>=2.5.2 pytest>=2.5.2
pytest-cache>=1.0
pytest-cov>=1.6 pytest-cov>=1.6
pytest-flakes>=0.2 pytest-flakes>=0.2
pytest-pep8>=1.0.5 pytest-pep8>=1.0.5
+1 -1
View File
@@ -1,5 +1,5 @@
Flask>=0.9 Flask>=0.9
Flask-Login>=0.1.3 Flask-Login>=0.3.0,<0.4
Flask-Mail>=0.7.3 Flask-Mail>=0.7.3
Flask-Principal>=0.3.3 Flask-Principal>=0.3.3
Flask-WTF>=0.8 Flask-WTF>=0.8
+2 -2
View File
@@ -32,7 +32,7 @@ class PyTest(TestCommand):
'--cov-report', 'term-missing', '--cov-report', 'term-missing',
'--pep8', '--pep8',
'--flakes', '--flakes',
'--clearcache' '--cache-clear'
] ]
self.test_suite = True self.test_suite = True
@@ -44,7 +44,7 @@ class PyTest(TestCommand):
setup( setup(
name='Flask-Security', name='Flask-Security',
version='1.7.4', version='1.7.5',
url='https://github.com/mattupstate/flask-security', url='https://github.com/mattupstate/flask-security',
license='MIT', license='MIT',
author='Matt Wright', author='Matt Wright',
+2 -2
View File
@@ -1,4 +1,4 @@
{%- if current_user.is_authenticated() -%} {%- if current_user.is_authenticated -%}
<p>Hello {{ current_user.email }}</p> <p>Hello {{ current_user.email }}</p>
{%- endif %} {%- endif %}
<ul> <ul>
@@ -11,7 +11,7 @@
<li><a href="{{ url_for('admin_or_editor') }}">Admin or Editor</a></li> <li><a href="{{ url_for('admin_or_editor') }}">Admin or Editor</a></li>
{% endif -%} {% endif -%}
<li> <li>
{%- if current_user.is_authenticated() -%} {%- if current_user.is_authenticated -%}
<a href="{{ url_for('security.logout') }}">Log out</a> <a href="{{ url_for('security.logout') }}">Log out</a>
{%- else -%} {%- else -%}
<a href="{{ url_for('security.login') }}">Log in</a> <a href="{{ url_for('security.login') }}">Log in</a>
+4
View File
@@ -8,6 +8,8 @@
import pytest import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import password_changed from flask_security.signals import password_changed
from utils import authenticate from utils import authenticate
@@ -20,6 +22,8 @@ def test_recoverable_flag(app, client, get_message):
@password_changed.connect_via(app) @password_changed.connect_via(app)
def on_password_changed(app, user): def on_password_changed(app, user):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
recorded.append(user) recorded.append(user)
authenticate(client) authenticate(client)
+13
View File
@@ -226,6 +226,19 @@ def test_multi_auth_basic(client):
assert response.status_code == 401 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): def test_multi_auth_token(client):
response = json_authenticate(client) response = json_authenticate(client)
token = response.jdata['response']['user']['authentication_token'] token = response.jdata['response']['user']['authentication_token']
+6
View File
@@ -10,6 +10,8 @@ import time
import pytest 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.signals import user_confirmed, confirm_instructions_sent
from flask_security.utils import capture_registrations 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) @user_confirmed.connect_via(app)
def on_confirmed(app, user): def on_confirmed(app, user):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
recorded_confirms.append(user) recorded_confirms.append(user)
@confirm_instructions_sent.connect_via(app) @confirm_instructions_sent.connect_via(app)
def on_instructions_sent(app, user): def on_instructions_sent(app, user):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
recorded_instructions_sent.append(user) recorded_instructions_sent.append(user)
# Test login before confirmation # Test login before confirmation
+2
View File
@@ -81,6 +81,8 @@ def test_context_processors(client, app):
def mail(): def mail():
return {'foo': 'bar'} return {'foo': 'bar'}
client.get('/logout')
with app.mail.record_messages() as outbox: with app.mail.record_messages() as outbox:
client.post('/reset', data=dict(email='matt@lp.com')) client.post('/reset', data=dict(email='matt@lp.com'))
+26 -9
View File
@@ -11,7 +11,8 @@ import pytest
from flask_security import Security from flask_security import Security
from flask_security.forms import LoginForm, RegisterForm, ConfirmRegisterForm, \ from flask_security.forms import LoginForm, RegisterForm, ConfirmRegisterForm, \
SendConfirmationForm, PasswordlessLoginForm, ForgotPasswordForm, ResetPasswordForm, \ 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 flask_security.utils import capture_reset_password_requests, md5, string_types
from utils import authenticate, init_app_with_options, populate_data 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() @pytest.mark.changeable()
def test_basic_custom_forms(app, sqlalchemy_datastore): def test_basic_custom_forms(app, sqlalchemy_datastore):
class MyLoginForm(LoginForm): class MyLoginForm(LoginForm):
email = TextField('My Login Email Address Field') email = StringField('My Login Email Address Field')
class MyRegisterForm(RegisterForm): class MyRegisterForm(RegisterForm):
email = TextField('My Register Email Address Field') email = StringField('My Register Email Address Field')
class MyForgotPasswordForm(ForgotPasswordForm): class MyForgotPasswordForm(ForgotPasswordForm):
email = TextField('My Forgot Email Address Field', email = StringField('My Forgot Email Address Field',
validators=[email_required, email_validator, valid_user_email]) validators=[email_required, email_validator, valid_user_email])
class MyResetPasswordForm(ResetPasswordForm): class MyResetPasswordForm(ResetPasswordForm):
password = TextField('My Reset Password Field') password = StringField('My Reset Password Field')
class MyChangePasswordForm(ChangePasswordForm): class MyChangePasswordForm(ChangePasswordForm):
password = PasswordField('My Change Password Field') password = PasswordField('My Change Password Field')
@@ -96,10 +97,10 @@ def test_confirmable_custom_form(app, sqlalchemy_datastore):
app.config['SECURITY_CONFIRMABLE'] = True app.config['SECURITY_CONFIRMABLE'] = True
class MyRegisterForm(ConfirmRegisterForm): class MyRegisterForm(ConfirmRegisterForm):
email = TextField('My Register Email Address Field') email = StringField('My Register Email Address Field')
class MySendConfirmationForm(SendConfirmationForm): class MySendConfirmationForm(SendConfirmationForm):
email = TextField('My Send Confirmation Email Address Field') email = StringField('My Send Confirmation Email Address Field')
app.security = Security(app, app.security = Security(app,
datastore=sqlalchemy_datastore, datastore=sqlalchemy_datastore,
@@ -119,7 +120,7 @@ def test_passwordless_custom_form(app, sqlalchemy_datastore):
app.config['SECURITY_PASSWORDLESS'] = True app.config['SECURITY_PASSWORDLESS'] = True
class MyPasswordlessLoginForm(PasswordlessLoginForm): class MyPasswordlessLoginForm(PasswordlessLoginForm):
email = TextField('My Passwordless Email Address Field') email = StringField('My Passwordless Email Address Field')
app.security = Security(app, app.security = Security(app,
datastore=sqlalchemy_datastore, datastore=sqlalchemy_datastore,
@@ -191,3 +192,19 @@ def test_password_unicode_password_salt(client):
assert response.status_code == 302 assert response.status_code == 302
response = authenticate(client, follow_redirects=True) response = authenticate(client, follow_redirects=True)
assert b'Hello matt@lp.com' in response.data 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
+6 -1
View File
@@ -10,8 +10,10 @@ import time
import pytest import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import login_instructions_sent 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 from utils import logout
@@ -23,6 +25,9 @@ def test_trackable_flag(app, client, get_message):
@login_instructions_sent.connect_via(app) @login_instructions_sent.connect_via(app)
def on_instructions_sent(app, user, login_token): 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) recorded.append(user)
# Test disabled account # Test disabled account
+47 -1
View File
@@ -10,8 +10,10 @@ import time
import pytest 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.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 from utils import authenticate, logout
@@ -28,6 +30,9 @@ def test_recoverable_flag(app, client, get_message):
@reset_password_instructions_sent.connect_via(app) @reset_password_instructions_sent.connect_via(app)
def on_instructions_sent(app, user, token): 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) recorded_instructions_sent.append(user)
# Test the reset view # Test the reset view
@@ -117,6 +122,47 @@ def test_expired_reset_token(client, get_message):
assert msg in response.data 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') @pytest.mark.settings(reset_url='/custom_reset')
def test_custom_reset_url(client): def test_custom_reset_url(client):
response = client.get('/custom_reset') response = client.get('/custom_reset')
+5
View File
@@ -8,6 +8,8 @@
import pytest import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import user_registered from flask_security.signals import user_registered
from utils import authenticate, logout 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 # Test registering is successful, sends email, and fires signal
@user_registered.connect_via(app) @user_registered.connect_via(app)
def on_user_registerd(app, user, confirm_token): def on_user_registerd(app, user, confirm_token):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
assert confirm_token is None
recorded.append(user) recorded.append(user)
data = dict( data = dict(
+16
View File
@@ -26,3 +26,19 @@ def test_trackable_flag(app, client):
assert user.last_login_ip == 'untrackable' assert user.last_login_ip == 'untrackable'
assert user.current_login_ip == '127.0.0.1' assert user.current_login_ip == '127.0.0.1'
assert user.login_count == 2 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
+1 -1
View File
@@ -1,5 +1,5 @@
[tox] [tox]
envlist = py26, py27, py33, py34, pypy envlist = py26, py27, py33, py34, py35, pypy
[testenv] [testenv]
deps = deps =