From 839620aeee3969adef1065c95a131403067ea790 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Thu, 22 Aug 2019 17:16:56 +0200 Subject: [PATCH] enable autologin after first manual login (#30018) --- .../authenticators.py | 30 ++++---- .../authentic2_auth_kerberos/login.html | 10 ++- src/authentic2_auth_kerberos/utils.py | 28 ------- src/authentic2_auth_kerberos/views.py | 15 +++- tests/conftest.py | 53 +++++++++++++ tests/settings.py | 2 + tests/test_login.py | 76 +++++++++++++++++++ tox.ini | 3 +- 8 files changed, 169 insertions(+), 48 deletions(-) delete mode 100644 src/authentic2_auth_kerberos/utils.py create mode 100644 tests/conftest.py create mode 100644 tests/test_login.py diff --git a/src/authentic2_auth_kerberos/authenticators.py b/src/authentic2_auth_kerberos/authenticators.py index 83938ed..383a489 100644 --- a/src/authentic2_auth_kerberos/authenticators.py +++ b/src/authentic2_auth_kerberos/authenticators.py @@ -1,7 +1,10 @@ from django.utils.translation import gettext_noop -from django import forms +from django.shortcuts import render + +from authentic2 import constants, utils + +from . import app_settings -from . import app_settings, utils class KerberosAuthenticator(object): def enabled(self): @@ -13,14 +16,15 @@ class KerberosAuthenticator(object): def id(self): return 'kerberos' - def form(self): - return forms.Form - - def post(self, request, form, nonce, next): - return utils.redirect_next(request, 'kerberos-login', nonce=nonce) - - def template(self): - return 'authentic2_auth_kerberos/login.html' - - def is_hidden(self, request): - return 'a2_just_logged_out' not in request.COOKIES + def login(self, request, *args, **kwargs): + context = kwargs.get('context', {}) + # autologin is allowed if : + # * a Kerberos login has already happened on this terminal + # * we do not come from the logout view + if (request.COOKIES.get('a2_kerberos_ok') == '1' + and 'a2_just_logged_out' not in request.COOKIES): + context['autologin'] = True + submit = request.method == 'POST' and 'login-kerberos' in request.POST + if submit: + return utils.redirect_to_login(request, login_url='kerberos-login') + return render(request, 'authentic2_auth_kerberos/login.html', context) diff --git a/src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/login.html b/src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/login.html index fd6d39f..71d5cc5 100644 --- a/src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/login.html +++ b/src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/login.html @@ -1,11 +1,13 @@ {% load i18n %}
- {% include "authentic2_auth_kerberos/autologin.html" %} -
+ {% if autologin %} + {% include "authentic2_auth_kerberos/autologin.html" %} + {% endif %} + {% csrf_token %} - + {% if cancel %} - + {% endif %}
diff --git a/src/authentic2_auth_kerberos/utils.py b/src/authentic2_auth_kerberos/utils.py deleted file mode 100644 index 35eeaac..0000000 --- a/src/authentic2_auth_kerberos/utils.py +++ /dev/null @@ -1,28 +0,0 @@ -import urlparse -import urllib - -from django.http import HttpResponseRedirect -from django.core.urlresolvers import reverse - -def redirect_next(request, url, **kwargs): - '''Redirect to URL, add or replace parameters with kwargs - - You can use relative or fully qualifier, or view names. You can provide - view args and kwargs using named argument args and kwargs. - ''' - # specialize on the next url parameter as it is so frequent - if 'next' in request.REQUEST: - next_url = request.REQUEST.get('next') - kwargs['next'] = next_url - if not url.startswith('/') and not url.startswith('http:') and not url.startswith('https:'): - view_args = kwargs.pop('args', None) - view_kwargs = kwargs.pop('kwargs', None) - url = reverse(url, args=view_args, kwargs=view_kwargs) - parsed = urlparse.urlparse(url) - params = urlparse.parse_qs(parsed.query) - for key in kwargs.keys(): - if kwargs[key] is None: - del kwargs[key] - params.update(kwargs) - url = urlparse.urlunparse(parsed[:4] + (urllib.urlencode(params),) + parsed[5:]) - return HttpResponseRedirect(url) diff --git a/src/authentic2_auth_kerberos/views.py b/src/authentic2_auth_kerberos/views.py index f2722dd..330d6de 100644 --- a/src/authentic2_auth_kerberos/views.py +++ b/src/authentic2_auth_kerberos/views.py @@ -2,17 +2,28 @@ import logging from django_kerberos.views import NegotiateView -from authentic2.models import AuthenticationEvent from authentic2 import utils -__ALL_ = [ 'login' ] +__ALL_ = ['login'] + class A2NegotiateView(NegotiateView): + authentication_successful = False + def __init__(self, *args, **kwargs): super(A2NegotiateView, self).__init__(*args, **kwargs) self.logger = logging.getLogger(__name__) def login_user(self, request, user): + self.authentication_successful = True utils.login(request, user, 'kerberos') + def principal_valid(self, request, *args, **kwargs): + response = super(A2NegotiateView, self).principal_valid(request, *args, **kwargs) + if self.authentication_successful: + # set cookie so that automatic login will be tried next time + response.set_cookie('a2_kerberos_ok', '1', max_age=86400 * 365) + return response + + login = A2NegotiateView.as_view() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3e6ec29 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# authentic2-auth-kerberos - Kerberos plugin for authentic2 +# Copyright (C) 2010-2019 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import pytest +import django_webtest + +from authentic2.custom_user.models import User +from authentic2.a2_rbac.utils import get_default_ou + + +@pytest.fixture +def app_factory(settings, tmpdir): + settings.MEDIA_ROOT = str(tmpdir.mkdir('media')) + wtm = django_webtest.WebTestMixin() + wtm._patch_settings() + try: + def factory(hostname='localhost'): + return django_webtest.DjangoTestApp(extra_environ={'HTTP_HOST': hostname}) + yield factory + finally: + wtm._unpatch_settings() + + +@pytest.fixture +def app(app_factory): + return app_factory() + + +@pytest.fixture +def create_user(): + def func(**kwargs): + password = kwargs.pop('password', None) or kwargs['username'] + user, created = User.objects.get_or_create(**kwargs) + if password: + user.set_password(password) + user.save() + return user + return func diff --git a/tests/settings.py b/tests/settings.py index 2a167b4..fbe641a 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -14,3 +14,5 @@ if 'postgres' in DATABASES['default']['ENGINE']: for key in ('PGPORT', 'PGHOST', 'PGUSER', 'PGPASSWORD'): if key in os.environ: DATABASES['default'][key[2:]] = os.environ[key] + +ALLOWED_HOSTS = ['localhost'] diff --git a/tests/test_login.py b/tests/test_login.py new file mode 100644 index 0000000..69e8167 --- /dev/null +++ b/tests/test_login.py @@ -0,0 +1,76 @@ +# authentic2-auth-kerberos - Kerberos plugin for authentic2 +# Copyright (C) 2010-2019 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest + +from authentic2.custom_user.models import User + + +@pytest.fixture(autouse=True) +def kerberos(monkeypatch): + import django_kerberos.views + + class MockKerberos(object): + server_init_result = 1 + server_step_result = 1 + server_response_result = 'x' + principal = 'user@REALM' + + def set(self, **kwargs): + self.__dict__ = kwargs + + def authGSSServerInit(self, service): + return self.server_init_result, {} + + def authGSSServerStep(self, context, authstr): + return self.server_step_result + + def authGSSServerResponse(self, context): + return self.server_response_result + + def authGSSServerUserName(self, context): + return self.principal + + def authGSSServerClean(self, context): + pass + + class KrbError(Exception): + pass + monkeypatch.setattr('django_kerberos.views.kerberos', MockKerberos()) + return django_kerberos.views.kerberos + + +def test_default(settings, app, db): + settings.A2_AUTH_KERBEROS_DJANGO_BACKEND = True + + assert User.objects.count() == 0 + assert 'a2_kerberos_ok' not in app.cookies + response = app.get('/login/') + assert 'login-kerberos' in response.text + assert 'autologin' not in response.text + response = response.forms['kerberos-form'].submit(name='login-kerberos') + assert response.location == '/accounts/kerberos/login/' + response = response.follow(headers={'Authorization': 'Negotiate y'}) + assert app.cookies['a2_kerberos_ok'] == '1' + assert response.location == '/' + assert User.objects.count() == 1 + assert User.objects.get(username='user@realm') + + # logout + app.session.flush() + response = app.get('/login/') + assert 'login-kerberos' in response.text + assert 'autologin' in response.text diff --git a/tox.ini b/tox.ini index 49cb59f..b9d6436 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ [tox] toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/authentic2-auth-kerberos/ -envlist = py27-coverage-{dj18,dj111}-{oldldap,},pylint +envlist = py27-coverage-dj111-{oldldap,},pylint [testenv] whitelist_externals = @@ -42,6 +42,7 @@ deps = ldaptools http://git.entrouvert.org/authentic.git/snapshot/authentic-master.tar.bz2 oldldap: python-ldap<3 + django-webtest commands = ./getlasso.sh py.test {env:COVERAGE:} {env:JUNIT:} {posargs:tests}