From e8a1a7221c2bb102b083272168e9d393edcaaa5e Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Sat, 2 Feb 2019 11:04:09 +0100 Subject: [PATCH] add Python3 and django 1.11 support (fixes #30321) Also remove the sample application. --- Jenkinsfile | 50 ++++++ README | 2 + debian/control | 13 +- pylint.sh | 13 ++ sample/README | 1 - sample/config.py.example | 3 - sample/manage.py | 10 -- sample/sample/__init__.py | 0 sample/sample/settings.py | 98 ------------ sample/sample/templates/base.html | 7 - sample/sample/templates/index.html | 14 -- .../sample/templates/registration/login.html | 9 -- sample/sample/urls.py | 16 -- sample/sample/views.py | 4 - sample/sample/wsgi.py | 14 -- setup.py | 27 ++-- src/django_kerberos/backends.py | 20 ++- src/django_kerberos/urls.py | 6 +- src/django_kerberos/views.py | 6 +- tests/settings.py | 42 +++++ tests/test_login.py | 146 +++++++++++++++++- tox.ini | 48 +++--- 22 files changed, 319 insertions(+), 230 deletions(-) create mode 100644 Jenkinsfile create mode 100755 pylint.sh delete mode 100644 sample/README delete mode 100644 sample/config.py.example delete mode 100755 sample/manage.py delete mode 100644 sample/sample/__init__.py delete mode 100644 sample/sample/settings.py delete mode 100644 sample/sample/templates/base.html delete mode 100644 sample/sample/templates/index.html delete mode 100644 sample/sample/templates/registration/login.html delete mode 100644 sample/sample/urls.py delete mode 100644 sample/sample/views.py delete mode 100644 sample/sample/wsgi.py diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..ccb6310 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,50 @@ +@Library('eo-jenkins-lib@master') import eo.Utils + +pipeline { + agent any + options { disableConcurrentBuilds() } + stages { + stage('Unit Tests') { + steps { + sh """ +rm -rf htmlcov* .coverage* coverage* junit*.xml +rm -rf venv +virtualenv -p python3 venv +. venv/bin/activate +pip install tox""" + sh './venv/bin/tox -rv' + } + post { + always { + script { + utils = new Utils() + utils.publish_coverage('coverage.xml') + utils.publish_coverage_native('index.html') + utils.publish_pylint('pylint.out') + } + junit 'junit*.xml' + } + } + } + stage('Packaging') { + steps { + script { + if (env.JOB_NAME == 'django-kerberos' && env.GIT_BRANCH == 'origin/master') { + sh 'sudo -H -u eobuilder /usr/local/bin/eobuilder django-kerberos' + } + } + } + } + } + post { + always { + script { + utils = new Utils() + utils.mail_notify(currentBuild, env, 'admin+jenkins-django-kerberos@entrouvert.com') + } + } + success { + cleanWs() + } + } +} diff --git a/README b/README index 1492f1b..482074a 100644 --- a/README +++ b/README @@ -3,6 +3,8 @@ Kerberos authentication for Django Provide Kerberos authentication to Django applications. +Python 2 and 3, Django >1.8 are supported. + Basic usage =========== diff --git a/debian/control b/debian/control index a7d371a..deb20b1 100644 --- a/debian/control +++ b/debian/control @@ -1,5 +1,5 @@ Source: python-django-kerberos -Maintainer: Benjamin Dauvergne +Maintainer: Benjamin Dauvergne Section: python Priority: optional Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6), debhelper (>= 7.4.3), @@ -10,7 +10,16 @@ X-Python-Version: >= 2.6 Package: python-django-kerberos Architecture: all Depends: ${misc:Depends}, ${python:Depends}, - python-django (>= 1.5), + python-six, + python-django (>= 1.8), python-kerberos Description: Kerberos authentication frontend for Authentic2 +Package: python3-django-kerberos +Architecture: all +Depends: ${misc:Depends}, ${python3:Depends}, + python3-six, + python3-django (>= 1.8), + python3-kerberos +Description: Kerberos authentication frontend for Authentic2 + diff --git a/pylint.sh b/pylint.sh new file mode 100755 index 0000000..d7295cc --- /dev/null +++ b/pylint.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -e -x +env +if [ -f /var/lib/jenkins/pylint.django.rc ]; then + PYLINT_RC=/var/lib/jenkins/pylint.django.rc +elif [ -f pylint.django.rc ]; then + PYLINT_RC=pylint.django.rc +else + echo No pylint RC found + exit 0 +fi +pylint -f parseable --rcfile ${PYLINT_RC} "$@" | tee pylint.out || /bin/true diff --git a/sample/README b/sample/README deleted file mode 100644 index e883b1b..0000000 --- a/sample/README +++ /dev/null @@ -1 +0,0 @@ -Rename config.py.example to config.py, and update default settings. diff --git a/sample/config.py.example b/sample/config.py.example deleted file mode 100644 index d2c2c7b..0000000 --- a/sample/config.py.example +++ /dev/null @@ -1,3 +0,0 @@ -KERBEROS_SERVICE_PRINCIPAL = 'HTTP/fenouil.entrouvert.org@ENTROUVERT.ORG' -KERBEROS_DEFAULT_REALM = 'ENTROUVERT.ORG' -KERBEROS_BACKEND_ADMIN_REGEXP = '^.*/admin@.*$' diff --git a/sample/manage.py b/sample/manage.py deleted file mode 100755 index e30e858..0000000 --- a/sample/manage.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/sample/sample/__init__.py b/sample/sample/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/sample/sample/settings.py b/sample/sample/settings.py deleted file mode 100644 index 6edad4a..0000000 --- a/sample/sample/settings.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Django settings for sample project. - -For more information on this file, see -https://docs.djangoproject.com/en/1.6/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.6/ref/settings/ -""" - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 's0xb*2mi0#pi48tri&x6cwr96k30mmdu%e6pa28_=n9^4eh-3=' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -TEMPLATE_DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django_kerberos', -) - -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) - -AUTHENTICATION_BACKENDS = ( - 'django_kerberos.backends.KerberosBackend', - 'django_kerberos.backends.KerberosPasswordBackend', -) - -ROOT_URLCONF = 'sample.urls' - -WSGI_APPLICATION = 'sample.wsgi.application' - -TEMPLATE_DIRS = ( os.path.join(BASE_DIR, 'sample', 'templates'), ) - - -# Database -# https://docs.djangoproject.com/en/1.6/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - -# Internationalization -# https://docs.djangoproject.com/en/1.6/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.6/howto/static-files/ - -STATIC_URL = '/static/' - -LOGIN_URL = 'kerberos-login' -LOGIN_REDIRECT_URL = '/' - -KERBEROS_BACKEND_CREATE = True -KERBEROS_BACKEND_ADMIN_REGEXP = r'^.*/admin$' - -execfile('config.py', globals()) diff --git a/sample/sample/templates/base.html b/sample/sample/templates/base.html deleted file mode 100644 index b5d6e62..0000000 --- a/sample/sample/templates/base.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - {% block content %} - {% endblock %} - - diff --git a/sample/sample/templates/index.html b/sample/sample/templates/index.html deleted file mode 100644 index 527538b..0000000 --- a/sample/sample/templates/index.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - {% include "django_kerberos/autologin.html" %} - {% if user.is_authenticated %} -

Hello {{ user }}

- {% if user.is_superuser %} -

Admin

- {% endif %} -

Logout

- {% else %} -

Please login with kerberos or with your password

- {% endif %} -{% endblock %} diff --git a/sample/sample/templates/registration/login.html b/sample/sample/templates/registration/login.html deleted file mode 100644 index 10f8cba..0000000 --- a/sample/sample/templates/registration/login.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
- {% csrf_token %} - {{ form.as_p }} - -
-{% endblock %} diff --git a/sample/sample/urls.py b/sample/sample/urls.py deleted file mode 100644 index a81f8d9..0000000 --- a/sample/sample/urls.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.conf.urls import patterns, include, url - -from django.contrib import admin -admin.autodiscover() - -from . import views - -urlpatterns = patterns('', - # Examples: - url(r'^$', views.home, name='home'), - # url(r'^blog/', include('blog.urls')), - - url('^accounts/kerberos/', include('django_kerberos.urls')), - url('^accounts/', include('django.contrib.auth.urls')), - url(r'^admin/', include(admin.site.urls)), -) diff --git a/sample/sample/views.py b/sample/sample/views.py deleted file mode 100644 index 61d8045..0000000 --- a/sample/sample/views.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.shortcuts import render - -def home(request): - return render(request, 'index.html') diff --git a/sample/sample/wsgi.py b/sample/sample/wsgi.py deleted file mode 100644 index 1301c9c..0000000 --- a/sample/sample/wsgi.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -WSGI config for sample project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ -""" - -import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample.settings") - -from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() diff --git a/setup.py b/setup.py index 4698360..2c1edc5 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ from setuptools.command.sdist import sdist class eo_sdist(sdist): def run(self): - print "creating VERSION file" + print("creating VERSION file") if os.path.exists('VERSION'): os.remove('VERSION') version = get_version() @@ -17,35 +17,41 @@ class eo_sdist(sdist): version_file.write(version) version_file.close() sdist.run(self) - print "removing VERSION file" + print("removing VERSION file") if os.path.exists('VERSION'): os.remove('VERSION') def get_version(): '''Use the VERSION, if absent generates a version with git describe, if not - tag exists, take 0.0.0- and add the length of the commit log. + tag exists, take 0.0- and add the length of the commit log. ''' if os.path.exists('VERSION'): with open('VERSION', 'r') as v: return v.read() if os.path.exists('.git'): - p = subprocess.Popen(['git', 'describe', '--dirty', '--match=v*'], stdout=subprocess.PIPE, + p = subprocess.Popen(['git', 'describe', '--dirty=.dirty','--match=v*'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) result = p.communicate()[0] if p.returncode == 0: - result = result.split()[0][1:] + result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v + if '-' in result: # not a tagged version + real_number, commit_count, commit_hash = result.split('-', 2) + version = '%s.post%s+%s' % (real_number, commit_count, commit_hash) + else: + version = result + return version else: - result = '0.0.0-%s' % len(subprocess.check_output( - ['git', 'rev-list', 'HEAD']).splitlines()) - return result.replace('-', '.').replace('.g', '+g') - return '0.0.0' + return '0.0.post%s' % len( + subprocess.check_output( + ['git', 'rev-list', 'HEAD']).splitlines()) + return '0.0' setup(name="django-kerberos", version=get_version(), license="AGPLv3 or later", description="Kerberos authentication for Django", - long_description=file('README').read(), + long_description=open('README').read(), url="http://dev.entrouvert.org/projects/authentic/", author="Entr'ouvert", author_email="info@entrouvert.org", @@ -53,6 +59,7 @@ setup(name="django-kerberos", maintainer_email="bdauvergne@entrouvert.com", packages=find_packages('src'), install_requires=[ + 'six', 'django>1.5', 'pykerberos', ], diff --git a/src/django_kerberos/backends.py b/src/django_kerberos/backends.py index 1cac582..0fa7bae 100644 --- a/src/django_kerberos/backends.py +++ b/src/django_kerberos/backends.py @@ -17,7 +17,7 @@ import re import logging -from . import app_settings +import six from django.core.exceptions import ImproperlyConfigured from django.utils.encoding import force_bytes @@ -27,6 +27,8 @@ from django.contrib.auth.backends import ModelBackend import kerberos +from . import app_settings + class KerberosBackend(ModelBackend): def __init__(self): @@ -74,7 +76,7 @@ class KerberosBackend(ModelBackend): self.provision_user(principal, user) return user - def authenticate(self, principal=None, **kwargs): + def authenticate(self, request=None, principal=None): if principal and self.authorize_principal(principal): return self.lookup_user(principal) @@ -102,22 +104,26 @@ class KerberosPasswordBackend(KerberosBackend): ' set') return app_settings.SERVICE_PRINCIPAL - def authenticate(self, username=None, password=None, **kwargs): + def authenticate(self, request=None, username=None, password=None): '''Verify username and password using Kerberos''' if not username: return - principal = force_bytes(self.principal_from_username(username)) - password = force_bytes(password) + kerb_principal = principal = self.principal_from_username(username) + kerb_password = password + + if six.PY2: + kerb_principal = force_bytes(kerb_principal) + kerb_password = force_bytes(kerb_principal) try: - if not kerberos.checkPassword(principal, password, + if not kerberos.checkPassword(kerb_principal, kerb_password, self.service_principal(), self.default_realm()): return except kerberos.KrbError as e: logging.getLogger(__name__).error( - 'password validation forprincipal %r failed %s', principal, e) + 'password validation for principal %r failed %s', principal, e) return else: if principal and self.authorize_principal(principal): diff --git a/src/django_kerberos/urls.py b/src/django_kerberos/urls.py index 8fe5228..1eed5f1 100644 --- a/src/django_kerberos/urls.py +++ b/src/django_kerberos/urls.py @@ -14,10 +14,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.conf.urls import patterns, url +from django.conf.urls import url from . import views -urlpatterns = patterns('', +urlpatterns = [ url(r'^login/$', views.login, name='kerberos-login'), -) +] diff --git a/src/django_kerberos/views.py b/src/django_kerberos/views.py index 9403e7b..235b4a1 100644 --- a/src/django_kerberos/views.py +++ b/src/django_kerberos/views.py @@ -52,7 +52,7 @@ class NegotiateView(View): '''Do something with the principal we received''' self.logger.info(u'got ticket for principal %s', self.principal) user = authenticate(principal=self.principal) - next_url = request.REQUEST.get(self.NEXT_URL_FIELD) or settings.LOGIN_REDIRECT_URL + next_url = request.POST.get(self.NEXT_URL_FIELD) or request.GET.get(self.NEXT_URL_FIELD) or settings.LOGIN_REDIRECT_URL if user: self.login_user(request, user) if request.is_ajax(): @@ -88,8 +88,8 @@ class NegotiateView(View): # ensure context is finalized try: if result != 1: - self.logger.warning(u'authGSSServerInit result is non-zero: %s', result) - details = u'authGSSServerInit result is non-zero: %s' % result + self.logger.warning(u'authGSSServerInit result is non-one: %s', result) + details = u'authGSSServerInit result is non-one: %s' % result return TemplateResponse(request, self.error_template_name, context={'details': details}, status=500) try: diff --git a/tests/settings.py b/tests/settings.py index 27f9439..c36ae6d 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,5 +1,42 @@ +import django import os.path +DATABASES = { + 'default': { + 'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'), + } +} + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(os.path.dirname(__file__), 'templates')], + 'APP_DIRS': True, + }, +] + +if django.VERSION < (1, 10): + MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.middleware.http.ConditionalGetMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + ) +else: + MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + ] + INSTALLED_APPS = ( 'django.contrib.contenttypes', 'django.contrib.auth', @@ -9,3 +46,8 @@ INSTALLED_APPS = ( TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), 'templates'),) ROOT_URLCONF = 'django_kerberos.urls' SECRET_KEY = 'xxx' + +AUTHENTICATION_BACKENDS = ( + 'django_kerberos.backends.KerberosBackend', + 'django_kerberos.backends.KerberosPasswordBackend', +) diff --git a/tests/test_login.py b/tests/test_login.py index 707b8f7..9f32b92 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -1,6 +1,9 @@ +import logging import pytest +import json import kerberos +from django.contrib.auth.models import User @pytest.fixture @@ -11,7 +14,8 @@ def kerberos_mock(request, mocker): 'kerberos.authGSSServerStep', 'kerberos.authGSSServerResponse', 'kerberos.authGSSServerUserName', - 'kerberos.authGSSServerClean' + 'kerberos.authGSSServerClean', + 'kerberos.checkPassword' ) for name in d: if hasattr(request, 'param') and name in request.param: @@ -29,9 +33,143 @@ def test_login_no_header(client, settings, kerberos_mock): @pytest.mark.parametrize('kerberos_mock', ['kerberos.authGSSServerInit'], indirect=True) def test_login_missing_keytab(client, settings, kerberos_mock, caplog): resp = client.get('/login/', HTTP_AUTHORIZATION='Negotiate coin') - for key, mock in kerberos_mock.iteritems(): + for key, mock in kerberos_mock.items(): assert mock.call_count == 0 - assert 'keytab problem' in resp.content - assert 'keytab problem' in caplog.text() + assert b'keytab problem' in resp.content + assert 'keytab problem' in caplog.text +def test_login(client, db, settings, kerberos_mock, caplog): + caplog.set_level(logging.INFO) + kerberos_mock['kerberos.authGSSServerInit'].side_effect = kerberos.KrbError('coin') + response = client.get('/login/', HTTP_AUTHORIZATION='Negotiate xxxx') + assert response.status_code == 500 + assert b'exception during authGSSServerInit' in response.content + assert 'exception during authGSSServerInit' in caplog.text + assert b'coin' in response.content + kerberos_mock['kerberos.authGSSServerInit'].side_effect = None + caplog.clear() + + kerberos_mock['kerberos.authGSSServerInit'].return_value = 0, None + response = client.get('/login/', HTTP_AUTHORIZATION='Negotiate xxxx') + assert response.status_code == 500 + assert b'authGSSServerInit result is non-one' in response.content + assert 'authGSSServerInit result is non-one' in caplog.text + caplog.clear() + + kerberos_mock['kerberos.authGSSServerInit'].return_value = 1, None + kerberos_mock['kerberos.authGSSServerStep'].side_effect = kerberos.KrbError('coin') + response = client.get('/login/', HTTP_AUTHORIZATION='Negotiate xxxx') + assert response.status_code == 500 + assert b'exception during authGSSServerStep' in response.content + assert 'exception during authGSSServerStep' in caplog.text + assert b'coin' in response.content + kerberos_mock['kerberos.authGSSServerStep'].side_effect = None + caplog.clear() + + kerberos_mock['kerberos.authGSSServerStep'].return_value = 0 + response = client.get('/login/', HTTP_AUTHORIZATION='Negotiate xxxx') + assert response.status_code == 401 + + kerberos_mock['kerberos.authGSSServerStep'].return_value = 1 + kerberos_mock['kerberos.authGSSServerUserName'].side_effect = kerberos.KrbError('coin') + response = client.get('/login/', HTTP_AUTHORIZATION='Negotiate xxxx') + assert response.status_code == 500 + assert b'exception during authGSSServerUserName' in response.content + assert 'exception during authGSSServerUserName' in caplog.text + assert b'coin' in response.content + kerberos_mock['kerberos.authGSSServerUserName'].side_effect = None + caplog.clear() + + kerberos_mock['kerberos.authGSSServerUserName'].return_value = 'john.doe@EXAMPLE.COM' + kerberos_mock['kerberos.authGSSServerResponse'].return_value = 'yyyy' + response = client.get('/login/', HTTP_AUTHORIZATION='Negotiate xxxx') + assert response.status_code == 302 + assert 'principal john.doe@EXAMPLE.COM has no local user' in caplog.text + caplog.clear() + + user = User.objects.create(username='john.doe@example.com') + assert '_auth_user_id' not in client.session + response = client.get('/login/', HTTP_AUTHORIZATION='Negotiate xxxx') + assert response.status_code == 302 + assert response['WWW-Authenticate'] == 'Negotiate yyyy' + assert 'principal john.doe@EXAMPLE.COM has no local user' not in caplog.text + assert client.session['_auth_user_id'] == str(user.id) + client.logout() + user.delete() + assert User.objects.count() == 0 + caplog.clear() + + settings.KERBEROS_BACKEND_CREATE = True + assert '_auth_user_id' not in client.session + response = client.get('/login/', HTTP_AUTHORIZATION='Negotiate xxxx') + assert response.status_code == 302 + assert 'principal john.doe@EXAMPLE.COM has no local user' not in caplog.text + assert User.objects.count() == 1 + user = User.objects.get() + assert not user.is_staff + assert not user.is_superuser + assert client.session['_auth_user_id'] == str(user.id) + assert 'got ticket for principal john.doe@EXAMPLE.COM' in caplog.text + client.logout() + caplog.clear() + + settings.KERBEROS_BACKEND_ADMIN_REGEXP = 'john.doe' + assert '_auth_user_id' not in client.session + response = client.get('/login/', HTTP_AUTHORIZATION='Negotiate xxxx') + assert response.status_code == 302 + assert 'principal john.doe@EXAMPLE.COM has no local user' not in caplog.text + assert User.objects.count() == 1 + user = User.objects.get() + assert user.is_staff + assert user.is_superuser + assert client.session['_auth_user_id'] == str(user.id) + assert 'got ticket for principal john.doe@EXAMPLE.COM' in caplog.text + assert 'giving superuser power to principal \'john.doe@EXAMPLE.COM\'' in caplog.text + client.logout() + caplog.clear() + + assert '_auth_user_id' not in client.session + response = client.get('/login/', HTTP_AUTHORIZATION='Negotiate xxxx', HTTP_X_REQUESTED_WITH='XMLHttpRequest') + assert response.status_code == 200 + assert json.loads(response.content.decode('ascii')) is True + + +def test_password_backend(db, settings, kerberos_mock, caplog): + from django.contrib.auth import authenticate + + settings.KERBEROS_DEFAULT_REALM = 'EXAMPLE.COM' + settings.KERBEROS_SERVICE_PRINCIPAL = 'HTTP/SERVICE.EXAMPLE.COM@EXAMPLE.COM' + + m = kerberos_mock['kerberos.checkPassword'] + m.return_value = False + assert authenticate(username='john.doe', password='password') is None + assert User.objects.count() == 0 + + m.return_value = True + assert authenticate(username='john.doe', password='password') is None + assert User.objects.count() == 0 + + user = User.objects.create(username='john.doe@example.com') + assert authenticate(username='john.doe', password='password') == user + user.delete() + + assert User.objects.count() == 0 + settings.KERBEROS_BACKEND_CREATE = True + new_user = authenticate(username='john.doe', password='password') + assert new_user + assert User.objects.count() == 1 + assert new_user.username == 'john.doe@example.com' + assert not new_user.has_usable_password() + + settings.KERBEROS_KEEP_PASSWORD = True + new_user = authenticate(username='john.doe', password='password') + assert User.objects.count() == 1 + assert new_user.username == 'john.doe@example.com' + assert new_user.has_usable_password() + assert new_user.check_password('password') + + caplog.clear() + m.side_effect = kerberos.KrbError('coin') + assert authenticate(username='john.doe', password='password') is None + assert 'password validation for principal %r failed coin' % u'john.doe@EXAMPLE.COM' in caplog.text diff --git a/tox.ini b/tox.ini index 83a1f0a..99eefe4 100644 --- a/tox.ini +++ b/tox.ini @@ -3,40 +3,38 @@ # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. -[testenv:coverage] +[tox] +toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/django-kerberos/{env:BRANCH_NAME:} +envlist = py27-coverage-{dj18,dj111}-{pg,sqlite},py3-coverage-{dj18,dj111,dj20,djlast}-{pg,sqlite},pylint + +[testenv] +whitelist_externals = + /bin/mv + /bin/rm setenv = DJANGO_SETTINGS_MODULE=settings PYTHONPATH=tests + coverage: COVERAGE=--cov-branch --cov-append --cov=src/ --cov-report=html --cov-report=xml --cov-config .coveragerc + sqlite: DB_ENGINE=django.db.backends.sqlite3 + pg: DB_ENGINE=django.db.backends.postgresql_psycopg2 usedevelop = true deps = - pytest + dj18: django>1.8,<1.9 + dj18: django-tables2<1.1 + dj111: django<2.0 + dj20: django<2.1 + djlast: django + pg: psycopg2-binary + pytest<4.2 pytest-mock pytest-django pytest-cov - pytest-capturelog commands = - py.test {posargs:--junit-xml=junit.xml --cov=src --cov-report xml --nomigrations tests} + py.test {env:COVERAGE:} --junit-xml=junit-{envname}.xml {posargs:tests} -[testenv:nocoverage] -setenv = - DJANGO_SETTINGS_MODULE=settings - PYTHONPATH=tests +[testenv:pylint] deps = - pytest - pytest-mock - pytest-django - pytest-cov - pytest-capturelog + pylint<1.8 + pylint-django<0.8.1 commands = - py.test {posargs:--nomigrations tests} - -[testenv:package] -# eobuilder is not on pypi, too bad -deps = setuptools - pip<8 - pyasn1 - ndg-httpsclient - pyopenssl -commands = - pip install -U --find-links https://jenkins.entrouvert.org/packages/ eobuilder - sh -c "sudo -u eobuilder -E env HOME=/var/lib/eobuilder PATH=$PATH $VIRTUAL_ENV/bin/eobuilder-ctl -d wheezy,jessie {posargs:django-kerberos}" + pylint: ./pylint.sh src/django_kerberos/