From 510d1356a2c2884c5895c321d1c8a65e1269b117 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Fri, 14 Mar 2014 15:26:53 -0400 Subject: [PATCH] A bunch of adjustments to satisfy existing tests and even some new ones --- flask_security/core.py | 36 +++++++++++------------ flask_security/datastore.py | 16 +++++----- flask_security/forms.py | 23 +++++---------- flask_security/utils.py | 58 +++++++++++++++++++++++++------------ flask_security/views.py | 8 ++--- 5 files changed, 77 insertions(+), 64 deletions(-) diff --git a/flask_security/core.py b/flask_security/core.py index 5f8682d..b719d13 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -87,7 +87,18 @@ _default_config = { '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', - 'USER_IDENTITY_ATTRIBUTES': ['email'] + 'USER_IDENTITY_ATTRIBUTES': ['email'], + 'PASSWORD_SCHEMES': [ + 'bcrypt', + 'des_crypt', + 'pbkdf2_sha256', + 'pbkdf2_sha512', + 'sha256_crypt', + 'sha512_crypt', + # And always last one... + 'plaintext' + ], + 'DEPRECATED_PASSWORD_SCHEMES': ['auto'] } #: Default Flask-Security messages @@ -162,17 +173,6 @@ _default_messages = { 'Please reauthenticate to access this page.', 'info'), } -_allowed_password_hash_schemes = [ - 'bcrypt', - 'des_crypt', - 'pbkdf2_sha256', - 'pbkdf2_sha512', - 'sha256_crypt', - 'sha512_crypt', - # And always last one... - 'plaintext' -] - _default_forms = { 'login_form': LoginForm, 'confirm_register_form': ConfirmRegisterForm, @@ -242,11 +242,12 @@ def _get_principal(app): def _get_pwd_context(app): pw_hash = cv('PASSWORD_HASH', app=app) - if pw_hash not in _allowed_password_hash_schemes: - allowed = (', '.join(_allowed_password_hash_schemes[:-1]) + - ' and ' + _allowed_password_hash_schemes[-1]) + schemes = cv('PASSWORD_SCHEMES', app=app) + deprecated = cv('DEPRECATED_PASSWORD_SCHEMES', app=app) + if pw_hash not in schemes: + allowed = (', '.join(schemes[:-1]) + ' and ' + schemes[-1]) raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed)) - return CryptContext(schemes=_allowed_password_hash_schemes, default=pw_hash) + return CryptContext(schemes=schemes, default=pw_hash, deprecated=deprecated) def _get_serializer(app, name): @@ -345,9 +346,6 @@ class _SecurityState(object): rv.update(fn()) return rv - def context_processor(self, fn): - self._add_ctx_processor(None, fn) - def forgot_password_context_processor(self, fn): self._add_ctx_processor('forgot_password', fn) diff --git a/flask_security/datastore.py b/flask_security/datastore.py index 8aa4adc..fdd4294 100644 --- a/flask_security/datastore.py +++ b/flask_security/datastore.py @@ -237,17 +237,18 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore): query = QCombination(QCombination.AND, queries) try: return self.user_model.objects(query).first() - except ValidationError: + except ValidationError: # pragma: no cover return None 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 + # TODO: Not sure why this was added but tests pass without it + # 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): @@ -295,6 +296,7 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore): user = self.put(user) for role in roles: self.add_role_to_user(user, role) + self.put(user) return user def add_role_to_user(self, user, role): @@ -309,7 +311,7 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore): if result.count(): return False else: - self.UserRole.create(user=user.id, role=role.id) + self.put(self.UserRole.create(user=user.id, role=role.id)) return True def remove_role_from_user(self, user, role): diff --git a/flask_security/forms.py b/flask_security/forms.py index a913bae..44db7a7 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -10,12 +10,8 @@ """ import inspect -try: - from urlparse import urlsplit -except ImportError: - from urllib.parse import urlsplit -from flask import request, current_app +from flask import request, current_app, flash from flask_wtf import Form as BaseForm from wtforms import TextField, PasswordField, validators, \ SubmitField, HiddenField, BooleanField, ValidationError, Field @@ -23,7 +19,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, config_value +from .utils import verify_and_update_password, get_message, config_value, validate_redirect_url # Convenient reference _datastore = LocalProxy(lambda: current_app.extensions['security'].datastore) @@ -137,12 +133,10 @@ class NextFormMixin(): next = HiddenField() def validate_next(self, field): - if field.data: - url_next = urlsplit(field.data) - url_base = 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 and not validate_redirect_url(field.data): + field.data = '' + flash(*get_message('INVALID_REDIRECT')) + raise ValidationError(get_message('INVALID_REDIRECT')[0]) class RegisterFormMixin(): @@ -209,6 +203,8 @@ class LoginForm(Form, NextFormMixin): def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) + if not self.next.data: + self.next.data = request.args.get('next', '') self.remember.default = config_value('DEFAULT_REMEMBER_ME') def validate(self): @@ -275,9 +271,6 @@ class ChangePasswordForm(Form, PasswordFormMixin): if not super(ChangePasswordForm, self).validate(): return False - if self.password.data.strip() == '': - self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0]) - return False if not verify_and_update_password(self.password.data, current_user): self.password.errors.append(get_message('INVALID_PASSWORD')[0]) return False diff --git a/flask_security/utils.py b/flask_security/utils.py index 38375da..eea0e4d 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -14,6 +14,11 @@ import hashlib import hmac import sys +try: + from urlparse import urlsplit +except ImportError: # pragma: no cover + from urllib.parse import urlsplit + from contextlib import contextmanager from datetime import datetime, timedelta @@ -36,11 +41,11 @@ _pwd_context = LocalProxy(lambda: _security.pwd_context) PY3 = sys.version_info[0] == 3 if PY3: - string_types = str, - text_type = str + string_types = str, # pragma: no cover, no flakes + text_type = str # pragma: no cover, no flakes else: - string_types = basestring, - text_type = unicode + string_types = basestring, # pragma: no cover, no flakes + text_type = unicode # pragma: no cover, no flakes def login_user(user, remember=None): @@ -53,7 +58,7 @@ def login_user(user, remember=None): if remember is None: remember = config_value('DEFAULT_REMEMBER_ME') - if not _login_user(user, remember): + if not _login_user(user, remember): # pragma: no cover return False if _security.trackable: @@ -119,13 +124,16 @@ def verify_and_update_password(password, user): :param password: A plaintext password to verify :param user: The user to verify against """ - - if _security.password_hash != 'plaintext': + print _pwd_context.default_scheme() + print password, user.password + if _pwd_context.identify(user.password) != 'plaintext': password = get_hmac(password) verified, new_password = _pwd_context.verify_and_update(password, user.password) + print verified, new_password if verified and new_password: user.password = new_password _datastore.put(user) + return verified @@ -186,14 +194,32 @@ def url_for_security(endpoint, **values): return url_for(endpoint, **values) -def get_post_action_redirect(config_key): - return (get_url(request.args.get('next')) or - get_url(request.form.get('next')) or - find_redirect(config_key)) +def validate_redirect_url(url): + try: + url_next = urlsplit(url) + except: + return False + url_base = urlsplit(request.host_url) + if url_next.netloc and url_next.netloc != url_base.netloc: + return False + return True -def get_post_login_redirect(): - return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW') +def get_post_action_redirect(config_key, declared=None): + urls = [ + get_url(request.args.get('next')), + get_url(request.form.get('next')), + find_redirect(config_key) + ] + if declared: + urls.append(declared) + for url in urls: + if validate_redirect_url(url): + return url + + +def get_post_login_redirect(declared=None): + return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW', declared) def get_post_register_redirect(): @@ -314,11 +340,7 @@ def get_token_status(token, serializer, max_age=None): except SignatureExpired: d, data = serializer.loads_unsafe(token) expired = True - except BadSignature: - invalid = True - except TypeError: - invalid = True - except ValueError: + except (BadSignature, TypeError, ValueError): invalid = True if data: diff --git a/flask_security/views.py b/flask_security/views.py index ec9f899..bf343d0 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -75,11 +75,8 @@ def login(): after_this_request(_commit) if not request.json: - return redirect(get_post_login_redirect()) - - form.next.data = (get_url(request.args.get('next')) or - get_url(request.form.get('next')) or - '') + rv = get_post_login_redirect(form.next.data) + return redirect(rv) if request.json: return _render_json(form, True) @@ -124,6 +121,7 @@ def register(): login_user(user) if not request.json: + print('wtf') return redirect(get_post_register_redirect()) return _render_json(form, True)