1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-09 10:50:24 +00:00

[thirdpart] Add shibboleth

This commit is contained in:
zhengxie
2014-12-17 14:29:29 +08:00
parent c2a07137ef
commit c914fe30c5
12 changed files with 458 additions and 0 deletions

View File

View File

@@ -0,0 +1,29 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
#At a minimum you will need username,
default_shib_attributes = {
"Shibboleth-eppn": (True, "username"),
}
SHIB_ATTRIBUTE_MAP = getattr(settings, 'SHIBBOLETH_ATTRIBUTE_MAP', default_shib_attributes)
#Set to true if you are testing and want to insert sample headers.
SHIB_MOCK_HEADERS = getattr(settings, 'SHIBBOLETH_MOCK_HEADERS', False)
LOGIN_URL = getattr(settings, 'LOGIN_URL', None)
if not LOGIN_URL:
raise ImproperlyConfigured("A LOGIN_URL is required. Specify in settings.py")
#Optional logout parameters
#This should look like: https://sso.school.edu/idp/logout.jsp?return=%s
#The return url variable will be replaced in the LogoutView.
LOGOUT_URL = getattr(settings, 'SHIBBOLETH_LOGOUT_URL', None)
#LOGOUT_REDIRECT_URL specifies a default logout page that will always be used when
#users logout from Shibboleth.
LOGOUT_REDIRECT_URL = getattr(settings, 'SHIBBOLETH_LOGOUT_REDIRECT_URL', None)
#Name of key. Probably no need to change this.
LOGOUT_SESSION_KEY = getattr(settings, 'SHIBBOLETH_FORCE_REAUTH_SESSION_KEY', 'shib_force_reauth')

View File

@@ -0,0 +1,46 @@
from django.db import connection
from django.contrib.auth.models import User, Permission
from django.contrib.auth.backends import RemoteUserBackend
class ShibbolethRemoteUserBackend(RemoteUserBackend):
"""
This backend is to be used in conjunction with the ``RemoteUserMiddleware``
found in the middleware module of this package, and is used when the server
is handling authentication outside of Django.
By default, the ``authenticate`` method creates ``User`` objects for
usernames that don't already exist in the database. Subclasses can disable
this behavior by setting the ``create_unknown_user`` attribute to
``False``.
"""
# Create a User object if not already in the database?
create_unknown_user = True
def authenticate(self, remote_user, shib_meta):
"""
The username passed as ``remote_user`` is considered trusted. This
method simply returns the ``User`` object with the given username,
creating a new ``User`` object if ``create_unknown_user`` is ``True``.
Returns None if ``create_unknown_user`` is ``False`` and a ``User``
object with the given username is not found in the database.
"""
if not remote_user:
return
user = None
username = self.clean_username(remote_user)
shib_user_params = dict([(k, shib_meta[k]) for k in User._meta.get_all_field_names() if k in shib_meta])
# Note that this could be accomplished in one try-except clause, but
# instead we use get_or_create when creating unknown users since it has
# built-in safeguards for multiple threads.
if self.create_unknown_user:
user, created = User.objects.get_or_create(username=shib_user_params.get('username'), defaults=shib_user_params)
if created:
user = self.configure_user(user)
else:
try:
user = User.objects.get(**shib_user_params)
except User.DoesNotExist:
pass
return user

View File

@@ -0,0 +1,26 @@
from django.core.urlresolvers import reverse
from urllib import quote
def login_link(request):
"""
This assumes your login link is the Shibboleth login page for your server
and uses the 'target' url parameter.
"""
full_path = quote(request.get_full_path())
login = reverse('shibboleth:login')
ll = "%s?target=%s" % (login, full_path)
return { 'login_link': ll }
def logout_link(request, *args):
"""
This assumes your login link is the Shibboleth login page for your server
and uses the 'target' url parameter.
e.g: https://school.edu/Shibboleth.sso/Login
"""
from app_settings import LOGOUT_URL, LOGOUT_REDIRECT_URL
#LOGOUT_REDIRECT_URL specifies a default logout page that will always be used when
#users logout from Shibboleth.
target = LOGOUT_REDIRECT_URL or quote(request.build_absolute_uri())
logout = reverse('shibboleth:logout')
ll = "%s?target=%s" % (logout, target)
return { 'logout_link': ll }

View File

@@ -0,0 +1,23 @@
"""
Decorators to use with Shibboleth.
"""
from django.conf import settings
from django.contrib import auth
from middleware import ShibbolethRemoteUserMiddleware
def login_optional(func):
"""
Decorator to pull Shib attributes and log user in, if possible. Does not
enforce login.
"""
def decorator(request,*args, **kwargs):
#Do nothing if the remoteuser backend isn't activated
if 'shibboleth.backends.ShibbolethRemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS:
pass
else:
shib = ShibbolethRemoteUserMiddleware()
#Proccess the request with the Shib middlemare, which will log the
#user in if we can.
proc = shib.process_request(request)
return func(request, *args, **kwargs)
return decorator

View File

@@ -0,0 +1,102 @@
from django.contrib.auth.middleware import RemoteUserMiddleware
from django.contrib import auth
from django.core.exceptions import ImproperlyConfigured
from shibboleth.app_settings import SHIB_ATTRIBUTE_MAP, LOGOUT_SESSION_KEY
class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware):
"""
Authentication Middleware for use with Shibboleth. Uses the recommended pattern
for remote authentication from: http://code.djangoproject.com/svn/django/tags/releases/1.3/django/contrib/auth/middleware.py
"""
def process_request(self, request):
# AuthenticationMiddleware is required so that request.user exists.
if not hasattr(request, 'user'):
raise ImproperlyConfigured(
"The Django remote user auth middleware requires the"
" authentication middleware to be installed. Edit your"
" MIDDLEWARE_CLASSES setting to insert"
" 'django.contrib.auth.middleware.AuthenticationMiddleware'"
" before the RemoteUserMiddleware class.")
#To support logout. If this variable is True, do not
#authenticate user and return now.
if request.session.get(LOGOUT_SESSION_KEY) == True:
return
else:
#Delete the shib reauth session key if present.
request.session.pop(LOGOUT_SESSION_KEY, None)
#Locate the remote user header.
try:
username = request.META[self.header]
except KeyError:
# If specified header doesn't exist then return (leaving
# request.user set to AnonymousUser by the
# AuthenticationMiddleware).
return
# If the user is already authenticated and that user is the user we are
# getting passed in the headers, then the correct user is already
# persisted in the session and we don't need to continue.
if request.user.is_authenticated():
if request.user.username == self.clean_username(username, request):
return
# Make sure we have all required Shiboleth elements before proceeding.
shib_meta, error = self.parse_attributes(request)
# Add parsed attributes to the session.
request.session['shib'] = shib_meta
if error:
raise ShibbolethValidationError("All required Shibboleth elements"
" not found. %s" % shib_meta)
# We are seeing this user for the first time in this session, attempt
# to authenticate the user.
user = auth.authenticate(remote_user=username, shib_meta=shib_meta)
if user:
# User is valid. Set request.user and persist user in the session
# by logging the user in.
request.user = user
auth.login(request, user)
user.set_unusable_password()
user.save()
# call make profile.
self.make_profile(user, shib_meta)
#setup session.
self.setup_session(request)
def make_profile(self, user, shib_meta):
"""
This is here as a stub to allow subclassing of ShibbolethRemoteUserMiddleware
to include a make_profile method that will create a Django user profile
from the Shib provided attributes. By default it does nothing.
"""
return
def setup_session(self, request):
"""
If you want to add custom code to setup user sessions, you
can extend this.
"""
return
def parse_attributes(self, request):
"""
Parse the incoming Shibboleth attributes.
From: https://github.com/russell/django-shibboleth/blob/master/django_shibboleth/utils.py
Pull the mapped attributes from the apache headers.
"""
shib_attrs = {}
error = False
meta = request.META
for header, attr in SHIB_ATTRIBUTE_MAP.items():
required, name = attr
value = meta.get(header, None)
shib_attrs[name] = value
if not value or value == '':
if required:
error = True
return shib_attrs, error
class ShibbolethValidationError(Exception):
pass

1
thirdpart/shibboleth/models.py Executable file
View File

@@ -0,0 +1 @@
#intentionally left blank

View File

@@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
<head>
</head>
<body>
<div>
<h1>User stuff</h1>
<p>The mapped shib attributes will display here.</p>
<ul>
<li>username: {{ user.username }}</li>
<li>name: {{ user.first_name }} {{ user.last_name }}</li>
<li>email: {{ user.email }}</li>
</ul>
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
from shib import *

View File

@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
import os
from django.conf import settings
from django.utils import unittest
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.client import Client
SAMPLE_HEADERS = {
"REMOTE_USER": 'devloper@school.edu',
"Shib-Application-ID": "default",
"Shib-Authentication-Method": "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified",
"Shib-AuthnContext-Class": "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified",
"Shib-Identity-Provider": "https://sso.college.edu/idp/shibboleth",
"Shib-Session-ID": "1",
"Shib-Session-Index": "12",
"Shibboleth-affiliation": "member@college.edu;staff@college.edu",
"Shibboleth-schoolBarCode": "12345678",
"Shibboleth-schoolNetId": "Sample_Developer",
"Shibboleth-schoolStatus": "active",
"Shibboleth-department": "University Library, Integrated Technology Services",
"Shibboleth-displayName": "Sample Developer",
"Shibboleth-eppn": "sampledeveloper@school.edu",
"Shibboleth-givenName": "Sample",
"Shibboleth-isMemberOf": "SCHOOL:COMMUNITY:EMPLOYEE:ADMINISTRATIVE:BASE;SCHOOL:COMMUNITY:EMPLOYEE:STAFF:SAC:P;COMMUNITY:ALL;SCHOOL:COMMUNITY:EMPLOYEE:STAFF:SAC:M;",
"Shibboleth-mail": "Sample_Developer@school.edu",
"Shibboleth-persistent-id": "https://sso.college.edu/idp/shibboleth!https://server.college.edu/shibboleth-sp!sk1Z9qKruvXY7JXvsq4GRb8GCUk=",
"Shibboleth-sn": "Developer",
"Shibboleth-title": "Library Developer",
"Shibboleth-unscoped-affiliation": "member;staff"
}
settings.SHIBBOLETH_ATTRIBUTE_MAP = {
"Shib-Identity-Provider": (True, "idp"),
"Shibboleth-mail": (True, "email"),
"Shibboleth-eppn": (True, "username"),
"Shibboleth-schoolStatus": (True, "status"),
"Shibboleth-affiliation": (True, "affiliation"),
"Shib-Session-ID": (True, "session_id"),
"Shibboleth-givenName": (True, "first_name"),
"Shibboleth-sn": (True, "last_name"),
"Shibboleth-mail": (True, "email"),
"Shibboleth-schoolBarCode": (False, "barcode")
}
settings.AUTHENTICATION_BACKENDS += (
'shibboleth.backends.ShibbolethRemoteUserBackend',
)
settings.MIDDLEWARE_CLASSES += (
'shibboleth.middleware.ShibbolethRemoteUserMiddleware',
)
settings.ROOT_URLCONF = 'shibboleth.urls'
settings.SHIBBOLETH_LOGOUT_URL = 'https://sso.school.edu/logout?next=%s'
settings.SHIBBOLETH_LOGOUT_REDIRECT_URL = 'http://school.edu/'
from shibboleth.views import ShibbolethView
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
class AttributesTest(unittest.TestCase):
def setUp(self):
self.c = Client()
def test_decorator_not_authenticated(self):
"""
"""
resp = self.c.get('/')
self.assertEqual(resp.status_code, 302)
#Test the context - shouldn't exist
self.assertEqual(resp.context, None)
def test_decorator_authenticated(self):
"""
"""
resp = self.c.get('/', **SAMPLE_HEADERS)
self.assertEqual(resp.status_code, 200)
#Test the context
user = resp.context.get('user')
self.assertEqual(user.email, 'Sample_Developer@school.edu')
self.assertEqual(user.first_name, 'Sample')
self.assertEqual(user.last_name, 'Developer')
self.assertTrue(user.is_authenticated())
self.assertFalse(user.is_anonymous())
class LogoutTest(unittest.TestCase):
def setUp(self):
self.c = Client()
def test_logout(self):
"""
"""
from shibboleth import app_settings
#Login
login = self.c.get('/', **SAMPLE_HEADERS)
self.assertEqual(login.status_code, 200)
#Logout
logout = self.c.get('/logout/', **SAMPLE_HEADERS)
self.assertEqual(logout.status_code, 302)
#Ensure redirect happened.
self.assertEqual(
logout['Location'],
'https://sso.school.edu/logout?next=http://school.edu/'
)
#Check to see if the session has the force logout key.
self.assertTrue(self.c.session.get(app_settings.LOGOUT_SESSION_KEY))
#Load root url to see if user is in fact logged out.
resp = self.c.get('/', **SAMPLE_HEADERS)
self.assertEqual(resp.status_code, 302)
#Make sure the context is empty.
self.assertEqual(resp.context, None)

14
thirdpart/shibboleth/urls.py Executable file
View File

@@ -0,0 +1,14 @@
from distutils.version import StrictVersion
import django
if StrictVersion(django.get_version()) < StrictVersion('1.4'):
from django.conf.urls.defaults import *
else:
from django.conf.urls import patterns, url
from views import ShibbolethView, ShibbolethLogoutView, ShibbolethLoginView
urlpatterns = patterns('',
url(r'^login/$', ShibbolethLoginView.as_view(), name='login'),
url(r'^logout/$', ShibbolethLogoutView.as_view(), name='logout'),
url(r'^$', ShibbolethView.as_view(), name='info'),
)

80
thirdpart/shibboleth/views.py Executable file
View File

@@ -0,0 +1,80 @@
from django.conf import settings
from django.contrib import auth
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView
from urllib import quote
#Logout settings.
from shibboleth.app_settings import LOGOUT_URL, LOGOUT_REDIRECT_URL, LOGOUT_SESSION_KEY
class ShibbolethView(TemplateView):
"""
This is here to offer a Shib protected page that we can
route users through to login.
"""
template_name = 'shibboleth/user_info.html'
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
"""
Django docs say to decorate the dispatch method for
class based views.
https://docs.djangoproject.com/en/dev/topics/auth/
"""
return super(ShibbolethView, self).dispatch(request, *args, **kwargs)
def get(self, request, **kwargs):
"""Process the request."""
next = self.request.GET.get('next', None)
if next is not None:
return redirect(next)
return super(ShibbolethView, self).get(request)
def get_context_data(self, **kwargs):
context = super(ShibbolethView, self).get_context_data(**kwargs)
context['user'] = self.request.user
return context
class ShibbolethLoginView(TemplateView):
"""
Pass the user to the Shibboleth login page.
Some code borrowed from:
https://github.com/stefanfoulis/django-class-based-auth-views.
"""
redirect_field_name = "target"
def get(self, *args, **kwargs):
#Remove session value that is forcing Shibboleth reauthentication.
self.request.session.pop(LOGOUT_SESSION_KEY, None)
login = settings.LOGIN_URL + '?target=%s' % quote(self.request.GET.get(self.redirect_field_name))
return redirect(login)
class ShibbolethLogoutView(TemplateView):
"""
Pass the user to the Shibboleth logout page.
Some code borrowed from:
https://github.com/stefanfoulis/django-class-based-auth-views.
"""
redirect_field_name = "target"
def get(self, *args, **kwargs):
#Log the user out.
auth.logout(self.request)
#Set session key that middleware will use to force
#Shibboleth reauthentication.
self.request.session[LOGOUT_SESSION_KEY] = True
#Get target url in order of preference.
target = LOGOUT_REDIRECT_URL or\
quote(self.request.GET.get(self.redirect_field_name)) or\
quote(request.build_absolute_uri())
logout = LOGOUT_URL % target
return redirect(logout)