mirror of
https://github.com/wassname/flask-security.git
synced 2026-08-02 12:40:33 +08:00
Merge https://github.com/mattupstate/flask-security into develop
Conflicts: flask_security/views.py
This commit is contained in:
@@ -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'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user