From 3309c6005c4650cb5ec6f81a9c877ca48c639e12 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Wed, 21 Nov 2018 15:24:29 +0100 Subject: [PATCH] misc: add support for request based enable conditions on authentication frontends (#28215) --- src/authentic2/app_settings.py | 1 + .../auth2_auth/auth2_ssl/app_settings.py | 1 + .../auth2_auth/auth2_ssl/authenticators.py | 9 ++++++--- src/authentic2/authenticators.py | 20 ++++++++++++++++--- src/authentic2/utils.py | 4 ++-- src/authentic2/views.py | 8 ++++---- src/authentic2_auth_oidc/app_settings.py | 4 ++++ src/authentic2_auth_oidc/authenticators.py | 9 ++++++--- src/authentic2_auth_saml/app_settings.py | 4 ++++ src/authentic2_auth_saml/authenticators.py | 9 ++++++--- tests/test_login.py | 20 ++++++++++++++++++- 11 files changed, 70 insertions(+), 19 deletions(-) diff --git a/src/authentic2/app_settings.py b/src/authentic2/app_settings.py index 67ecb553..ce0f8eec 100644 --- a/src/authentic2/app_settings.py +++ b/src/authentic2/app_settings.py @@ -157,6 +157,7 @@ default_settings = dict( definition='path of a class to validate passwords'), A2_PASSWORD_POLICY_SHOW_LAST_CHAR=Setting(default=False, definition='Show last character in password fields'), A2_AUTH_PASSWORD_ENABLE=Setting(default=True, definition='Activate login/password authentication', names=('AUTH_PASSWORD',)), + A2_AUTH_PASSWORD_ENABLE_CONDITION=Setting(default=None, definition='Filters', names=('FRONTEND ENABLE CONDITION',)), A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting(default=0, definition='Failure count before logging a warning to ' 'authentic2.user_login_failure. No warning will be send if value is ' diff --git a/src/authentic2/auth2_auth/auth2_ssl/app_settings.py b/src/authentic2/auth2_auth/auth2_ssl/app_settings.py index 5ff159e9..8707e528 100644 --- a/src/authentic2/auth2_auth/auth2_ssl/app_settings.py +++ b/src/authentic2/auth2_auth/auth2_ssl/app_settings.py @@ -16,6 +16,7 @@ class AppSettings(object): CREATE_USERNAME_CALLBACK=None, USE_COOKIE=False, CREATE_USER=False, + ENABLE_CONDITION=None ) def __init__(self, prefix): diff --git a/src/authentic2/auth2_auth/auth2_ssl/authenticators.py b/src/authentic2/auth2_auth/auth2_ssl/authenticators.py index 5332b93b..7be542d2 100644 --- a/src/authentic2/auth2_auth/auth2_ssl/authenticators.py +++ b/src/authentic2/auth2_auth/auth2_ssl/authenticators.py @@ -3,11 +3,14 @@ import django.forms from . import views, app_settings from authentic2.utils import redirect_to_login +from authentic2.authenticators import BaseAuthenticator -class SSLAuthenticator(object): - def enabled(self): - return app_settings.ENABLE +class SSLAuthenticator(BaseAuthenticator): + def enabled(self, request=None): + if app_settings.ENABLE: + return self.eval_enable_condition(app_settings.ENABLE_CONDITION, request) + return False def id(self): return 'ssl' diff --git a/src/authentic2/authenticators.py b/src/authentic2/authenticators.py index 0944d7b6..4450da5e 100644 --- a/src/authentic2/authenticators.py +++ b/src/authentic2/authenticators.py @@ -1,14 +1,28 @@ +import logging + from django.shortcuts import render from django.utils.translation import ugettext as _, ugettext_lazy from . import views, app_settings, utils, constants, forms +class BaseAuthenticator(object): + def eval_enable_condition(self, condition, request): + if condition: + try: + return eval(condition, {'request': request}) + except Exception, e: + logging.getLogger(__name__).warning('filter expression error: %r' % e) + return False + return True + -class LoginPasswordAuthenticator(object): +class LoginPasswordAuthenticator(BaseAuthenticator): submit_name = 'login-password-submit' - def enabled(self): - return app_settings.A2_AUTH_PASSWORD_ENABLE + def enabled(self, request=None): + if app_settings.A2_AUTH_PASSWORD_ENABLE: + return self.eval_enable_condition(app_settings.A2_AUTH_PASSWORD_ENABLE_CONDITION, request) + return False def name(self): return ugettext_lazy('Password') diff --git a/src/authentic2/utils.py b/src/authentic2/utils.py index d32a5a67..649d4dd5 100644 --- a/src/authentic2/utils.py +++ b/src/authentic2/utils.py @@ -152,7 +152,7 @@ def load_backend(path): return cls() -def get_backends(setting_name='IDP_BACKENDS'): +def get_backends(setting_name='IDP_BACKENDS', request=None): '''Return the list of enabled cleaned backends.''' backends = [] for backend_path in getattr(app_settings, setting_name): @@ -161,7 +161,7 @@ def get_backends(setting_name='IDP_BACKENDS'): backend_path, kwargs = backend_path backend = load_backend(backend_path) # If no enabled method is defined on the backend, backend enabled by default. - if hasattr(backend, 'enabled') and not backend.enabled(): + if hasattr(backend, 'enabled') and not backend.enabled(request): continue kwargs_settings = getattr(app_settings, setting_name + '_KWARGS', {}) if backend_path in kwargs_settings: diff --git a/src/authentic2/views.py b/src/authentic2/views.py index 13c96d0f..23c87818 100644 --- a/src/authentic2/views.py +++ b/src/authentic2/views.py @@ -281,7 +281,7 @@ def login(request, template_name='authentic2/login.html', redirect_to = settings.LOGIN_REDIRECT_URL nonce = request.GET.get(constants.NONCE_FIELD_NAME) - frontends = utils.get_backends('AUTH_FRONTENDS') + frontends = utils.get_backends('AUTH_FRONTENDS', request) blocks = [] @@ -402,11 +402,11 @@ class ProfileView(cbv.TemplateNamesMixin, TemplateView): return super(ProfileView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): - context = super(ProfileView, self).get_context_data(**kwargs) - frontends = utils.get_backends('AUTH_FRONTENDS') - request = self.request + context = super(ProfileView, self).get_context_data(**kwargs) + frontends = utils.get_backends('AUTH_FRONTENDS', request) + if request.method == "POST": for frontend in frontends: if 'submit-%s' % frontend.id in request.POST: diff --git a/src/authentic2_auth_oidc/app_settings.py b/src/authentic2_auth_oidc/app_settings.py index 95ac369a..15162c61 100644 --- a/src/authentic2_auth_oidc/app_settings.py +++ b/src/authentic2_auth_oidc/app_settings.py @@ -18,6 +18,10 @@ class AppSettings(object): def ENABLE(self): return self._setting('ENABLE', True) + @property + def ENABLE_CONDITION(self): + return self._setting('ENDABLE_CONDITION', None) + import sys diff --git a/src/authentic2_auth_oidc/authenticators.py b/src/authentic2_auth_oidc/authenticators.py index 4cac9a77..750114a1 100644 --- a/src/authentic2_auth_oidc/authenticators.py +++ b/src/authentic2_auth_oidc/authenticators.py @@ -2,11 +2,14 @@ from django.utils.translation import gettext_noop from django.shortcuts import render from . import app_settings, utils +from authentic2.authenticators import BaseAuthenticator -class OIDCAuthenticator(object): - def enabled(self): - return app_settings.ENABLE and utils.has_providers() +class OIDCAuthenticator(BaseAuthenticator): + def enabled(self, request=None): + if app_settings.ENABLE and utils.has_providers(): + return self.eval_enable_condition(app_settings.ENABLE_CONDITION, request) + return False def name(self): return gettext_noop('OpenIDConnect') diff --git a/src/authentic2_auth_saml/app_settings.py b/src/authentic2_auth_saml/app_settings.py index f4034a0b..1b3567b8 100644 --- a/src/authentic2_auth_saml/app_settings.py +++ b/src/authentic2_auth_saml/app_settings.py @@ -19,6 +19,10 @@ class AppSettings(object): def enable(self): return self._setting('ENABLE', False) + @property + def enable_condition(self): + return self._setting('ENABLE_CONDITION', None) + import sys diff --git a/src/authentic2_auth_saml/authenticators.py b/src/authentic2_auth_saml/authenticators.py index d1f1ffc4..0c4ffd1d 100644 --- a/src/authentic2_auth_saml/authenticators.py +++ b/src/authentic2_auth_saml/authenticators.py @@ -4,15 +4,18 @@ from django.shortcuts import render from mellon.utils import get_idp, get_idps from authentic2.utils import redirect_to_login +from authentic2.authenticators import BaseAuthenticator from . import app_settings -class SAMLAuthenticator(object): +class SAMLAuthenticator(BaseAuthenticator): id = 'saml' - def enabled(self): - return app_settings.enable and list(get_idps()) + def enabled(self, request=None): + if app_settings.enable: + return self.eval_enable_condition(app_settings.enable_condition, request) + return False def name(self): return gettext_noop('SAML') diff --git a/tests/test_login.py b/tests/test_login.py index 476c1f50..8dc54161 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -3,7 +3,7 @@ from urllib import quote from django.contrib.auth import get_user_model -from utils import login +from utils import login, check_log def test_login_inactive_user(db, app): @@ -31,6 +31,24 @@ def test_login_inactive_user(db, app): login(app, user1) assert '_auth_user_id' not in app.session +def test_login_with_conditionnal_enabled_frontend(db, app, settings, caplog): + settings.A2_AUTH_PASSWORD_ENABLE_CONDITION = "request.META['REMOTE_ADDR'] in ('127.0.0.1',)" + response = app.get('/login/') + assert 'name="login-password-submit"' in response + + # set invalid display condition + settings.A2_AUTH_PASSWORD_ENABLE_CONDITION = "request.META['REMOTE_ADDR'] in ('0.0.0.0',)" + response = app.get('/login/') + # login form must not be displayed + assert 'name="login-password-submit"' not in response + assert len(caplog.records) == 0 + + # set a condition with error + with check_log(caplog, 'filter expression error: SyntaxError(\'unexpected EOF while parsing\''): + settings.A2_AUTH_PASSWORD_ENABLE_CONDITION = "request.META['REMOTE_ADDR'] in " + response = app.get('/login/') + assert 'name="login-password-submit"' not in response + def test_registration_url_on_login_page(db, app): response = app.get('/login/?next=/whatever') -- 2.19.1