From 815c8695fc07be0f16a23f7d30c016089af826f6 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Tue, 10 Jul 2012 17:24:39 -0400 Subject: [PATCH] Code clean upr --- flask_security/confirmable.py | 4 +- flask_security/core.py | 151 ++++++++++++++-------------------- flask_security/recoverable.py | 4 +- flask_security/utils.py | 58 +++++++++---- flask_security/views.py | 2 +- tests/functional_tests.py | 2 +- 6 files changed, 109 insertions(+), 112 deletions(-) diff --git a/flask_security/confirmable.py b/flask_security/confirmable.py index a160d21..7d87220 100644 --- a/flask_security/confirmable.py +++ b/flask_security/confirmable.py @@ -15,7 +15,7 @@ from flask import current_app as app, request, url_for from werkzeug.local import LocalProxy from .exceptions import UserNotFoundError, ConfirmationError, TokenExpiredError -from .utils import generate_token, send_mail +from .utils import generate_token, send_mail, get_within_delta from .signals import user_confirmed, confirm_instructions_sent @@ -96,7 +96,7 @@ def requires_confirmation(user): @should_confirm_email def confirmation_token_is_expired(user): """Returns `True` if the user's confirmation token is expired.""" - token_expires = datetime.utcnow() - _security.confirm_email_within + token_expires = datetime.utcnow() - get_within_delta('CONFIRM_EMAIL_WITHIN') return user.confirmation_sent_at < token_expires diff --git a/flask_security/core.py b/flask_security/core.py index 922ed12..0d5ef20 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -9,8 +9,6 @@ :license: MIT, see LICENSE for more details. """ -from datetime import timedelta - from flask import current_app, Blueprint from flask.ext.login import AnonymousUser as AnonymousUserBase, \ UserMixin as BaseUserMixin, LoginManager, current_user @@ -19,10 +17,11 @@ from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \ from passlib.context import CryptContext from werkzeug.datastructures import ImmutableList -from . import views, exceptions, utils +from . import views, exceptions from .confirmable import confirmation_token_is_expired, requires_confirmation, \ reset_confirmation_token from .decorators import login_required +from .utils import config_value as cv, get_config #: Default Flask-Security configuration @@ -30,7 +29,6 @@ _default_config = { 'URL_PREFIX': None, 'FLASH_MESSAGES': True, 'PASSWORD_HASH': 'plaintext', - 'AUTH_PROVIDER': 'flask.ext.security::AuthenticationProvider', 'AUTH_URL': '/auth', 'LOGOUT_URL': '/logout', 'REGISTER_URL': '/register', @@ -136,6 +134,57 @@ def _on_identity_loaded(sender, identity): identity.user = current_user +def _get_login_manager(app): + lm = LoginManager() + lm.anonymous_user = AnonymousUser + lm.login_view = cv('LOGIN_VIEW', app=app) + lm.user_loader(_user_loader) + lm.token_loader(_token_loader) + lm.init_app(app) + return lm + + +def _get_principal(app): + p = Principal(app, use_sessions=False) + p.identity_loader(_identity_loader) + return p + + +def _get_pwd_context(app): + pw_hash = cv('PASSWORD_HASH', app=app) + return CryptContext(schemes=[pw_hash], default=pw_hash) + + +def _create_blueprint(config): + bp = Blueprint('flask_security', __name__, template_folder='templates') + + bp.route(config['SECURITY_AUTH_URL'], + methods=['POST'], + endpoint='authenticate')(views.authenticate) + + bp.route(config['SECURITY_LOGOUT_URL'], + endpoint='logout')(login_required(views.logout)) + + if config['SECURITY_REGISTERABLE']: + bp.route(config['SECURITY_REGISTER_URL'], + methods=['POST'], + endpoint='register')(views.register) + + if config['SECURITY_RECOVERABLE']: + bp.route(config['SECURITY_FORGOT_URL'], + methods=['POST'], + endpoint='forgot')(views.forgot) + bp.route(config['SECURITY_RESET_URL'], + methods=['POST'], + endpoint='reset')(views.reset) + + if config['SECURITY_CONFIRMABLE']: + bp.route(config['SECURITY_CONFIRM_URL'], + endpoint='confirm')(views.confirm) + + return bp + + class Security(object): """The :class:`Security` class initializes the Flask-Security extension. @@ -151,10 +200,6 @@ class Security(object): :param app: The application. :param datastore: An instance of a user datastore. - :param confirmable: Set to `True` to enable email confirmation - :param registerable: Set to `False` to disable registration endpoints - :param recoverable: Set to `False` to disable password recovery - endpoints """ if app is None or datastore is None: return @@ -162,96 +207,22 @@ class Security(object): for key, value in _default_config.items(): app.config.setdefault('SECURITY_' + key, value) - login_manager = LoginManager() - login_manager.anonymous_user = AnonymousUser - login_manager.login_view = utils.config_value(app, 'LOGIN_VIEW') - login_manager.user_loader(_user_loader) - login_manager.token_loader(_token_loader) - login_manager.init_app(app) - - Provider = utils.get_class_from_string(app, 'AUTH_PROVIDER') - pw_hash = utils.config_value(app, 'PASSWORD_HASH') - - self.login_manager = login_manager - - self.pwd_context = CryptContext(schemes=[pw_hash], default=pw_hash) - - self.auth_provider = Provider() - - self.principal = Principal(app, use_sessions=False) - self.principal.identity_loader(_identity_loader) - self.datastore = datastore + self.auth_provider = AuthenticationProvider() + self.login_manager = _get_login_manager(app) + self.principal = _get_principal(app) + self.pwd_context = _get_pwd_context(app) - self.auth_url = utils.config_value(app, 'AUTH_URL') - self.logout_url = utils.config_value(app, 'LOGOUT_URL') - self.reset_url = utils.config_value(app, 'RESET_URL') - self.register_url = utils.config_value(app, 'REGISTER_URL') - self.confirm_url = utils.config_value(app, 'CONFIRM_URL') - self.forgot_url = utils.config_value(app, 'FORGOT_URL') - - self.post_login_view = utils.config_value(app, 'POST_LOGIN_VIEW') - self.post_logout_view = utils.config_value(app, 'POST_LOGOUT_VIEW') - self.post_register_view = utils.config_value(app, 'POST_REGISTER_VIEW') - self.post_confirm_view = utils.config_value(app, 'POST_CONFIRM_VIEW') - self.post_forgot_view = utils.config_value(app, 'POST_FORGOT_VIEW') - self.reset_password_error_view = utils.config_value(app, 'RESET_PASSWORD_ERROR_VIEW') - - self.default_roles = utils.config_value(app, "DEFAULT_ROLES") - self.login_without_confirmation = utils.config_value(app, 'LOGIN_WITHOUT_CONFIRMATION') - self.confirmable = utils.config_value(app, 'CONFIRMABLE') - self.registerable = utils.config_value(app, 'REGISTERABLE') - self.recoverable = utils.config_value(app, 'RECOVERABLE') - self.trackable = utils.config_value(app, 'TRACKABLE') - self.email_sender = utils.config_value(app, 'EMAIL_SENDER') - self.token_authentication_key = utils.config_value(app, 'TOKEN_AUTHENTICATION_KEY') - self.token_authentication_header = utils.config_value(app, 'TOKEN_AUTHENTICATION_HEADER') - - self.confirm_email_within_text = utils.config_value(app, 'CONFIRM_EMAIL_WITHIN') - values = self.confirm_email_within_text.split() - self.confirm_email_within = timedelta(**{values[1]: int(values[0])}) - - self.reset_password_within_text = utils.config_value(app, 'RESET_PASSWORD_WITHIN') - values = self.reset_password_within_text.split() - self.reset_password_within = timedelta(**{values[1]: int(values[0])}) + for key, value in get_config(app).items(): + setattr(self, key.lower(), value) identity_loaded.connect_via(app)(_on_identity_loaded) - bp = Blueprint('flask_security', __name__, template_folder='templates') - - bp.route(self.auth_url, - methods=['POST'], - endpoint='authenticate')(views.authenticate) - - bp.route(self.logout_url, - endpoint='logout')(login_required(views.logout)) - - self._setup_registerable(bp) if self.registerable else None - self._setup_recoverable(bp) if self.recoverable else None - self._setup_confirmable(bp) if self.confirmable else None - - app.register_blueprint(bp, - url_prefix=utils.config_value(app, 'URL_PREFIX')) + app.register_blueprint(_create_blueprint(app.config), + url_prefix=cv('URL_PREFIX', app=app)) app.security = self - def _setup_registerable(self, bp): - bp.route(self.register_url, - methods=['POST'], - endpoint='register')(views.register) - - def _setup_recoverable(self, bp): - bp.route(self.forgot_url, - methods=['POST'], - endpoint='forgot')(views.forgot) - bp.route(self.reset_url, - methods=['POST'], - endpoint='reset')(views.reset) - - def _setup_confirmable(self, bp): - bp.route(self.confirm_url, - endpoint='confirm')(views.confirm) - class AuthenticationProvider(object): """The default authentication provider implementation.""" diff --git a/flask_security/recoverable.py b/flask_security/recoverable.py index 617cce9..3fb319d 100644 --- a/flask_security/recoverable.py +++ b/flask_security/recoverable.py @@ -18,7 +18,7 @@ from .exceptions import ResetPasswordError, UserNotFoundError, \ TokenExpiredError from .signals import password_reset, password_reset_requested, \ confirm_instructions_sent -from .utils import generate_token, send_mail +from .utils import generate_token, send_mail, get_within_delta # Convenient references @@ -87,7 +87,7 @@ def password_reset_token_is_expired(user): :param user: The user to examine """ - token_expires = datetime.utcnow() - _security.reset_password_within + token_expires = datetime.utcnow() - get_within_delta('RESET_PASSWORD_WITHIN') return user.reset_password_sent_at < token_expires diff --git a/flask_security/utils.py b/flask_security/utils.py index 7d51ad5..e290290 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -12,7 +12,7 @@ import base64 import os from contextlib import contextmanager -from importlib import import_module +from datetime import timedelta from flask import url_for, flash, current_app, request, session, render_template from flask.ext.login import make_secure_token @@ -38,21 +38,10 @@ def do_flash(message, category=None): :param message: The flash message :param category: The flash message category """ - if config_value(current_app, 'FLASH_MESSAGES'): + if config_value('FLASH_MESSAGES'): flash(message, category) -def get_class_from_string(app, key): - """Get a reference to a class by its configuration key name. - - :param app: The application instance to work against - :param key: The configuration key to use - """ - cv = config_value(app, key).split('::') - cm = import_module(cv[0]) - return getattr(cm, cv[1]) - - def get_url(endpoint_or_url): """Returns a URL if a valid endpoint is found. Otherwise, returns the provided value. @@ -85,14 +74,51 @@ def find_redirect(key): return result -def config_value(app, key, default=None): +def get_config(app): + """Conveniently get the security configuration for the specified + application without the annoying 'SECURITY_' prefix. + + :param app: The application to inspect + """ + items = app.config.items() + prefix = 'SECURITY_' + + def strip_prefix(tup): + return (tup[0].replace('SECURITY_', ''), tup[1]) + + return dict([strip_prefix(i) for i in items if i[0].startswith(prefix)]) + + +def config_value(key, app=None, default=None): """Get a Flask-Security configuration value. - :param app: The application to retrieve the configuration from :param key: The configuration key without the prefix `SECURITY_` + :param app: An optional specific application to inspect. Defaults to Flask's + `current_app` :param default: An optional default value if the value is not set """ - return app.config.get('SECURITY_' + key.upper(), default) + app = app or current_app + return get_config(app).get(key.upper(), default) + + +def get_within_delta(key, app=None): + """Get a timedelta object from the application configuration following + the internal convention of:: + + + + Examples of valid config values:: + + 5 days + 10 minutes + + :param key: The config value key without the 'SECURITY_' prefix + :param app: Optional application to inspect. Defaults to Flask's + `current_app` + """ + txt = config_value(key, app=app) + values = txt.split() + return timedelta(**{values[1]: int(values[0])}) def send_mail(subject, recipient, template, context=None): diff --git a/flask_security/views.py b/flask_security/views.py index 423be77..9e60b93 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -152,7 +152,7 @@ def confirm(): msg = 'You did not confirm your email within %s. ' \ 'A new confirmation code has been sent to %s' % ( - _security.confirm_email_within_text, e.user.email) + _security.confirm_email_within, e.user.email) do_flash(msg, 'error') return redirect('/') # TODO: Don't redirect to root diff --git a/tests/functional_tests.py b/tests/functional_tests.py index 62725b4..2817ea3 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -213,7 +213,7 @@ class ConfirmableTests(SecurityTest): self.assertIn(e, outbox[0].html) self.assertNotIn(token, outbox[0].html) - expire_text = self.app.security.confirm_email_within_text + expire_text = self.app.security.confirm_email_within text = 'You did not confirm your email within %s' % expire_text self.assertIn(text, r.data)