From 8d6a6e5c4dca51f0b38ac3f81551639e6fd1c710 Mon Sep 17 00:00:00 2001 From: Simon Coulton Date: Wed, 28 Jun 2017 11:29:17 +1000 Subject: [PATCH] feat: additional authenticated_callback on login decorator --- setup.py | 2 +- tests/watson/auth/test_decorators.py | 31 +++++++++++++- watson/auth/__init__.py | 2 +- watson/auth/authentication.py | 7 +++- watson/auth/decorators.py | 53 ++++++++++++++---------- watson/auth/listeners.py | 6 ++- watson/auth/views/debug/panels/auth.html | 52 +++++++++++------------ 7 files changed, 99 insertions(+), 54 deletions(-) diff --git a/setup.py b/setup.py index 3a3b46a..6f88bc8 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ def confirm(prompt): long_description=readme, author='Simon Coulton', - author_email='simon at bespohk.com', + author_email='simon@bespohk.com', license=license, classifiers=[ diff --git a/tests/watson/auth/test_decorators.py b/tests/watson/auth/test_decorators.py index 2b7df87..93ed042 100644 --- a/tests/watson/auth/test_decorators.py +++ b/tests/watson/auth/test_decorators.py @@ -7,7 +7,7 @@ from watson.framework import controllers, exceptions from tests.watson.auth import support from watson.auth.decorators import auth, login, logout, forgotten, reset -from watson.auth import authentication, managers +from watson.auth import managers from watson.validators import abc @@ -32,6 +32,14 @@ def __call__(self, value): return False +def authenticated_callback_success(user, request): + return True + + +def authenticated_callback_failure(user, request): + return False + + class SampleController(controllers.Action): @auth @@ -62,6 +70,14 @@ def unauthed_url_redirect(self): def login_redirect_action(self, form): return 'login' + @login(authenticated_callback=authenticated_callback_success) + def login_authenticated_callback_success(self): + return 'success' + + @login(authenticated_callback=authenticated_callback_failure) + def login_authenticated_callback_failure(self): + return 'success' + @auth(roles='admin', unauthorized_url='/unauthorized-test') def unauthorized_custom_url_action(self): pass @@ -162,6 +178,19 @@ def test_login_redirect_to_referrer(self): response = self.controller.login_redirect_action() assert response.headers['location'] == 'http://127.0.0.1/existing-url?to-here&and-here' + def test_authenticated_callback(self): + post_data = 'username=admin&password=test' + environ = sample_environ(REQUEST_METHOD='POST', + CONTENT_LENGTH=len(post_data)) + environ['wsgi.input'] = BufferedReader( + BytesIO(post_data.encode('utf-8'))) + self.controller.request = Request.from_environ(environ, 'watson.http.sessions.Memory') + self.controller.login_authenticated_callback_failure() + assert len(self.controller.flash_messages) + self.controller.flash_messages.clear() + self.controller.login_authenticated_callback_success() + assert not len(self.controller.flash_messages) + class TestLogout(object): def setup(self): diff --git a/watson/auth/__init__.py b/watson/auth/__init__.py index 4e507fb..869958f 100644 --- a/watson/auth/__init__.py +++ b/watson/auth/__init__.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = '3.3.0' +__version__ = '3.4.4' diff --git a/watson/auth/authentication.py b/watson/auth/authentication.py index 18ee8a4..9227225 100644 --- a/watson/auth/authentication.py +++ b/watson/auth/authentication.py @@ -16,7 +16,8 @@ def generate_password(password, rounds=10, encoding='utf-8'): mixed: The generated password and the salt used """ salt = bcrypt.gensalt(rounds) - return bcrypt.hashpw(password.encode(encoding), salt), salt + hashed_password = bcrypt.hashpw(password.encode(encoding), salt) + return hashed_password.decode(encoding), salt.decode(encoding) def check_password(password, existing_password, salt, encoding='utf-8'): @@ -31,6 +32,10 @@ def check_password(password, existing_password, salt, encoding='utf-8'): Returns: boolean: True/False if valid or invalid """ + if isinstance(salt, str): + salt = salt.encode(encoding) + if isinstance(existing_password, str): + existing_password = existing_password.encode(encoding) return bcrypt.hashpw(password.encode(encoding), salt) == existing_password diff --git a/watson/auth/decorators.py b/watson/auth/decorators.py index 8ca1798..3e38488 100644 --- a/watson/auth/decorators.py +++ b/watson/auth/decorators.py @@ -85,19 +85,29 @@ def wrapper(self, *args, **kwargs): return decorator -def login(func=None, method='POST', form_class=None, auto_redirect=True): +def login( + func=None, + method='POST', + form_class=None, + auto_redirect=True, + authenticated_callback=None): """Attempts to authenticate a user if the required fields have been posted. By setting auto_redirect to False, the user roles and permissions can be checked within the login route and redirected from there. Args: - func (callable): the function that is being wrapped + func (callable): The function that is being wrapped method (string): The HTTP method that authentication will be performed against. form_class (string): The qualified class name of the form. auto_redirect (boolean): Whether or not to automatically redirect to a different url on successful login. + authenticated_callback (callable): An additional callback that can be + used to authenticate the user after + a valid user record has been found. + Takes user and request objects as + arguments. Example: @@ -134,19 +144,21 @@ def wrapper(self, *args, **kwargs): user = authenticator.authenticate( username=username_field, password=password_field) - if user: + if not user: + valid = False + if valid and authenticated_callback and not authenticated_callback(user, self.request): + valid = False + if valid and user: authenticator.assign_user_to_session( user, self.request, auth_config['session']['key']) - if auto_redirect: - redirect_url = login_config['urls']['success'] - if self.request.get['redirect']: - redirect_url = parse.unquote_plus( - self.request.get['redirect']) - return self.redirect(redirect_url, clear=True) - else: - valid = False + if valid and user and auto_redirect: + redirect_url = login_config['urls']['success'] + if self.request.get['redirect']: + redirect_url = parse.unquote_plus( + self.request.get['redirect']) + return self.redirect(redirect_url, clear=True) else: valid = False if not valid: @@ -285,24 +297,23 @@ def wrapper(self, *args, **kwargs): form.data = request if form.is_valid(): self.flash_messages.add( - form_config['messages']['success'], 'error') + form_config['messages']['success'], 'success') redirect_url = reset_config['urls']['success'] auto_login = authenticate_on_reset or reset_config['authenticate_on_reset'] - if auto_login: - authenticator.assign_user_to_session( - token.user, - request, - auth_config['session']['key']) - forgotten_password_token_manager.update_user_password( - token, form.password) - redirect_url = auth_config['login']['urls']['success'] + forgotten_password_token_manager.update_user_password( + token, form.password) forgotten_password_token_manager.notify_user( token.user, request=self.request, subject=auth_config['reset_password']['subject_line'], template=auth_config['reset_password']['template'], password=form.password) - forgotten_password_token_manager.delete_token(token) + if auto_login: + authenticator.assign_user_to_session( + token.user, + request, + auth_config['session']['key']) + redirect_url = auth_config['login']['urls']['success'] return self.redirect(redirect_url, clear=True) else: self.flash_messages.add( diff --git a/watson/auth/listeners.py b/watson/auth/listeners.py index ddd74d7..d36754e 100644 --- a/watson/auth/listeners.py +++ b/watson/auth/listeners.py @@ -48,9 +48,11 @@ def setup_config(self): self.app_config['auth'] = auth_config self.container.get('jinja2_renderer').add_package_loader( 'watson.auth', 'views') + router = self.container.get('router') for route, definition in config.routes.items(): - definition['name'] = route - self.container.get('router').add_definition(definition) + if route not in router: + definition['name'] = route + router.add_definition(definition) def setup_authenticator(self): for name, definition in config.definitions.items(): diff --git a/watson/auth/views/debug/panels/auth.html b/watson/auth/views/debug/panels/auth.html index c311cba..80d7ac8 100644 --- a/watson/auth/views/debug/panels/auth.html +++ b/watson/auth/views/debug/panels/auth.html @@ -1,4 +1,3 @@ -{% if user %} -
Session
-
- {{ request.session.id }}

- - - - - - - - {% for key, data in request.session %} - - - - - {% else %} - - - - {% endfor %} - -
KeyData
{{ key }}{{ data }}
No session data.
-
+{% if user %}
Roles
@@ -71,7 +48,7 @@ {% else %} - + {% endfor %} @@ -79,6 +56,27 @@

-{% else %} -Unauthenticated guest, no data available. {% endif %} +
Session
+
+ {{ request.session.id }} {{ request.session.timeout }}

+
No permissions set for this user.No permissions set for this user.
+ + + + + + + {% for key, data in request.session %} + + + + + {% else %} + + + + {% endfor %} + +
KeyData
{{ key }}{{ data }}
No session data.
+