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:
0
thirdpart/shibboleth/__init__.py
Executable file
0
thirdpart/shibboleth/__init__.py
Executable file
29
thirdpart/shibboleth/app_settings.py
Executable file
29
thirdpart/shibboleth/app_settings.py
Executable 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')
|
||||
|
||||
|
46
thirdpart/shibboleth/backends.py
Normal file
46
thirdpart/shibboleth/backends.py
Normal 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
|
26
thirdpart/shibboleth/context_processors.py
Executable file
26
thirdpart/shibboleth/context_processors.py
Executable 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 }
|
23
thirdpart/shibboleth/decorators.py
Executable file
23
thirdpart/shibboleth/decorators.py
Executable 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
|
102
thirdpart/shibboleth/middleware.py
Executable file
102
thirdpart/shibboleth/middleware.py
Executable 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
1
thirdpart/shibboleth/models.py
Executable file
@@ -0,0 +1 @@
|
||||
#intentionally left blank
|
18
thirdpart/shibboleth/templates/shibboleth/user_info.html
Executable file
18
thirdpart/shibboleth/templates/shibboleth/user_info.html
Executable 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>
|
||||
|
||||
|
1
thirdpart/shibboleth/tests/__init__.py
Executable file
1
thirdpart/shibboleth/tests/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
from shib import *
|
118
thirdpart/shibboleth/tests/shib.py
Executable file
118
thirdpart/shibboleth/tests/shib.py
Executable 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
14
thirdpart/shibboleth/urls.py
Executable 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
80
thirdpart/shibboleth/views.py
Executable 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)
|
||||
|
||||
|
Reference in New Issue
Block a user