From 3778e18202848566c1e2c8bd4ec40b03a94c9ac5 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 13 Aug 2014 18:05:14 +0200 Subject: [PATCH] first commit --- COPYING | 2 + MANIFEST.in | 3 + README | 35 ++++++++++ setup.py | 67 +++++++++++++++++++ src/authentic2_auth_kerberos/__init__.py | 15 +++++ src/authentic2_auth_kerberos/admin.py | 0 src/authentic2_auth_kerberos/app_settings.py | 25 +++++++ .../auth_frontends.py | 23 +++++++ src/authentic2_auth_kerberos/backends.py | 28 ++++++++ src/authentic2_auth_kerberos/dashboard.py | 11 +++ src/authentic2_auth_kerberos/decorators.py | 14 ++++ src/authentic2_auth_kerberos/forms.py | 3 + .../migrations/__init__.py | 0 src/authentic2_auth_kerberos/models.py | 0 .../static/authentic2_auth_kerberos/.keepme | 0 .../authentic2_auth_kerberos/index.html | 1 + .../authentic2_auth_kerberos/login.html | 11 +++ src/authentic2_auth_kerberos/urls.py | 7 ++ src/authentic2_auth_kerberos/utils.py | 28 ++++++++ src/authentic2_auth_kerberos/views.py | 34 ++++++++++ 20 files changed, 307 insertions(+) create mode 100644 COPYING create mode 100644 MANIFEST.in create mode 100644 README create mode 100755 setup.py create mode 100644 src/authentic2_auth_kerberos/__init__.py create mode 100644 src/authentic2_auth_kerberos/admin.py create mode 100644 src/authentic2_auth_kerberos/app_settings.py create mode 100644 src/authentic2_auth_kerberos/auth_frontends.py create mode 100644 src/authentic2_auth_kerberos/backends.py create mode 100644 src/authentic2_auth_kerberos/dashboard.py create mode 100644 src/authentic2_auth_kerberos/decorators.py create mode 100644 src/authentic2_auth_kerberos/forms.py create mode 100644 src/authentic2_auth_kerberos/migrations/__init__.py create mode 100644 src/authentic2_auth_kerberos/models.py create mode 100644 src/authentic2_auth_kerberos/static/authentic2_auth_kerberos/.keepme create mode 100644 src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/index.html create mode 100644 src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/login.html create mode 100644 src/authentic2_auth_kerberos/urls.py create mode 100644 src/authentic2_auth_kerberos/utils.py create mode 100644 src/authentic2_auth_kerberos/views.py diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..ff9eff1 --- /dev/null +++ b/COPYING @@ -0,0 +1,2 @@ +authentic2-auth-kerberos 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..7628020 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include COPYING +recursive-include src/authentic2_auth_kerberos/templates *.html +recursive-include src/authentic2_auth_kerberos/static *.js *.css *.png diff --git a/README b/README new file mode 100644 index 0000000..436757e --- /dev/null +++ b/README @@ -0,0 +1,35 @@ +Authentic2 Auth Kerberos +========================== + +It provides basic implementation of the HTTP Negotiate authentication mechanism +and autologin support using Javascript and a cookie. + +The django-kerberos_ project is used as a basis for this plugin. + +Install +------- + +You just have to install the package in your virtualenv and relaunch, it will +be automatically loaded by authentic2. + +You must define the KRB5_KTNAME environment to the path of a keytab file +containing the key for your service principal. See django-kerberos_ +documentation for details. + +Settings +-------- + +- A2_AUTH_KERBEROS_ENABLED: enable the authentication module, default is True. +- A2_AUTH_KERBEROS_CREATE_USER: whether to create users for Kerberos + principals, default is True. +- A2_AUTH_KERBEROS_REALM: default reaml to attribute to user, default is None. + If not None, the Kerberos realm is replaced by this one. It's incompatible + with support for multiple realms. + +.. _django-kerberos: https://pypi.python.org/pypi/django-kerberos + +Roadmap +------- + +- LDAP support (think AD support) +- Linking Kerberos principals to existing users diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..085a78c --- /dev/null +++ b/setup.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +from setuptools import setup, find_packages +import os + +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 + +README = file(os.path.join( + os.path.dirname(__file__), + 'README')).read() + +setup(name='authentic2-auth-kerberos', + version=get_version(), + license='AGPLv3', + description='Authentic2 Auth Kerberos', + long_description=README, + author="Entr'ouvert", + author_email="info@entrouvert.com", + packages=find_packages('src'), + package_dir={ + '': 'src', + }, + package_data={ + 'authentic2_auth_kerberos': [ + 'templates/authentic2_auth_kerberos/*.html', + 'static/authentic2_auth_kerberos/js/*.js', + 'static/authentic2_auth_kerberos/css/*.css', + 'static/authentic2_auth_kerberos/img/*.png', + ], + }, + install_requires=[ + 'authentic2', + 'django-kerberos', + ], + entry_points={ + 'authentic2.plugin': [ + 'authentic2-auth-kerberos = authentic2_auth_kerberos:Plugin', + ], + }, +) diff --git a/src/authentic2_auth_kerberos/__init__.py b/src/authentic2_auth_kerberos/__init__.py new file mode 100644 index 0000000..14b10f6 --- /dev/null +++ b/src/authentic2_auth_kerberos/__init__.py @@ -0,0 +1,15 @@ +__version__ = '1.0.0' + +class Plugin(object): + def get_before_urls(self): + from . import urls + return urls.urlpatterns + + def get_apps(self): + return [__name__, 'django_kerberos'] + + def get_authentication_backends(self): + return ['authentic2_auth_kerberos.backends.A2KerberosBackend'] + + def get_auth_frontends(self): + return ['authentic2_auth_kerberos.auth_frontends.KerberosFrontend'] diff --git a/src/authentic2_auth_kerberos/admin.py b/src/authentic2_auth_kerberos/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/src/authentic2_auth_kerberos/app_settings.py b/src/authentic2_auth_kerberos/app_settings.py new file mode 100644 index 0000000..850f990 --- /dev/null +++ b/src/authentic2_auth_kerberos/app_settings.py @@ -0,0 +1,25 @@ +class AppSettings(object): + __DEFAULTS = { + 'ENABLED': True, + 'CREATE_USER': True, + 'REALM': None, + } + + def __init__(self, prefix): + self.prefix = prefix + + def _setting(self, name, dflt): + from django.conf import settings + return getattr(settings, self.prefix+name, dflt) + + def __getattr__(self, name): + if name not in self.__DEFAULTS: + raise AttributeError(name) + return self._setting(name, self.__DEFAULTS[name]) + +# Ugly? Guido recommends this himself ... +# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html +import sys +app_settings = AppSettings('A2_AUTH_KERBEROS_') +app_settings.__name__ = __name__ +sys.modules[__name__] = app_settings diff --git a/src/authentic2_auth_kerberos/auth_frontends.py b/src/authentic2_auth_kerberos/auth_frontends.py new file mode 100644 index 0000000..6535895 --- /dev/null +++ b/src/authentic2_auth_kerberos/auth_frontends.py @@ -0,0 +1,23 @@ +from django.utils.translation import gettext_noop +from django import forms + +from . import app_settings, utils + +class KerberosFrontend(object): + def enabled(self): + return app_settings.ENABLED + + def name(self): + return gettext_noop('Kerberos') + + 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' diff --git a/src/authentic2_auth_kerberos/backends.py b/src/authentic2_auth_kerberos/backends.py new file mode 100644 index 0000000..6136648 --- /dev/null +++ b/src/authentic2_auth_kerberos/backends.py @@ -0,0 +1,28 @@ +from django_kerberos.backends import KerberosBackend + +import logging + +from . import app_settings + +class A2KerberosBackend(KerberosBackend): + def __init__(self): + super(A2KerberosBackend, self).__init__() + self.logger = logging.getLogger(__name__) + + def username_from_principal(self, principal): + if app_settings.REALM: + username, domain = principal.rsplit('@', 1) + return '{0}@{1}'.format(username, app_settings.REALM) + return super(A2KerberosBackend, self).username_from_principal(principal) + + def should_create_user(self): + return app_settings.CREATE_USER + + def provision_user(self, principal, user): + pass + + def authenticate(self, principal=None, **kwargs): + if not app_settings.ENABLED: + return + return super(A2KerberosBackend, self).authenticate(principal=principal, + **kwargs) diff --git a/src/authentic2_auth_kerberos/dashboard.py b/src/authentic2_auth_kerberos/dashboard.py new file mode 100644 index 0000000..d7b3f99 --- /dev/null +++ b/src/authentic2_auth_kerberos/dashboard.py @@ -0,0 +1,11 @@ +from django.utils.translation import ugettext_lazy as _ + +from admin_tools.dashboard import modules + + +def get_admin_modules(): + '''Show Client model in authentic2 admin''' + model_list = modules.ModelList(_('Authentic2 Auth Kerberos'), + models=('authentic2_auth_kerberos.models.*',)) + return (model_list,) + diff --git a/src/authentic2_auth_kerberos/decorators.py b/src/authentic2_auth_kerberos/decorators.py new file mode 100644 index 0000000..a6f19bf --- /dev/null +++ b/src/authentic2_auth_kerberos/decorators.py @@ -0,0 +1,14 @@ +import functools + +from django.http import Http404 + +from . import app_settings + +def plugin_enabled(view): + '''If plugin is not enabled, return 404''' + @functools.wraps(view) + def wrapper(*args, **kwargs): + if not app_settings.ENABLED: + raise Http404 + return view(*args, **kwargs) + return wrapper diff --git a/src/authentic2_auth_kerberos/forms.py b/src/authentic2_auth_kerberos/forms.py new file mode 100644 index 0000000..32d64dc --- /dev/null +++ b/src/authentic2_auth_kerberos/forms.py @@ -0,0 +1,3 @@ +from django import forms + + diff --git a/src/authentic2_auth_kerberos/migrations/__init__.py b/src/authentic2_auth_kerberos/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/authentic2_auth_kerberos/models.py b/src/authentic2_auth_kerberos/models.py new file mode 100644 index 0000000..e69de29 diff --git a/src/authentic2_auth_kerberos/static/authentic2_auth_kerberos/.keepme b/src/authentic2_auth_kerberos/static/authentic2_auth_kerberos/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/index.html b/src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/index.html new file mode 100644 index 0000000..5d16e06 --- /dev/null +++ b/src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/index.html @@ -0,0 +1 @@ +{% comment %}placeholder{% endcomment %} diff --git a/src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/login.html b/src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/login.html new file mode 100644 index 0000000..e23b0ce --- /dev/null +++ b/src/authentic2_auth_kerberos/templates/authentic2_auth_kerberos/login.html @@ -0,0 +1,11 @@ +{% load i18n %} +
+ {% include "django_kerberos/autologin.html" %} +
+ {% csrf_token %} + + {% if cancel %} + + {% endif %} +
+
diff --git a/src/authentic2_auth_kerberos/urls.py b/src/authentic2_auth_kerberos/urls.py new file mode 100644 index 0000000..da8ec06 --- /dev/null +++ b/src/authentic2_auth_kerberos/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import patterns, url, include + +from . import views + +urlpatterns = patterns('', + url(r'^accounts/kerberos/login/$', views.login, name='kerberos-login'), +) diff --git a/src/authentic2_auth_kerberos/utils.py b/src/authentic2_auth_kerberos/utils.py new file mode 100644 index 0000000..35eeaac --- /dev/null +++ b/src/authentic2_auth_kerberos/utils.py @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..166d989 --- /dev/null +++ b/src/authentic2_auth_kerberos/views.py @@ -0,0 +1,34 @@ +import logging + +from django_kerberos.views import NegotiateView + +from authentic2.models import AuthenticationEvent + +__ALL_ = [ 'login' ] + +class A2NegotiateView(NegotiateView): + def __init__(self, *args, **kwargs): + self.logger = logging.getLogger(__name__) + super(A2NegotiateView, self).__init__(*args, **kwargs) + + def user_found(self, request, user, *args, **kwargs): + response = super(A2NegotiateView, self).user_found(request, user, + *args, **kwargs) + nonce = request.REQUEST.get('nonce', '') + if nonce: + self.logger.info('logged in %r as %r (nonce %r)', self.principal, + user, nonce) + else: + self.logger.info('logged in %r as %r', self.principal, user) + AuthenticationEvent.objects.create( + who=unicode(user), + how='kerberos', + nonce=nonce) + return response + + def user_not_found(self, request, user, *args, **kwargs): + self.logger.debug('unable to log in %r', self.principal) + return super(A2NegotiateView, self).user_not_found(request, user, + *args, **kwargs) + +login = A2NegotiateView.as_view()