first commit
This commit is contained in:
commit
778f0df7b7
|
@ -0,0 +1,2 @@
|
|||
cmsplugin-blurp is entirely under the copyright of Entr'ouvert and distributed
|
||||
under the license AGPLv3 or later.
|
|
@ -0,0 +1,51 @@
|
|||
@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')
|
||||
}
|
||||
sh './merge-junit-results.py junit-*.xml >junit.xml'
|
||||
junit 'junit.xml'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Packaging') {
|
||||
steps {
|
||||
script {
|
||||
if (env.JOB_NAME == 'django-gssapi' && env.GIT_BRANCH == 'origin/master') {
|
||||
sh 'sudo -H -u eobuilder /usr/local/bin/eobuilder django-gssapi'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
script {
|
||||
utils = new Utils()
|
||||
utils.mail_notify(currentBuild, env, 'admin+jenkins-django-gssapi@entrouvert.com')
|
||||
}
|
||||
}
|
||||
success {
|
||||
cleanWs()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
include COPYING
|
||||
include NEWS
|
||||
include VERSION
|
||||
include tox.ini
|
||||
recursive-include tests *.py *.html
|
||||
recursive-include sample *.py *.html README *.py.example
|
||||
recursive-include src/django_gssapi/templates *.html
|
||||
recursive-include src/django_gssapi/static *.js
|
|
@ -0,0 +1,91 @@
|
|||
GSSAPI authentication for Django
|
||||
==================================
|
||||
|
||||
Provide GSSAPI (SPNEGO) authentication to Django applications.
|
||||
|
||||
It's a rewrite of django-kerberos using python-gssapi.
|
||||
|
||||
It's only tested with MIT Kerberos 5 using package k5test.
|
||||
|
||||
Python 2 and 3, Django >1.8 are supported.
|
||||
|
||||
Basic usage
|
||||
===========
|
||||
|
||||
Add this to your project `urls.py`::
|
||||
|
||||
url('^auth/gssapi/', include('django_gssapi.urls')),
|
||||
|
||||
And use the default authentication backend, by adding that to your `settings.py` file::
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django_gssapi.backends.GSSAPIBackend',
|
||||
)
|
||||
|
||||
View
|
||||
====
|
||||
|
||||
django-gssapi provide a base LoginView that you can subclass to get the
|
||||
behaviour your need, the main extension points are:
|
||||
|
||||
- `challenge()` returns the 401 response with the challenge, you should override it
|
||||
to show a template explaining the failure,
|
||||
- `success(user)` it should log the given user and redirect to REDIRECT_FIELD_NAME,
|
||||
- `get_service_name()` it should return a gssapi.Name for your service, by
|
||||
default it returns None, so GSSAPI will match any name available (for example
|
||||
with Kerberos it will match any name in your keytab, like
|
||||
@HTTP/my.domain.com@).
|
||||
|
||||
Settings
|
||||
========
|
||||
|
||||
To make your application use GSSAPI as its main login method::
|
||||
|
||||
LOGIN_URL = 'gssapi-login'
|
||||
|
||||
Your application need an environment where the GSSAPI mechanism like Kerberos
|
||||
will work, for Kerberos it means having a default keytab of creating one and
|
||||
setting its path in KRB5_KTNAME or you can use `GSSAPI_STORE` with MIT Kerberos
|
||||
5 and credential store extension to indicate a keytab::
|
||||
|
||||
GSSAPI_STORE = {'keytab': 'FILE:/var/lib/mykeytab'}
|
||||
|
||||
You can also force a GSSAPI name for you service with::
|
||||
|
||||
import gssapi
|
||||
|
||||
GSSAPI_NAME = gssapi.Name('HTTP/my.service.com', gssapi.MechType.hostbased_service)
|
||||
|
||||
GSSAPI authentication backend
|
||||
=============================
|
||||
|
||||
A dummy backend is provided in `django_gssapi.backends.GSSAPIBackend` it looks
|
||||
up user with the same username as the GSSAPI name. You should implement it for
|
||||
your use case.
|
||||
|
||||
A custom authentication backend must have the following signature::
|
||||
|
||||
class CustomGSSAPIBackend(object):
|
||||
def authenticate(self, request, gssapi_name):
|
||||
pass
|
||||
|
||||
The parameter `gssapi_name` is a `gssapi.Name` object, it can be casted to
|
||||
string to get the raw name.
|
||||
|
||||
Kerberos username/password backend
|
||||
==================================
|
||||
|
||||
If your users does not have their browser configured for SPNEGO HTTP
|
||||
authentication you can also provide a classic login/password form which check
|
||||
passwords using Kerberos. For this use
|
||||
`django_gssapi.backends.KerberosPasswordBackend`, the username is used as the
|
||||
raw principal name.
|
||||
|
||||
|
||||
django-rest-framework authentication backend
|
||||
============================================
|
||||
|
||||
To authenticate users with GSSAPI you can use
|
||||
`django_gssapi.drf.GSSAPIAuthentication`, it uses the configured GSSAPI
|
||||
authentication backend to find an user and returns the GSSAPI name in
|
||||
`request.auth`.
|
|
@ -0,0 +1,5 @@
|
|||
python-django-gssapi (0.1) stable; urgency=low
|
||||
|
||||
* New upstream release
|
||||
|
||||
-- Benjamin Dauvergne <bdauvergne@entrouvert.org> Thu, 23 Aug 2019 19:00:29 +0100
|
|
@ -0,0 +1,33 @@
|
|||
Source: python-django-gssapi
|
||||
Maintainer: Benjamin Dauvergne <bdauvergne@entrouvert.com>
|
||||
Section: python
|
||||
Priority: optional
|
||||
Build-Depends: dh-python,
|
||||
debhelper (>= 9),
|
||||
python-setuptools (>= 0.6b3),
|
||||
python-all (>= 2.6),
|
||||
python-django (>= 1.8),
|
||||
python3-all,
|
||||
python3-setuptools,
|
||||
python3-django (>= 1.8)
|
||||
Standards-Version: 3.9.1
|
||||
X-Python-Version: >= 2.7
|
||||
X-Python3-Version: >= 3.4
|
||||
|
||||
Package: python-django-gssapi
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends},
|
||||
python-six,
|
||||
python-django (>= 1.8),
|
||||
python-gssapi
|
||||
Description: GSSAPI authentication for Django
|
||||
|
||||
Package: python3-django-gssapi
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends},
|
||||
python3-six,
|
||||
python3-django (>= 1.8),
|
||||
python3-gssapi
|
||||
Description: GSSAPI authentication for Django
|
||||
.
|
||||
This is the Python 3 version of the package.
|
|
@ -0,0 +1,5 @@
|
|||
python-django-gssapi (0.1) stable; urgency=low
|
||||
|
||||
* New upstream release
|
||||
|
||||
-- Benjamin Dauvergne <bdauvergne@entrouvert.org> Thu, 23 Aug 2019 19:00:29 +0100
|
|
@ -0,0 +1 @@
|
|||
9
|
|
@ -0,0 +1,33 @@
|
|||
Source: python-django-gssapi
|
||||
Maintainer: Benjamin Dauvergne <bdauvergne@entrouvert.com>
|
||||
Section: python
|
||||
Priority: optional
|
||||
Build-Depends: dh-python,
|
||||
debhelper (>= 9),
|
||||
python-setuptools (>= 0.6b3),
|
||||
python-all (>= 2.6),
|
||||
python-django (>= 1.8),
|
||||
python3-all,
|
||||
python3-setuptools,
|
||||
python3-django (>= 1.8)
|
||||
Standards-Version: 3.9.1
|
||||
X-Python-Version: >= 2.7
|
||||
X-Python3-Version: >= 3.4
|
||||
|
||||
Package: python-django-gssapi
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends},
|
||||
python-six,
|
||||
python-django (>= 1.8),
|
||||
python-gssapi
|
||||
Description: GSSAPI authentication for Django
|
||||
|
||||
Package: python3-django-gssapi
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends},
|
||||
python3-six,
|
||||
python3-django (>= 1.8),
|
||||
python3-gssapi
|
||||
Description: GSSAPI authentication for Django
|
||||
.
|
||||
This is the Python 3 version of the package.
|
|
@ -0,0 +1,2 @@
|
|||
django python-django
|
||||
gssapi python-gssapi
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/make -f
|
||||
export PYBUILD_NAME=django-gssapi
|
||||
|
||||
%:
|
||||
dh $@ --with python2,python3 --buildsystem=pybuild
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Corey Goldberg, Dec 2012
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
"""Merge multiple JUnit XML files into a single results file.
|
||||
Output dumps to sdtdout.
|
||||
example usage:
|
||||
$ python merge_junit_results.py results1.xml results2.xml > results.xml
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if not args:
|
||||
usage()
|
||||
sys.exit(2)
|
||||
if '-h' in args or '--help' in args:
|
||||
usage()
|
||||
sys.exit(2)
|
||||
merge_results(args[:])
|
||||
|
||||
|
||||
def merge_results(xml_files):
|
||||
failures = 0
|
||||
tests = 0
|
||||
errors = 0
|
||||
time = 0.0
|
||||
cases = []
|
||||
|
||||
for file_name in xml_files:
|
||||
tree = ET.parse(file_name)
|
||||
test_suite = tree.getroot()
|
||||
failures += int(test_suite.attrib['failures'])
|
||||
tests += int(test_suite.attrib['tests'])
|
||||
errors += int(test_suite.attrib['errors'])
|
||||
time += float(test_suite.attrib['time'])
|
||||
name = test_suite.attrib.get('name', '')
|
||||
for child in test_suite.getchildren():
|
||||
child.attrib['classname'] = '%s-%s' % (name, child.attrib.get('classname', ''))
|
||||
cases.append(test_suite.getchildren())
|
||||
|
||||
new_root = ET.Element('testsuite')
|
||||
new_root.attrib['failures'] = '%s' % failures
|
||||
new_root.attrib['tests'] = '%s' % tests
|
||||
new_root.attrib['errors'] = '%s' % errors
|
||||
new_root.attrib['time'] = '%s' % time
|
||||
for case in cases:
|
||||
new_root.extend(case)
|
||||
new_tree = ET.ElementTree(new_root)
|
||||
ET.dump(new_tree)
|
||||
|
||||
|
||||
def usage():
|
||||
this_file = os.path.basename(__file__)
|
||||
print('Usage: %s results1.xml results2.xml' % this_file)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/make -f
|
||||
export PYBUILD_NAME=django-gssapi
|
||||
|
||||
%:
|
||||
dh $@ --with python2,python3 --buildsystem=pybuild
|
||||
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#! /usr/bin/env python
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools.command.sdist import sdist
|
||||
|
||||
|
||||
class eo_sdist(sdist):
|
||||
def run(self):
|
||||
print("creating VERSION file")
|
||||
if os.path.exists('VERSION'):
|
||||
os.remove('VERSION')
|
||||
version = get_version()
|
||||
version_file = open('VERSION', 'w')
|
||||
version_file.write(version)
|
||||
version_file.close()
|
||||
sdist.run(self)
|
||||
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- 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=.dirty','--match=v*'], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
result = p.communicate()[0]
|
||||
if p.returncode == 0:
|
||||
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:
|
||||
return '0.0.post%s' % len(
|
||||
subprocess.check_output(
|
||||
['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0'
|
||||
|
||||
setup(name="django-gssapi",
|
||||
version=get_version(),
|
||||
license="AGPLv3 or later",
|
||||
description="GSSAPI authentication for Django",
|
||||
long_description=open('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'),
|
||||
zip_safe=False,
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'six',
|
||||
'django>1.8',
|
||||
'gssapi',
|
||||
],
|
||||
package_dir={
|
||||
'': 'src',
|
||||
},
|
||||
package_data={
|
||||
'django_gssapi': [
|
||||
'templates/django_gssapi/*.html',
|
||||
'static/js/*.js',
|
||||
],
|
||||
},
|
||||
cmdclass={'sdist': eo_sdist})
|
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
|
@ -0,0 +1,78 @@
|
|||
# django-gssapi - SPNEGO/Kerberos authentication for Django applications
|
||||
# Copyright (C) 2014-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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
import gssapi
|
||||
import gssapi.exceptions
|
||||
import gssapi.raw as gb
|
||||
|
||||
|
||||
logger = logging.getLogger('django_gssapi')
|
||||
|
||||
|
||||
class GSSAPIBackend(object):
|
||||
def authenticate(self, request, gssapi_name):
|
||||
warnings.warn('example backend do not use in production!')
|
||||
User = get_user_model()
|
||||
try:
|
||||
user = User.objects.get(username=str(gssapi_name))
|
||||
except User.DoesNotExist:
|
||||
logger.debug('GSSAPI no user found for name %s', gssapi_name)
|
||||
else:
|
||||
if user.is_active:
|
||||
return user
|
||||
|
||||
|
||||
class KerberosPasswordBackend(object):
|
||||
def principal_from_user(self, user):
|
||||
return user.username
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
'''Verify username and password using Kerberos'''
|
||||
warnings.warn('Kerberos: example backend do not use in production!')
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
if username is None:
|
||||
username = kwargs.get(User.USERNAME_FIELD)
|
||||
try:
|
||||
user = User._default_manager.get_by_natural_key(username)
|
||||
except User.DoesNotExist:
|
||||
logger.debug('Kerberos: no user for username %s', username)
|
||||
return
|
||||
else:
|
||||
if not user.is_active:
|
||||
return
|
||||
|
||||
principal = self.principal_from_user(user)
|
||||
|
||||
try:
|
||||
name = gb.import_name(force_bytes(principal), gb.NameType.kerberos_principal)
|
||||
if gb.acquire_cred_with_password(name, force_bytes(password)):
|
||||
if not user.check_password(password):
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
except gssapi.exceptions.GSSError as e:
|
||||
logger.debug('Kerberos: password check failed for principal %s: %s', principal, e)
|
|
@ -0,0 +1,68 @@
|
|||
# django-gssapi - SPNEGO/Kerberos authentication for Django applications
|
||||
# Copyright (C) 2014-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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import rest_framework.authentication
|
||||
|
||||
from . import utils
|
||||
|
||||
__all__ = ['GSSAPIAuthentication', 'add_gssapi_mutual_auth']
|
||||
|
||||
|
||||
class GSSAPIAuthentication(rest_framework.authentication.BaseAuthentication):
|
||||
def get_gssapi_store(self):
|
||||
return getattr(settings, 'GSSAPI_STORE', None)
|
||||
|
||||
def get_gssapi_name(self):
|
||||
# force a service name, ex. :
|
||||
# server_name = 'HTTP@%s' % self.request.get_host()
|
||||
# return gssapi.Name(server_name, name_type=gssapi.NameType.hostbased_service)
|
||||
# without one, any service name in keytab will do
|
||||
return getattr(settings, 'GSSAPI_NAME', None)
|
||||
|
||||
def authenticate(self, request):
|
||||
try:
|
||||
user, gss_name, token = utils.negotiate_and_auth(
|
||||
request,
|
||||
name=self.get_gssapi_name(),
|
||||
store=self.get_gssapi_store())
|
||||
except utils.NegotiateContinue as e:
|
||||
token = e.token
|
||||
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
# DRF authentication does not allow to implement
|
||||
# natively mutual GSSAPI authentication as we
|
||||
# cannot modify the response, if needed views can retrieve the result
|
||||
# token and set it on their response
|
||||
request._drf_gssapi_token = token
|
||||
return user, gss_name
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return utils.authenticate_header(token=get_gssapi_token(request))
|
||||
|
||||
|
||||
def add_gssapi_mutual_auth(request, response):
|
||||
token = get_gssapi_token(request)
|
||||
if token is not None:
|
||||
response['WWW-Authenticate'] = utils.authenticate_header(token=token)
|
||||
|
||||
|
||||
def get_gssapi_token(request):
|
||||
return getattr(request, '_drf_gssapi_token', None)
|
|
@ -0,0 +1,23 @@
|
|||
# django-gssapi - SPNEGO/Kerberos authentication for Django applications
|
||||
# Copyright (C) 2014-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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^login/$', views.login, name='gssapi-login'),
|
||||
]
|
|
@ -0,0 +1,89 @@
|
|||
# django-gssapi - SPNEGO/Kerberos authentication for Django applications
|
||||
# Copyright (C) 2014-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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import logging
|
||||
|
||||
import gssapi
|
||||
import gssapi.exceptions
|
||||
|
||||
from django import http
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
logger = logging.getLogger('django_kerberos')
|
||||
|
||||
|
||||
class NegotiateContinue(Exception):
|
||||
def __init__(self, token):
|
||||
self.token = token
|
||||
|
||||
|
||||
def negotiate(request, name=None, store=None):
|
||||
'''Try to authenticate the user using SPNEGO and Kerberos'''
|
||||
|
||||
if name:
|
||||
logger.debug(u'GSSAPI negotiate using name %s', name)
|
||||
|
||||
try:
|
||||
server_creds = gssapi.Credentials(usage='accept', name=name, store=store)
|
||||
except gssapi.exceptions.GSSError as e:
|
||||
logging.debug('GSSAPI credentials failure: %s', e)
|
||||
return None, None
|
||||
|
||||
if not request.META.get('HTTP_AUTHORIZATION', '').startswith('Negotiate '):
|
||||
return None, None
|
||||
|
||||
authstr = request.META['HTTP_AUTHORIZATION'][10:]
|
||||
try:
|
||||
in_token = base64.b64decode(authstr)
|
||||
except ValueError:
|
||||
return None, None
|
||||
|
||||
server_ctx = gssapi.SecurityContext(creds=server_creds, usage='accept')
|
||||
try:
|
||||
out_token = server_ctx.step(in_token)
|
||||
except gssapi.exceptions.GSSError as e:
|
||||
logging.debug('GSSAPI security context failure: %s', e)
|
||||
if not server_ctx.complete:
|
||||
raise NegotiateContinue(out_token)
|
||||
|
||||
return server_ctx.initiator_name, out_token
|
||||
|
||||
|
||||
def negotiate_and_auth(request, name=None, store=None):
|
||||
gssapi_name, token = negotiate(request, name=name, store=store)
|
||||
|
||||
if gssapi_name is None:
|
||||
return None, None, token
|
||||
|
||||
return authenticate(gssapi_name=gssapi_name), gssapi_name, token
|
||||
|
||||
|
||||
def challenge_response():
|
||||
'''Send negotiate challenge'''
|
||||
response = http.HttpResponse('GSSAPI authentication failed', status=401)
|
||||
response_add_www_authenticate(response)
|
||||
return response
|
||||
|
||||
|
||||
def authenticate_header(token=None):
|
||||
return 'Negotiate%s' % (' ' + base64.b64encode(token).decode('ascii') if token else '')
|
||||
|
||||
|
||||
def response_add_www_authenticate(response, token=None):
|
||||
response['WWW-Authenticate'] = authenticate_header(token)
|
|
@ -0,0 +1,102 @@
|
|||
# django-gssapi - SPNEGO/Kerberos authentication for Django applications
|
||||
# Copyright (C) 2014-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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from django import http
|
||||
from django.conf import settings
|
||||
from django.utils.http import is_safe_url
|
||||
from django.views.generic.base import View
|
||||
|
||||
from django.contrib.auth import authenticate, login as auth_login, REDIRECT_FIELD_NAME
|
||||
|
||||
from . import utils
|
||||
|
||||
logger = logging.getLogger('django_kerberos')
|
||||
|
||||
|
||||
class NegotiateFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LoginView(View):
|
||||
redirect_field_name = REDIRECT_FIELD_NAME
|
||||
|
||||
def get_success_url_allowed_hosts(self):
|
||||
return {self.request.get_host()}
|
||||
|
||||
def get_redirect_url(self):
|
||||
"""Return the user-originating redirect URL if it's safe."""
|
||||
redirect_to = self.request.POST.get(
|
||||
self.redirect_field_name,
|
||||
self.request.GET.get(self.redirect_field_name, '')
|
||||
)
|
||||
url_is_safe = is_safe_url(
|
||||
url=redirect_to,
|
||||
allowed_hosts=self.get_success_url_allowed_hosts(),
|
||||
require_https=self.request.is_secure(),
|
||||
)
|
||||
return redirect_to if url_is_safe else settings.LOGIN_REDIRECT_URL
|
||||
|
||||
def get_gssapi_store(self):
|
||||
return getattr(settings, 'GSSAPI_STORE', None)
|
||||
|
||||
def get_gssapi_name(self):
|
||||
# force a service name, ex. :
|
||||
# server_name = 'HTTP@%s' % self.request.get_host()
|
||||
# return gssapi.Name(server_name, name_type=gssapi.NameType.hostbased_service)
|
||||
# without one, any service name in keytab will do
|
||||
return getattr(settings, 'GSSAPI_NAME', None)
|
||||
|
||||
def challenge(self):
|
||||
'''Send negotiate challenge'''
|
||||
return utils.challenge_response()
|
||||
|
||||
def success(self, user):
|
||||
'''Do something with the user we found'''
|
||||
auth_login(self.request, user)
|
||||
return http.HttpResponseRedirect(self.get_redirect_url())
|
||||
|
||||
def negotiate(self):
|
||||
'''Try to authenticate the user using SPNEGO'''
|
||||
|
||||
try:
|
||||
user, gss_name, token = utils.negotiate_and_auth(
|
||||
self.request,
|
||||
name=self.get_gssapi_name(),
|
||||
store=self.get_gssapi_store())
|
||||
except utils.NegotiateContinue as e:
|
||||
token = e.token
|
||||
|
||||
if user is None:
|
||||
response = self.challenge()
|
||||
else:
|
||||
logger.debug('GSSAPI found user %s for name %s', user, gss_name)
|
||||
response = self.success(user)
|
||||
|
||||
utils.response_add_www_authenticate(response, token)
|
||||
return response
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.negotiate()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
return self.negotiate()
|
||||
|
||||
login = LoginView.as_view()
|
|
@ -0,0 +1,62 @@
|
|||
# django-gssapi - SPNEGO/Kerberos authentication for Django applications
|
||||
# Copyright (C) 2014-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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import os
|
||||
import pytest
|
||||
|
||||
import gssapi
|
||||
|
||||
import k5test
|
||||
import k5test._utils
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def k5env():
|
||||
k5realm = k5test.K5Realm()
|
||||
old_environ = os.environ.copy()
|
||||
try:
|
||||
|
||||
os.environ.update(k5realm.env)
|
||||
k5realm.http_princ = 'HTTP/testserver@%s' % k5realm.realm
|
||||
k5realm.addprinc(k5realm.http_princ)
|
||||
k5realm.extract_keytab(k5realm.http_princ, k5realm.keytab)
|
||||
|
||||
def spnego():
|
||||
service_name = gssapi.Name(k5realm.http_princ)
|
||||
service_name.canonicalize(gssapi.MechType.kerberos)
|
||||
|
||||
# first attempt
|
||||
ctx = gssapi.SecurityContext(usage='initiate', name=service_name)
|
||||
return 'Negotiate %s' % base64.b64encode(ctx.step()).decode('ascii')
|
||||
k5realm.spnego = spnego
|
||||
yield k5realm
|
||||
finally:
|
||||
os.environ.clear()
|
||||
os.environ.update(old_environ)
|
||||
k5realm.stop()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_princ(k5env):
|
||||
return User.objects.create(username=k5env.user_princ)
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
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',
|
||||
'django.contrib.sessions',
|
||||
'django_gssapi',
|
||||
)
|
||||
TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), 'templates'),)
|
||||
ROOT_URLCONF = 'django_gssapi.urls'
|
||||
SECRET_KEY = 'xxx'
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django_gssapi.backends.GSSAPIBackend',
|
||||
'django_gssapi.backends.KerberosPasswordBackend',
|
||||
)
|
|
@ -0,0 +1,2 @@
|
|||
{% block content %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,31 @@
|
|||
# django-gssapi - SPNEGO/Kerberos authentication for Django applications
|
||||
# Copyright (C) 2014-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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from django.contrib.auth import get_user_model, authenticate
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def test_kerberos_password(k5env, db):
|
||||
user = User.objects.create(username=k5env.user_princ)
|
||||
|
||||
k5env.run(['kdestroy'])
|
||||
|
||||
assert authenticate(username=k5env.user_princ, password='nogood') is None
|
||||
assert authenticate(username=k5env.user_princ, password=k5env.password('user')) == user
|
||||
assert not os.path.exists(k5env.ccache)
|
|
@ -0,0 +1,59 @@
|
|||
# django-gssapi - SPNEGO/Kerberos authentication for Django applications
|
||||
# Copyright (C) 2014-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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from django_gssapi.drf import GSSAPIAuthentication, add_gssapi_mutual_auth
|
||||
|
||||
|
||||
class RestView(APIView):
|
||||
authentication_classes = [GSSAPIAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
response = Response({
|
||||
'user': str(request.user),
|
||||
'auth': str(request.auth),
|
||||
})
|
||||
add_gssapi_mutual_auth(request, response)
|
||||
return response
|
||||
|
||||
view = RestView.as_view()
|
||||
|
||||
|
||||
def test_gssapi_authentication_no_auth(k5env, db, rf):
|
||||
request = rf.get('/')
|
||||
response = view(request)
|
||||
assert response.status_code == 401
|
||||
assert response['WWW-Authenticate'] == 'Negotiate'
|
||||
|
||||
|
||||
def test_gssapi_authentication_no_user(k5env, db, rf):
|
||||
request = rf.get('/', HTTP_AUTHORIZATION=k5env.spnego())
|
||||
response = view(request)
|
||||
assert response.status_code == 401
|
||||
assert response['WWW-Authenticate'] == 'Negotiate'
|
||||
|
||||
|
||||
def test_gssapi_authentication_passing(k5env, db, rf, user_princ):
|
||||
request = rf.get('/', HTTP_AUTHORIZATION=k5env.spnego())
|
||||
response = view(request)
|
||||
assert response.status_code == 200
|
||||
assert response.data['user'] == k5env.user_princ
|
||||
assert response.data['auth'] == k5env.user_princ
|
||||
assert response['WWW-Authenticate'].startswith('Negotiate ')
|
|
@ -0,0 +1,49 @@
|
|||
# django-gssapi - SPNEGO/Kerberos authentication for Django applications
|
||||
# Copyright (C) 2014-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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
import gssapi
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def test_login(k5env, client, caplog, db, settings):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
response = client.get('/login/')
|
||||
assert response.status_code == 401
|
||||
|
||||
response = client.get('/login/', HTTP_AUTHORIZATION=k5env.spnego())
|
||||
|
||||
assert response.status_code == 401
|
||||
assert '_auth_user_id' not in client.session
|
||||
|
||||
# create an user...
|
||||
User.objects.create(username=k5env.user_princ)
|
||||
|
||||
# and retry.
|
||||
response = client.get('/login/', HTTP_AUTHORIZATION=k5env.spnego())
|
||||
|
||||
assert response.status_code == 302
|
||||
assert client.session['_auth_user_id']
|
||||
|
||||
# break service name resolution
|
||||
settings.GSSAPI_NAME = gssapi.Name('HTTP@localhost', gssapi.NameType.hostbased_service)
|
||||
response = client.get('/login/', HTTP_AUTHORIZATION=k5env.spnego())
|
||||
assert response.status_code == 401
|
|
@ -0,0 +1,45 @@
|
|||
# Tox (http://tox.testrun.org/) is a tool for running tests
|
||||
# in multiple virtualenvs. This configuration file will run the
|
||||
# test suite on all supported python versions. To use it, "pip install tox"
|
||||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/django-gssapi/{env:BRANCH_NAME:}
|
||||
envlist = py27-coverage-dj111-{stretch,},py3-coverage-{dj111,dj20,djlast},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
|
||||
DB_ENGINE=django.db.backends.sqlite3
|
||||
usedevelop = true
|
||||
deps =
|
||||
stretch: gssapi<1.2.3
|
||||
dj18: django>1.8,<1.9
|
||||
dj18: django-tables2<1.1
|
||||
dj111: django<2.0
|
||||
dj20: django<2.1
|
||||
djlast: django
|
||||
pytest<4.2
|
||||
pytest-mock
|
||||
pytest-django
|
||||
pytest-cov
|
||||
k5test
|
||||
django-rest-framework
|
||||
commands =
|
||||
py.test {env:COVERAGE:} -o junit_suite_name={envname} --junit-xml=junit-{envname}.xml {posargs:tests}
|
||||
|
||||
[testenv:pylint]
|
||||
deps =
|
||||
pylint<1.8
|
||||
pylint-django<0.8.1
|
||||
commands =
|
||||
pylint: ./pylint.sh src/django_gssapi/
|
||||
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore:Kerberos. example backend.*
|
Loading…
Reference in New Issue