Conflicts:
	flask_security/views.py
This commit is contained in:
Bruno Rocha
2013-12-23 13:55:13 -02:00
19 changed files with 290 additions and 80 deletions
+1 -1
View File
@@ -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):
+6 -1
View File
@@ -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
View File
@@ -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:
+1 -1
View File
@@ -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
View File
@@ -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
+3 -2
View File
@@ -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
View File
@@ -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 = []
+3 -1
View File
@@ -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'))