mirror of
https://github.com/wassname/flask-security.git
synced 2026-06-28 16:20:24 +08:00
Merge https://github.com/mattupstate/flask-security into develop
Conflicts: flask_security/views.py
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
Flask-Security is written and maintained by Matt Wright and
|
||||
various contributors:
|
||||
|
||||
Development Lead
|
||||
````````````````
|
||||
|
||||
- Matt Wright <matt+github@nobien.net>
|
||||
|
||||
Patches and Suggestions
|
||||
```````````````````````
|
||||
|
||||
Alexander Sukharev
|
||||
Alexey Poryadin
|
||||
Andrew J. Camenga
|
||||
Anthony Plunkett
|
||||
Artem Andreev
|
||||
Catherine Wise
|
||||
Chris Haines
|
||||
Christophe Simonis
|
||||
David Ignacio
|
||||
Eric Butler
|
||||
Eskil Heyn Olsen
|
||||
Iuri de Silvio
|
||||
Jay Goel
|
||||
Joe Esposito
|
||||
Joe Hand
|
||||
Josh Purvis
|
||||
Kostyantyn Leschenko
|
||||
Luca Invernizzi
|
||||
Manuel Ebert
|
||||
Martin Maillard
|
||||
Paweł Krześniak
|
||||
Robert Clark
|
||||
Rodrigue Cloutier
|
||||
Rotem Yaari
|
||||
Srijan Choudhary
|
||||
Tristan Escalada
|
||||
Vadim Kotov
|
||||
@@ -54,6 +54,28 @@ Datastores
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Utils
|
||||
-----
|
||||
.. autofunction:: flask_security.utils.login_user
|
||||
|
||||
.. autofunction:: flask_security.utils.logout_user
|
||||
|
||||
.. autofunction:: flask_security.utils.get_hmac
|
||||
|
||||
.. autofunction:: flask_security.utils.verify_password
|
||||
|
||||
.. autofunction:: flask_security.utils.verify_and_update_password
|
||||
|
||||
.. autofunction:: flask_security.utils.encrypt_password
|
||||
|
||||
.. autofunction:: flask_security.utils.url_for_security
|
||||
|
||||
.. autofunction:: flask_security.utils.get_within_delta
|
||||
|
||||
.. autofunction:: flask_security.utils.send_mail
|
||||
|
||||
.. autofunction:: flask_security.utils.get_token_status
|
||||
|
||||
Signals
|
||||
-------
|
||||
See the `Flask documentation on signals`_ for information on how to use these
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.. include:: ../AUTHORS
|
||||
+57
-40
@@ -122,6 +122,9 @@ Template Paths
|
||||
``SECURITY_RESET_PASSWORD_TEMPLATE`` Specifies the path to the template for
|
||||
the reset password page. Defaults to
|
||||
``security/reset_password.html``.
|
||||
``SECURITY_CHANGE_PASSWORD_TEMPLATE`` Specifies the path to the template for
|
||||
the change password page. Defaults to
|
||||
``security/change_password.html``.
|
||||
``SECURITY_SEND_CONFIRMATION_TEMPLATE`` Specifies the path to the template for
|
||||
the resend confirmation instructions
|
||||
page. Defaults to
|
||||
@@ -204,43 +207,57 @@ Miscellaneous
|
||||
|
||||
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
|
||||
|
||||
======================================= ========================================
|
||||
``SECURITY_SEND_REGISTER_EMAIL`` Specifies whether registration email is
|
||||
sent. Defaults to ``True``.
|
||||
``SECURITY_SEND_PASSWORD_CHANGE_EMAIL`` Specifies whether password change email is
|
||||
sent. Defaults to ``True``.
|
||||
``SECURITY_CONFIRM_EMAIL_WITHIN`` Specifies the amount of time a user has
|
||||
before their confirmation link expires.
|
||||
Always pluralized the time unit for this
|
||||
value. Defaults to ``5 days``.
|
||||
``SECURITY_RESET_PASSWORD_WITHIN`` Specifies the amount of time a user has
|
||||
before their password reset link
|
||||
expires. Always pluralized the time unit
|
||||
for this value. Defaults to ``5 days``.
|
||||
``SECURITY_LOGIN_WITHIN`` Specifies the amount of time a user has
|
||||
before a login link expires. This is
|
||||
only used when the passwordless login
|
||||
feature is enabled. Always pluralized
|
||||
the time unit for this value. Defaults
|
||||
to ``1 days``.
|
||||
``SECURITY_LOGIN_WITHOUT_CONFIRMATION`` Specifies if a user may login before
|
||||
confirming their email when the value
|
||||
of ``SECURITY_CONFIRMABLE`` is set to
|
||||
``True``. Defaults to ``False``.
|
||||
``SECURITY_CONFIRM_SALT`` Specifies the salt value when generating
|
||||
confirmation links/tokens. Defaults to
|
||||
``confirm-salt``.
|
||||
``SECURITY_RESET_SALT`` Specifies the salt value when generating
|
||||
password reset links/tokens. Defaults to
|
||||
``reset-salt``.
|
||||
``SECURITY_LOGIN_SALT`` Specifies the salt value when generating
|
||||
login links/tokens. Defaults to
|
||||
``login-salt``.
|
||||
``SECURITY_REMEMBER_SALT`` Specifies the salt value when generating
|
||||
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``.
|
||||
======================================= ========================================
|
||||
============================================= ==================================
|
||||
``SECURITY_SEND_REGISTER_EMAIL`` Specifies whether registration
|
||||
email is sent. Defaults to
|
||||
``True``.
|
||||
``SECURITY_SEND_PASSWORD_CHANGE_EMAIL`` Specifies whether password change
|
||||
email is sent. Defaults to
|
||||
``True``.
|
||||
``SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL`` Specifies whether password reset
|
||||
notice email is sent. Defaults to
|
||||
``True``.
|
||||
|
||||
``SECURITY_CONFIRM_EMAIL_WITHIN`` Specifies the amount of time a
|
||||
user has before their confirmation
|
||||
link expires. Always pluralized
|
||||
the time unit for this value.
|
||||
Defaults to ``5 days``.
|
||||
``SECURITY_RESET_PASSWORD_WITHIN`` Specifies the amount of time a
|
||||
user has before their password
|
||||
reset link expires. Always
|
||||
pluralized the time unit for this
|
||||
value. Defaults to ``5 days``.
|
||||
``SECURITY_LOGIN_WITHIN`` Specifies the amount of time a
|
||||
user has before a login link
|
||||
expires. This is only used when
|
||||
the passwordless login feature is
|
||||
enabled. Always pluralized the
|
||||
time unit for this value.
|
||||
Defaults to ``1 days``.
|
||||
``SECURITY_LOGIN_WITHOUT_CONFIRMATION`` Specifies if a user may login
|
||||
before confirming their email when
|
||||
the value of
|
||||
``SECURITY_CONFIRMABLE`` is set to
|
||||
``True``. Defaults to ``False``.
|
||||
``SECURITY_CONFIRM_SALT`` Specifies the salt value when
|
||||
generating confirmation
|
||||
links/tokens. Defaults to
|
||||
``confirm-salt``.
|
||||
``SECURITY_RESET_SALT`` Specifies the salt value when
|
||||
generating password reset
|
||||
links/tokens. Defaults to
|
||||
``reset-salt``.
|
||||
``SECURITY_LOGIN_SALT`` Specifies the salt value when
|
||||
generating login links/tokens.
|
||||
Defaults to ``login-salt``.
|
||||
``SECURITY_REMEMBER_SALT`` Specifies the salt value when
|
||||
generating 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``.
|
||||
============================================= ==================================
|
||||
|
||||
@@ -10,4 +10,5 @@ Contents
|
||||
models
|
||||
customizing
|
||||
api
|
||||
changelog
|
||||
changelog
|
||||
authors
|
||||
|
||||
@@ -140,3 +140,23 @@ templates you can specify an email context processor with the
|
||||
@security.mail_context_processor
|
||||
def security_mail_processor():
|
||||
return dict(hello="world")
|
||||
|
||||
|
||||
Emails with Celery
|
||||
------------------
|
||||
|
||||
Sometimes it makes sense to send emails via a task queue, such as
|
||||
`Celery<http://www.celeryproject.org/>`_. To delay the sending of emails you can
|
||||
use the ``@security.send_mail_task`` decorator like so::
|
||||
|
||||
# Setup the task
|
||||
@celery.task
|
||||
def send_security_email(msg):
|
||||
# Use the Flask-Mail extension instance to send the incoming ``msg`` parameter
|
||||
# which is an instance of `flask_mail.Message`
|
||||
mail.send(msg)
|
||||
|
||||
@security.send_mail_task
|
||||
def delay_security_email(msg):
|
||||
send_security_email.delay(msg)
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ def generate_confirmation_token(user):
|
||||
|
||||
def requires_confirmation(user):
|
||||
"""Returns `True` if the user requires confirmation."""
|
||||
return _security.confirmable and user.confirmed_at == None
|
||||
return _security.confirmable and not _security.login_without_confirmation and user.confirmed_at == None
|
||||
|
||||
|
||||
def confirm_email_token_status(token):
|
||||
|
||||
@@ -55,6 +55,7 @@ _default_config = {
|
||||
'LOGIN_USER_TEMPLATE': 'security/login_user.html',
|
||||
'REGISTER_USER_TEMPLATE': 'security/register_user.html',
|
||||
'RESET_PASSWORD_TEMPLATE': 'security/reset_password.html',
|
||||
'CHANGE_PASSWORD_TEMPLATE': 'security/change_password.html',
|
||||
'SEND_CONFIRMATION_TEMPLATE': 'security/send_confirmation.html',
|
||||
'SEND_LOGIN_TEMPLATE': 'security/send_login.html',
|
||||
'CONFIRMABLE': False,
|
||||
@@ -65,6 +66,7 @@ _default_config = {
|
||||
'CHANGEABLE': False,
|
||||
'SEND_REGISTER_EMAIL': True,
|
||||
'SEND_PASSWORD_CHANGE_EMAIL': True,
|
||||
'SEND_PASSWORD_RESET_NOTICE_EMAIL': True,
|
||||
'LOGIN_WITHIN': '1 days',
|
||||
'CONFIRM_EMAIL_WITHIN': '5 days',
|
||||
'RESET_PASSWORD_WITHIN': '5 days',
|
||||
@@ -84,7 +86,8 @@ _default_config = {
|
||||
'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions',
|
||||
'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset',
|
||||
'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed',
|
||||
'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions'
|
||||
'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions',
|
||||
'USER_IDENTITY_ATTRIBUTES': ['email']
|
||||
}
|
||||
|
||||
#: Default Flask-Security messages
|
||||
@@ -111,11 +114,13 @@ _default_messages = {
|
||||
'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'),
|
||||
'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'),
|
||||
'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'),
|
||||
'PASSWORD_NOT_SET': ('No password is set for this user', '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'),
|
||||
'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'),
|
||||
'PASSWORD_IS_THE_SAME': ('Your new password must be different than your previous password.', 'error'),
|
||||
'PASSWORD_CHANGE': ('You successfully changed your password.', 'success'),
|
||||
'LOGIN': ('Please log in to access this page.', 'info'),
|
||||
'REFRESH': ('Please reauthenticate to access this page.', 'info'),
|
||||
|
||||
+34
-18
@@ -9,6 +9,8 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from .utils import get_identity_attributes
|
||||
|
||||
|
||||
class Datastore(object):
|
||||
def __init__(self, db):
|
||||
@@ -179,14 +181,14 @@ 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 get_user(self, identifier):
|
||||
if self._is_numeric(identifier):
|
||||
return self.user_model.query.get(identifier)
|
||||
for attr in get_identity_attributes():
|
||||
query = getattr(self.user_model, attr).ilike(identifier)
|
||||
rv = self.user_model.query.filter(query).first()
|
||||
if rv is not None:
|
||||
return rv
|
||||
|
||||
def _is_numeric(self, value):
|
||||
try:
|
||||
@@ -210,12 +212,18 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
|
||||
MongoEngineDatastore.__init__(self, db)
|
||||
UserDatastore.__init__(self, user_model, role_model)
|
||||
|
||||
def get_user(self, id_or_email):
|
||||
def get_user(self, identifier):
|
||||
from mongoengine import ValidationError
|
||||
try:
|
||||
return self.user_model.objects(id=id_or_email).first()
|
||||
return self.user_model.objects(id=identifier).first()
|
||||
except ValidationError:
|
||||
return self.user_model.objects(email__iexact=id_or_email).first()
|
||||
pass
|
||||
for attr in get_identity_attributes():
|
||||
query_key = '%s__iexact' % attr
|
||||
query = {query_key: identifier}
|
||||
rv = self.user_model.objects(**query).first()
|
||||
if rv is not None:
|
||||
return rv
|
||||
|
||||
def find_user(self, **kwargs):
|
||||
try:
|
||||
@@ -234,6 +242,12 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
|
||||
def find_role(self, role):
|
||||
return self.role_model.objects(name=role).first()
|
||||
|
||||
def add_role_to_user(self, user, role):
|
||||
rv = super(MongoEngineUserDatastore, self).add_role_to_user(user, role)
|
||||
if rv:
|
||||
self.put(user)
|
||||
return rv
|
||||
|
||||
|
||||
class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
||||
"""A PeeweeD datastore implementation for Flask-Security that assumes
|
||||
@@ -248,16 +262,18 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
||||
UserDatastore.__init__(self, user_model, role_model)
|
||||
self.UserRole = role_link
|
||||
|
||||
def get_user(self, id_or_email):
|
||||
def get_user(self, identifier):
|
||||
try:
|
||||
return self.user_model.get(self.user_model.id == id_or_email)
|
||||
return self.user_model.get(self.user_model.id == identifier)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return self.user_model.get(self.user_model.email ** id_or_email)
|
||||
except self.user_model.DoesNotExist:
|
||||
pass
|
||||
return None
|
||||
|
||||
for attr in get_identity_attributes():
|
||||
column = getattr(self.user_model, attr)
|
||||
try:
|
||||
return self.user_model.get(column ** identifier)
|
||||
except self.user_model.DoesNotExist:
|
||||
pass
|
||||
|
||||
def find_user(self, **kwargs):
|
||||
try:
|
||||
|
||||
@@ -51,7 +51,7 @@ def _check_token():
|
||||
args_key = _security.token_authentication_key
|
||||
header_token = request.headers.get(header_key, None)
|
||||
token = request.args.get(args_key, header_token)
|
||||
if request.json:
|
||||
if request.get_json(silent=True):
|
||||
token = request.json.get(args_key, token)
|
||||
|
||||
user = _security.login_manager.token_callback(token)
|
||||
|
||||
+15
-6
@@ -22,7 +22,7 @@ from flask_login import current_user
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .confirmable import requires_confirmation
|
||||
from .utils import verify_and_update_password, get_message
|
||||
from .utils import verify_and_update_password, get_message, encrypt_password, config_value
|
||||
|
||||
# Convenient reference
|
||||
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
|
||||
@@ -136,11 +136,12 @@ class NextFormMixin():
|
||||
next = HiddenField()
|
||||
|
||||
def validate_next(self, field):
|
||||
url_next = urlparse.urlsplit(field.data)
|
||||
url_base = urlparse.urlsplit(request.host_url)
|
||||
if url_next.netloc and url_next.netloc != url_base.netloc:
|
||||
field.data = ''
|
||||
raise ValidationError(get_message('INVALID_REDIRECT')[0])
|
||||
if field.data:
|
||||
url_next = urlparse.urlsplit(field.data)
|
||||
url_base = urlparse.urlsplit(request.host_url)
|
||||
if url_next.netloc and url_next.netloc != url_base.netloc:
|
||||
field.data = ''
|
||||
raise ValidationError(get_message('INVALID_REDIRECT')[0])
|
||||
|
||||
|
||||
class RegisterFormMixin():
|
||||
@@ -207,6 +208,7 @@ class LoginForm(Form, NextFormMixin):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoginForm, self).__init__(*args, **kwargs)
|
||||
self.remember.default = config_value('DEFAULT_REMEMBER_ME')
|
||||
|
||||
def validate(self):
|
||||
if not super(LoginForm, self).validate():
|
||||
@@ -220,11 +222,15 @@ class LoginForm(Form, NextFormMixin):
|
||||
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
|
||||
return False
|
||||
|
||||
|
||||
self.user = _datastore.get_user(self.email.data)
|
||||
|
||||
if self.user is None:
|
||||
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
|
||||
return False
|
||||
if not self.user.password:
|
||||
self.password.errors.append(get_message('PASSWORD_NOT_SET')[0])
|
||||
return False
|
||||
if not verify_and_update_password(self.password.data, self.user):
|
||||
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
|
||||
return False
|
||||
@@ -275,4 +281,7 @@ class ChangePasswordForm(Form, PasswordFormMixin):
|
||||
if not verify_and_update_password(self.password.data, current_user):
|
||||
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
|
||||
return False
|
||||
if self.password.data.strip() == self.new_password.data.strip():
|
||||
self.password.errors.append(get_message('PASSWORD_IS_THE_SAME')[0])
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -44,8 +44,9 @@ def send_password_reset_notice(user):
|
||||
|
||||
:param user: The user to send the notice to
|
||||
"""
|
||||
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
|
||||
'reset_notice', user=user)
|
||||
if config_value('SEND_PASSWORD_RESET_NOTICE_EMAIL'):
|
||||
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
|
||||
'reset_notice', user=user)
|
||||
|
||||
|
||||
def generate_reset_password_token(user):
|
||||
|
||||
+45
-1
@@ -39,7 +39,11 @@ _pwd_context = LocalProxy(lambda: _security.pwd_context)
|
||||
|
||||
|
||||
def login_user(user, remember=None):
|
||||
"""Performs the login and sends the appropriate signal."""
|
||||
"""Performs the login routine.
|
||||
|
||||
:param user: The user to login
|
||||
:param remember: Flag specifying if the remember cookie should be set. Defaults to ``False``
|
||||
"""
|
||||
|
||||
if remember is None:
|
||||
remember = config_value('DEFAULT_REMEMBER_ME')
|
||||
@@ -66,6 +70,8 @@ def login_user(user, remember=None):
|
||||
|
||||
|
||||
def logout_user():
|
||||
"""Logs out the current. This will also clean up the remember me cookie if it exists."""
|
||||
|
||||
for key in ('identity.name', 'identity.auth_type'):
|
||||
session.pop(key, None)
|
||||
identity_changed.send(current_app._get_current_object(),
|
||||
@@ -74,6 +80,11 @@ def logout_user():
|
||||
|
||||
|
||||
def get_hmac(password):
|
||||
"""Returns a Base64 encoded HMAC+SHA512 of the password signed with the salt specified
|
||||
by ``SECURITY_PASSWORD_SALT``.
|
||||
|
||||
:param password: The password to sign
|
||||
"""
|
||||
if _security.password_hash == 'plaintext':
|
||||
return password
|
||||
|
||||
@@ -88,10 +99,21 @@ def get_hmac(password):
|
||||
|
||||
|
||||
def verify_password(password, password_hash):
|
||||
"""Returns ``True`` if the password matches the supplied hash.
|
||||
|
||||
:param password: A plaintext password to verify
|
||||
:param password_hash: The expected hash value of the password (usually form your database)
|
||||
"""
|
||||
return _pwd_context.verify(get_hmac(password), password_hash)
|
||||
|
||||
|
||||
def verify_and_update_password(password, user):
|
||||
"""Returns ``True`` if the password is valid for the specified user. Additionally, the hashed
|
||||
password in the database is updated if the hashing algorithm happens to have changed.
|
||||
|
||||
:param password: A plaintext password to verify
|
||||
:param user: The user to verify against
|
||||
"""
|
||||
verified, new_password = _pwd_context.verify_and_update(get_hmac(password), user.password)
|
||||
if verified and new_password:
|
||||
user.password = new_password
|
||||
@@ -100,6 +122,10 @@ def verify_and_update_password(password, user):
|
||||
|
||||
|
||||
def encrypt_password(password):
|
||||
"""Encrypts the specified plaintext password using the configured encryption options.
|
||||
|
||||
:param password: The plaintext passwrod to encrypt
|
||||
"""
|
||||
return _pwd_context.encrypt(get_hmac(password))
|
||||
|
||||
|
||||
@@ -259,6 +285,14 @@ def send_mail(subject, recipient, template, **context):
|
||||
|
||||
|
||||
def get_token_status(token, serializer, max_age=None):
|
||||
"""Get the status of a token.
|
||||
|
||||
:param token: The token to check
|
||||
:param serializer: The name of the seriailzer. Can be one of the
|
||||
following: ``confirm``, ``login``, ``reset``
|
||||
:param max_age: The name of the max age config option. Can be on of
|
||||
the following: ``CONFIRM_EMAIL``, ``LOGIN``, ``RESET_PASSWORD``
|
||||
"""
|
||||
serializer = getattr(_security, serializer + '_serializer')
|
||||
max_age = get_max_age(max_age)
|
||||
user, data = None, None
|
||||
@@ -279,6 +313,16 @@ def get_token_status(token, serializer, max_age=None):
|
||||
return expired, invalid, user
|
||||
|
||||
|
||||
def get_identity_attributes(app=None):
|
||||
app = app or current_app
|
||||
attrs = app.config['SECURITY_USER_IDENTITY_ATTRIBUTES']
|
||||
try:
|
||||
attrs = [f.strip() for f in attrs.split(',')]
|
||||
except AttributeError:
|
||||
pass
|
||||
return attrs
|
||||
|
||||
|
||||
@contextmanager
|
||||
def capture_passwordless_login_requests():
|
||||
login_requests = []
|
||||
|
||||
@@ -123,6 +123,7 @@ def register():
|
||||
|
||||
if not request.json:
|
||||
return redirect(get_post_register_redirect())
|
||||
return _render_json(form, True)
|
||||
|
||||
if request.json:
|
||||
return _render_json(form)
|
||||
@@ -301,9 +302,10 @@ def change_password():
|
||||
get_url(_security.post_login_view))
|
||||
|
||||
if request.json:
|
||||
form.user = current_user
|
||||
return _render_json(form)
|
||||
|
||||
return _security.render_template('security/change_password.html',
|
||||
return _security.render_template(config_value('CHANGE_PASSWORD_TEMPLATE'),
|
||||
change_password_form=form,
|
||||
**_ctx('change_password'))
|
||||
|
||||
|
||||
@@ -352,6 +352,16 @@ class LoginWithoutImmediateConfirmTests(SecurityTest):
|
||||
self.assertIn(msg, r.data)
|
||||
self.assertIn('Hello %s' % e2, r.data)
|
||||
|
||||
def test_login_unconfirmed_user_when_login_without_confirmation_is_true(self):
|
||||
e = 'dude@lp.com'
|
||||
p = 'password'
|
||||
data = dict(email=e, password=p, password_confirm=p)
|
||||
r = self._post('/register', data=data, follow_redirects=True)
|
||||
self.assertIn(e, r.data)
|
||||
self.client.get('/logout')
|
||||
r = self.authenticate(email=e)
|
||||
self.assertIn(e, r.data)
|
||||
|
||||
|
||||
class RecoverableTests(SecurityTest):
|
||||
|
||||
@@ -474,6 +484,16 @@ class ChangePasswordTest(SecurityTest):
|
||||
self.assertNotIn('You successfully changed your password', r.data)
|
||||
self.assertIn('Password must be at least 6 characters', r.data)
|
||||
|
||||
def test_change_password_same_as_previous(self):
|
||||
self.authenticate()
|
||||
r = self._post('/change', data={
|
||||
'password': 'password',
|
||||
'new_password': 'password',
|
||||
'new_password_confirm': 'password'
|
||||
}, follow_redirects=True)
|
||||
self.assertNotIn('You successfully changed your password', r.data)
|
||||
self.assertIn('Your new password must be different than your previous password.', r.data)
|
||||
|
||||
def test_change_password_success(self):
|
||||
data = {
|
||||
'password': 'password',
|
||||
@@ -816,3 +836,14 @@ class ConfirmableExtendFormsTest(SecurityTest):
|
||||
def test_send_confirmation(self):
|
||||
r = self._get('/confirm', follow_redirects=True)
|
||||
self.assertIn("My Send Confirmation Email Address Field", r.data)
|
||||
|
||||
|
||||
class AdditionalUserIdentityAttributes(SecurityTest):
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username')
|
||||
}
|
||||
|
||||
def test_authenticate(self):
|
||||
r = self.authenticate(email='matt')
|
||||
self.assertIn('Hello matt@lp.com', r.data)
|
||||
|
||||
@@ -128,17 +128,17 @@ def create_roles():
|
||||
|
||||
|
||||
def create_users(count=None):
|
||||
users = [('matt@lp.com', 'password', ['admin'], True),
|
||||
('joe@lp.com', 'password', ['editor'], True),
|
||||
('dave@lp.com', 'password', ['admin', 'editor'], True),
|
||||
('jill@lp.com', 'password', ['author'], True),
|
||||
('tiya@lp.com', 'password', [], False)]
|
||||
users = [('matt@lp.com', 'matt', 'password', ['admin'], True),
|
||||
('joe@lp.com', 'joe', 'password', ['editor'], True),
|
||||
('dave@lp.com', 'dave', 'password', ['admin', 'editor'], True),
|
||||
('jill@lp.com', 'jill', 'password', ['author'], True),
|
||||
('tiya@lp.com', 'tiya', 'password', [], False)]
|
||||
count = count or len(users)
|
||||
|
||||
for u in users[:count]:
|
||||
pw = encrypt_password(u[1])
|
||||
ds.create_user(email=u[0], password=pw,
|
||||
roles=u[2], active=u[3])
|
||||
pw = encrypt_password(u[2])
|
||||
ds.create_user(email=u[0], username=u[1], password=pw,
|
||||
roles=u[3], active=u[4])
|
||||
ds.commit()
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ def create_app(config, **kwargs):
|
||||
|
||||
class User(db.Document, UserMixin):
|
||||
email = db.StringField(unique=True, max_length=255)
|
||||
username = db.StringField(max_length=255)
|
||||
password = db.StringField(required=True, max_length=255)
|
||||
last_login_at = db.DateTimeField()
|
||||
current_login_at = db.DateTimeField()
|
||||
|
||||
@@ -29,6 +29,7 @@ def create_app(config, **kwargs):
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
email = TextField()
|
||||
username = TextField()
|
||||
password = TextField()
|
||||
last_login_at = DateTimeField(null=True)
|
||||
current_login_at = DateTimeField(null=True)
|
||||
|
||||
@@ -32,6 +32,7 @@ def create_app(config, **kwargs):
|
||||
class User(db.Model, UserMixin):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(255), unique=True)
|
||||
username = db.Column(db.String(255))
|
||||
password = db.Column(db.String(255))
|
||||
last_login_at = db.Column(db.DateTime())
|
||||
current_login_at = db.Column(db.DateTime())
|
||||
|
||||
Reference in New Issue
Block a user