From cb05f9eb2a44d9e54b546332193df19f9d6a9472 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Sat, 9 Aug 2014 01:19:09 +0200 Subject: [PATCH] first commit --- COPYING | 2 + MANIFEST.in | 3 + README | 55 +++++++++++ sample/manage.py | 10 ++ sample/sample/__init__.py | 0 sample/sample/settings.py | 94 +++++++++++++++++++ sample/sample/templates/base.html | 7 ++ sample/sample/urls.py | 16 ++++ sample/sample/views.py | 7 ++ sample/sample/wsgi.py | 14 +++ setup.py | 62 ++++++++++++ src/django_kerberos/__init__.py | 1 + src/django_kerberos/app_settings.py | 20 ++++ src/django_kerberos/backends.py | 34 +++++++ src/django_kerberos/models.py | 0 .../templates/django_kerberos/error.html | 5 + .../django_kerberos/unauthorized.html | 5 + src/django_kerberos/urls.py | 7 ++ src/django_kerberos/views.py | 52 ++++++++++ 19 files changed, 394 insertions(+) create mode 100644 COPYING create mode 100644 MANIFEST.in create mode 100644 README create mode 100755 sample/manage.py create mode 100644 sample/sample/__init__.py create mode 100644 sample/sample/settings.py create mode 100644 sample/sample/templates/base.html create mode 100644 sample/sample/urls.py create mode 100644 sample/sample/views.py create mode 100644 sample/sample/wsgi.py create mode 100755 setup.py create mode 100644 src/django_kerberos/__init__.py create mode 100644 src/django_kerberos/app_settings.py create mode 100644 src/django_kerberos/backends.py create mode 100644 src/django_kerberos/models.py create mode 100644 src/django_kerberos/templates/django_kerberos/error.html create mode 100644 src/django_kerberos/templates/django_kerberos/unauthorized.html create mode 100644 src/django_kerberos/urls.py create mode 100644 src/django_kerberos/views.py diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..7895400 --- /dev/null +++ b/COPYING @@ -0,0 +1,2 @@ +cmsplugin-blurp is entirely under the copyright of Entr'ouvert and distributed +under the license AGPLv3 or later. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a24b278 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include COPYING +recursive-include sample *.py *.html +recursive-include src/django_kerberos/templates *.html diff --git a/README b/README new file mode 100644 index 0000000..a9f839c --- /dev/null +++ b/README @@ -0,0 +1,55 @@ +Kerberos authentication for Django +================================== + +Provide Kerberos authentication to Django applications. + +Basic usage +=========== + +Add this to your project `urls.py`:: + + url('^accounts/kerberos/', include('django_auth_kerb.urls')), + +And use the default authentication backend, by adding that to your `settings.py` file:: + + AUTHENTICATION_BACKENDS = ( + 'django_auth_kerberos.backends.KerberosBackend', + ) + +Settings +======== + +`KERBEROS_HOSTNAME` +------------------- + +Hostname for retrieving the service key, the correspondig principal will be +`HTTP/{KERBEROS_HOSTNAME}@DEFAULT_REAML`, default is `None`. If `None` the hostname +from the request will be used. + +`KERBEROS_KEYTAB` +----------------- + +File path of the keytab containing the key for the service principal, default +is `None`. If `None` the default host keytab will be tried, which should fails +since it's usually only readable by root. + +`KERBEROS_BACKEND_CREATE` +------------------------- + +Whether to create user if no existing model can be found, default is `False`. + +`KERBEROS_BACKEND_ADMIN_REGEXP` +------------------------------- + +A regular expression that the principal must match to get superuser privileges, +default is `None`. A classic example could be `r'^.*/admin$'`. + +Custom backend +============== + +A custom authentication backend can be used, in this case the signature of the +authenticate method must be:: + +class CustomKerberosBackend(object): + def authenticate(self, principal=None): + pass diff --git a/sample/manage.py b/sample/manage.py new file mode 100755 index 0000000..e30e858 --- /dev/null +++ b/sample/manage.py @@ -0,0 +1,10 @@ +#!/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 new file mode 100644 index 0000000..e69de29 diff --git a/sample/sample/settings.py b/sample/sample/settings.py new file mode 100644 index 0000000..72f5474 --- /dev/null +++ b/sample/sample/settings.py @@ -0,0 +1,94 @@ +""" +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', +) + +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' + +KERBEROS_BACKEND_CREATE = True +KERBEROS_BACKEND_ADMIN_REGEXP = r'^.*/admin$' diff --git a/sample/sample/templates/base.html b/sample/sample/templates/base.html new file mode 100644 index 0000000..b5d6e62 --- /dev/null +++ b/sample/sample/templates/base.html @@ -0,0 +1,7 @@ + + + + {% block content %} + {% endblock %} + + diff --git a/sample/sample/urls.py b/sample/sample/urls.py new file mode 100644 index 0000000..f8c75f7 --- /dev/null +++ b/sample/sample/urls.py @@ -0,0 +1,16 @@ +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_auth_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 new file mode 100644 index 0000000..b521b6a --- /dev/null +++ b/sample/sample/views.py @@ -0,0 +1,7 @@ +from django.contrib.auth.decorators import login_required +from django import http + + +@login_required +def home(request): + return http.HttpResponse(u'It worked ' + request.user.username + u'!', content_type='text/plain') diff --git a/sample/sample/wsgi.py b/sample/sample/wsgi.py new file mode 100644 index 0000000..1301c9c --- /dev/null +++ b/sample/sample/wsgi.py @@ -0,0 +1,14 @@ +""" +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 new file mode 100755 index 0000000..5db46a7 --- /dev/null +++ b/setup.py @@ -0,0 +1,62 @@ +#! /usr/bin/env python + +''' Setup script for django-kerberos +''' + +from setuptools import setup, find_packages + +def get_version(): + import glob + import re + import os + + version = None + for d in glob.glob('src/*'): + if not os.path.isdir(d): + continue + module_file = os.path.join(d, '__init__.py') + if not os.path.exists(module_file): + continue + for v in re.findall("""__version__ *= *['"](.*)['"]""", + open(module_file).read()): + assert version is None + version = v + if version: + break + assert version is not None + if os.path.exists('.git'): + import subprocess + p = subprocess.Popen(['git','describe','--dirty','--match=v*'], + stdout=subprocess.PIPE) + result = p.communicate()[0] + assert p.returncode == 0, 'git returned non-zero' + new_version = result.split()[0][1:] + assert new_version.split('-')[0] == version, '__version__ must match the last git annotated tag' + version = new_version.replace('-', '.') + return version + + +setup(name="django-kerberos", + version=get_version(), + license="AGPLv3 or later", + description="", + long_description=file('README').read(), + url="http://dev.entrouvert.org/projects/authentic/", + author="Entr'ouvert", + author_email="info@entrouvert.org", + maintainer="Benjamin Dauvergne", + maintainer_email="bdauvergne@entrouvert.com", + packages=find_packages('src'), + install_requires=[ + 'django>1.5', + ], + package_dir={ + '': 'src', + }, + package_data={ + 'django_kerberos': [ + 'templates/django_kerberos/*.html', + ], + }, + dependency_links=[], +) diff --git a/src/django_kerberos/__init__.py b/src/django_kerberos/__init__.py new file mode 100644 index 0000000..1f356cc --- /dev/null +++ b/src/django_kerberos/__init__.py @@ -0,0 +1 @@ +__version__ = '1.0.0' diff --git a/src/django_kerberos/app_settings.py b/src/django_kerberos/app_settings.py new file mode 100644 index 0000000..e800056 --- /dev/null +++ b/src/django_kerberos/app_settings.py @@ -0,0 +1,20 @@ +import sys + +class AppSettings(object): + __PREFIX = 'KERBEROS_' + __DEFAULTS = { + 'HOSTNAME': None, + 'KEYTAB': None, + 'BACKEND_CREATE': False, + 'BACKEND_ADMIN_REGEXP': None, + } + + def __getattr__(self, name): + from django.conf import settings + if name not in self.__DEFAULTS: + raise AttributeError + return getattr(settings, self.__PREFIX + name, self.__DEFAULTS[name]) + +app_settings = AppSettings() +app_settings.__name__ = __name__ +sys.modules[__name__] = app_settings diff --git a/src/django_kerberos/backends.py b/src/django_kerberos/backends.py new file mode 100644 index 0000000..701d8cf --- /dev/null +++ b/src/django_kerberos/backends.py @@ -0,0 +1,34 @@ +import re + +from . import app_settings + +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + +class KerberosBackend(ModelBackend): + def authenticate(self, principal=None): + '''Match principal username with Django user model username''' + if not principal: + return + User = get_user_model() + username = principal.split('@')[0] + username_field = getattr(User, 'USERNAME_FIELD', 'username') + kwargs = {username_field: username} + if app_settings.BACKEND_CREATE: + user, created = User.objects.get_or_create(**kwargs) + else: + try: + user = User.objects.get(**kwargs) + except User.DoesNotExist: + return + # basic authorization + if app_settings.BACKEND_ADMIN_REGEXP: + if re.match(app_settings.BACKEND_ADMIN_REGEXP, username): + if not user.is_staff or not user.is_superuser: + user.is_staff = True + user.is_superuser = True + user.save() + return user + + + diff --git a/src/django_kerberos/models.py b/src/django_kerberos/models.py new file mode 100644 index 0000000..e69de29 diff --git a/src/django_kerberos/templates/django_kerberos/error.html b/src/django_kerberos/templates/django_kerberos/error.html new file mode 100644 index 0000000..98ed931 --- /dev/null +++ b/src/django_kerberos/templates/django_kerberos/error.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +An error occurred during initialization of the GSSAPI context, verify that a properly initialized keytab is accessible +{% endblock %} diff --git a/src/django_kerberos/templates/django_kerberos/unauthorized.html b/src/django_kerberos/templates/django_kerberos/unauthorized.html new file mode 100644 index 0000000..d829266 --- /dev/null +++ b/src/django_kerberos/templates/django_kerberos/unauthorized.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +Access unauthorized please refresh your ticket cache +{% endblock %} diff --git a/src/django_kerberos/urls.py b/src/django_kerberos/urls.py new file mode 100644 index 0000000..de3834c --- /dev/null +++ b/src/django_kerberos/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import patterns, url + +from . import views + +urlpatterns = patterns('', + url(r'^login/$', views.login, name='kerberos-login'), +) diff --git a/src/django_kerberos/views.py b/src/django_kerberos/views.py new file mode 100644 index 0000000..e25b4ed --- /dev/null +++ b/src/django_kerberos/views.py @@ -0,0 +1,52 @@ +import kerberos +import os + +from django import http +from django.template.response import TemplateResponse +from django.contrib.auth import authenticate, login as auth_login +from django.conf import settings + +from . import app_settings + +def www_authenticate(request): + response = TemplateResponse(request, 'django_kerberos/unauthorized.html', status=401) + response['WWW-Authenticate'] = 'Negotiate' + return response + +def login(request): + '''Try to authenticate the user using SPNEGO and Kerberos''' + next_url = request.REQUEST.get('next') or settings.LOGIN_REDIRECT_URL + if app_settings.KEYTAB: + old = os.environ.get('KRB5_KTNAME') + os.environ['KRB5_KTNAME'] = app_settings.KEYTAB + try: + host = app_settings.HOSTNAME or request.get_host().split(':')[0] + service = 'HTTP@%s' % host + + if 'HTTP_AUTHORIZATION' in request.META: + kind, authstr = request.META['HTTP_AUTHORIZATION'].split(' ', 1) + print authstr + if kind == 'Negotiate': + result, context = kerberos.authGSSServerInit(service) + if result != 1: + return TemplateResponse(request, 'django_kerberos/error.html') + r = kerberos.authGSSServerStep(context, authstr) + if r == 1: + gssstring = kerberos.authGSSServerResponse(context) + else: + return www_authenticate(request) + principal = kerberos.authGSSServerUserName(context) + kerberos.authGSSServerClean(context) + user = authenticate(principal=principal) + if user: + auth_login(request, user) + response = http.HttpResponseRedirect(next_url) + response['WWW-Authenticate'] = 'Negotiate %s' % gssstring + return response + return www_authenticate(request) + finally: + if app_settings.KEYTAB: + if old: + os.environ['KRB5_KTNAME'] = old + else: + del os.environ['KRB5_KTNAME']