Commit e99f4a99 authored by Ben Waters's avatar Ben Waters

initial

parents
PREFIX=/usr/local
PYTHON=python
all: build
build:
$(PYTHON) setup.py build
clean:
$(PYTHON) setup.py clean --all
find . -name '*.py[co]' -exec rm -f "{}" ';'
rm -rf build dist django_cas.egg-info temp
install:
$(PYTHON) setup.py install --prefix="$(PREFIX)"
= Django CAS =
`django_gmucas` is a [http://www.ja-sig.org/products/cas/ CAS] 1.0 and CAS 2.0
authentication backend for [http://www.djangoproject.com/ Django] at George Mason University. It allows
you to use Django's built-in authentication mechanisms and `User` model while
adding support for CAS.
It also includes a middleware that intercepts calls to the original login
and logout pages and forwards them to the CASified versions, and adds
CAS support to the admin interface.
This is solely meant for GMU usage since the settings have been changed for Geoge Mason University's CAS authentication system. This is a fork of https://bitbucket.org/cpcc/django-cas/src. Credit to its authors at CPCC. Maintained by GMU SRCT.
== Installation ==
Run `python setup.py install`, or place the `django_gmucas` directory in your
`PYTHONPATH` directly. (Note: If you're using Python 2.4 or older, you'll need
to install [http://pypi.python.org/pypi/elementtree/ ElementTree] to use
CAS 2.0 functionality.)
Now add it to the middleware and authentication backends in your settings.
Make sure you also have the authentication middleware installed. Here's what
mine looks like:
{{{
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django_cas.middleware.CASMiddleware',
'django.middleware.doc.XViewMiddleware',
)
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'django_cas.backends.CASBackend',
)
}}}
Optional settings include:
* `CAS_ADMIN_PREFIX`: The URL prefix of the Django administration site.
If undefined, the CAS middleware will check the view being rendered to
see if it lives in `django.contrib.admin.views`.
* `CAS_EXTRA_LOGIN_PARAMS`: Extra URL parameters to add to the login URL
when redirecting the user.
* `CAS_IGNORE_REFERER`: If `True`, logging out of the application will
always send the user to the URL specified by `CAS_REDIRECT_URL`.
* `CAS_LOGOUT_COMPLETELY`: If `False`, logging out of the application
won't log the user out of CAS as well.
* `CAS_REDIRECT_URL`: Where to send a user after logging in or out if
there is no referrer and no next page set. Default is `/`.
* `CAS_RETRY_LOGIN`: If `True` and an unknown or invalid ticket is
received, the user is redirected back to the login page.
* `CAS_VERSION`: The CAS protocol version to use. `'1'` and `'2'` are
supported, with `'2'` being the default.
Make sure your project knows how to log users in and out by adding these to
your URL mappings:
{{{
(r'^login/$', 'django_gmucas.views.login'),
(r'^logout/$', 'django_gmucas.views.logout'),
}}}
Users should now be able to log into your site (and staff into the
administration interface) using CAS.
== Managing Access to the Admin Interface ==
At the moment, the best way to give a user access to the admin interface is
by doing one of the following:
* Create the initial superuser account with a username that matches the
desired user. `django_cas` will be able to make use of the existing
user.
* Similarly, create database fixtures for the superusers, and load them
when deploying the application.
* Ask the user to sign in to the application and, as an admin, log into
the admin interface and change their access through the Users table.
== Populating User Data ==
To add user data, subclass `CASBackend` and specify that as your
application's backend.
For example:
{{{
from django_cas.backends import CASBackend
class PopulatedCASBackend(CASBackend):
"""CAS authentication backend with user data populated from AD"""
def authenticate(self, ticket, service):
"""Authenticates CAS ticket and retrieves user data"""
user = super(PopulatedCASBackend, self).authenticate(
ticket, service)
# Connect to AD, modify user object, etc.
return user
}}}
== Preventing Infinite Redirects ==
Django's current implementation of its `permission_required` and
`user_passes_test` decorators (in `django.contrib.auth.decorators`) has a
known issue that can cause users to experience infinite redirects. The
decorators return the user to the login page, even if they're already logged
in, which causes a loop with SSO services like CAS.
`django_cas` provides fixed versions of these decorators in
`django_cas.decorators`. Usage is unchanged, and in the event that this issue
is fixed, the decorators should still work without issue.
For more information see http://code.djangoproject.com/ticket/4617.
== Customizing the 403 Error Page ==
Django doesn't provide a simple way to customize 403 error pages, so you'll
have to make a response middleware that handles `HttpResponseForbidden`.
For example, in `views.py`:
{{{
from django.http import HttpResponseForbidden
from django.template import RequestContext, loader
def forbidden(request, template_name='403.html'):
"""Default 403 handler"""
t = loader.get_template(template_name)
return HttpResponseForbidden(t.render(RequestContext(request)))
}}}
And in `middleware.py`:
{{{
from django.http import HttpResponseForbidden
from yourapp.views import forbidden
class Custom403Middleware(object):
"""Catches 403 responses and renders 403.html"""
def process_response(self, request, response):
if isinstance(response, HttpResponseForbidden):
return forbidden(request)
else:
return response
}}}
Now add `yourapp.middleware.Custom403Middleware` to your `MIDDLEWARE_CLASSES`
setting and create a template named `403.html`.
== CAS 2.0 support ==
The CAS 2.0 protocol is supported in the same way that 1.0 is; no extensions
or new features from the CAS 2.0 specification are implemented. `elementtree`
is required to use this functionality. (`elementtree` is also included in
Python 2.5's standard library.)
Note: The CAS 3.x server uses the CAS 2.0 protocol. There is no CAS 3.0
protocol, though the CAS 3.x server does allow extensions to the protocol.
== Differences Between Django CAS 1.0 and 2.0 ==
Version 2.0 of `django_cas` breaks compatibility in some small ways, in order
simplify the library. The following settings have been removed:
* `CAS_LOGIN_URL` and `CAS_LOGOUT_URL`: Version 2.0 is capable of
determining these automatically.
* `CAS_POPULATE_USER`: Subclass `CASBackend` instead (see above).
* `CAS_REDIRECT_FIELD_NAME`: Django's own `REDIRECT_FIELD_NAME` is now
used unconditionally.
"""Django CAS 1.0/2.0 authentication backend"""
from django.conf import settings
__all__ = []
_DEFAULTS = {
'CAS_ADMIN_PREFIX': None,
'CAS_EXTRA_LOGIN_PARAMS': None,
'CAS_IGNORE_REFERER': False,
'CAS_LOGOUT_COMPLETELY': True,
'CAS_REDIRECT_URL': '/',
'CAS_RETRY_LOGIN': False,
'CAS_SERVER_URL': None,
'CAS_VERSION': '2',
}
for key, value in _DEFAULTS.iteritems():
try:
getattr(settings, key)
except AttributeError:
setattr(settings, key, value)
# Suppress errors from DJANGO_SETTINGS_MODULE not being set
except ImportError:
pass
"""CAS authentication backend"""
from urllib import urlencode, urlopen
from urlparse import urljoin
import urllib2
from django.conf import settings
from django_cas.models import User
__all__ = ['CASBackend']
def _verify_cas1(ticket, service):
"""Verifies CAS 1.0 authentication ticket.
Returns username on success and None on failure.
"""
params = {'ticket': ticket, 'service': service}
url = (urljoin(settings.CAS_SERVER_URL, 'validate') + '?' +
urlencode(params))
page = urlopen(url)
try:
verified = page.readline().strip()
if verified == 'yes':
return page.readline().strip(), None
else:
return None, None
finally:
page.close()
def _verify_cas2(ticket, service):
"""Verifies CAS 2.0+ XML-based authentication ticket.
Returns username on success and None on failure.
"""
try:
from xml.etree import ElementTree
except ImportError:
from elementtree import ElementTree
params = {'ticket': ticket, 'service': service}
url = (urljoin(settings.CAS_SERVER_URL, 'proxyValidate') + '?' +
urlencode(params))
page = urlopen(url)
try:
response = page.read()
tree = ElementTree.fromstring(response)
if tree[0].tag.endswith('authenticationSuccess'):
return tree[0][0].text, None
else:
return None, None
finally:
page.close()
def _verify_cas3(ticket, service):
"""Verifies CAS 3.0+ XML-based authentication ticket and returns extended attributes.
Returns username on success and None on failure.
"""
try:
from xml.etree import ElementTree
except ImportError:
from elementtree import ElementTree
params = {'ticket': ticket, 'service': service}
url = (urljoin(settings.CAS_SERVER_URL, 'proxyValidate') + '?' +
urlencode(params))
page = urlopen(url)
try:
user = None
attributes = {}
response = page.read()
tree = ElementTree.fromstring(response)
if tree[0].tag.endswith('authenticationSuccess'):
for element in tree[0]:
if element.tag.endswith('user'):
user = element.text
elif element.tag.endswith('attributes'):
for attribute in element:
attributes[attribute.tag.split("}").pop()] = attribute.text
return user, attributes
finally:
page.close()
def get_saml_assertion(ticket):
return """<?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body><samlp:Request xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" MajorVersion="1" MinorVersion="1" RequestID="_192.168.16.51.1024506224022" IssueInstant="2002-06-19T17:03:44.022Z"><samlp:AssertionArtifact>""" + ticket + """</samlp:AssertionArtifact></samlp:Request></SOAP-ENV:Body></SOAP-ENV:Envelope>"""
SAML_1_0_NS = 'urn:oasis:names:tc:SAML:1.0:'
SAML_1_0_PROTOCOL_NS = '{' + SAML_1_0_NS + 'protocol' + '}'
SAML_1_0_ASSERTION_NS = '{' + SAML_1_0_NS + 'assertion' + '}'
def _verify_cas2_saml(ticket, service):
"""Verifies CAS 3.0+ XML-based authentication ticket and returns extended attributes.
@date: 2011-11-30
@author: Carlos Gonzalez Vila <carlewis@gmail.com>
Returns username and attributes on success and None,None on failure.
"""
try:
from xml.etree import ElementTree
except ImportError:
from elementtree import ElementTree
# We do the SAML validation
headers = {'soapaction': 'http://www.oasis-open.org/committees/security',
'cache-control': 'no-cache',
'pragma': 'no-cache',
'accept': 'text/xml',
'connection': 'keep-alive',
'content-type': 'text/xml'}
params = {'TARGET': service}
url = urllib2.Request(urljoin(settings.CAS_SERVER_URL, 'samlValidate') + '?' + urlencode(params), '', headers)
data = get_saml_assertion(ticket)
url.add_data(get_saml_assertion(ticket))
page = urllib2.urlopen(url)
try:
user = None
attributes = {}
response = page.read()
print response
tree = ElementTree.fromstring(response)
# Find the authentication status
success = tree.find('.//' + SAML_1_0_PROTOCOL_NS + 'StatusCode')
if success is not None and success.attrib['Value'] == 'samlp:Success':
# User is validated
attrs = tree.findall('.//' + SAML_1_0_ASSERTION_NS + 'Attribute')
for at in attrs:
if 'uid' in at.attrib.values():
user = at.find(SAML_1_0_ASSERTION_NS + 'AttributeValue').text
attributes['uid'] = user
values = at.findall(SAML_1_0_ASSERTION_NS + 'AttributeValue')
if len(values) > 1:
values_array = []
for v in values:
values_array.append(v.text)
attributes[at.attrib['AttributeName']] = values_array
else:
attributes[at.attrib['AttributeName']] = values[0].text
return user, attributes
finally:
page.close()
_PROTOCOLS = {'1': _verify_cas1, '2': _verify_cas2, '3': _verify_cas3, 'CAS_2_SAML_1_0': _verify_cas2_saml}
if settings.CAS_VERSION not in _PROTOCOLS:
raise ValueError('Unsupported CAS_VERSION %r' % settings.CAS_VERSION)
_verify = _PROTOCOLS[settings.CAS_VERSION]
class CASBackend(object):
"""CAS authentication backend"""
def authenticate(self, ticket, service, request):
"""Verifies CAS ticket and gets or creates User object"""
username, attributes = _verify(ticket, service)
if attributes:
request.session['attributes'] = attributes
if not username:
return None
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
# user will have an "unusable" password
user = User.objects.create_user(username, '')
user.save()
return user
def get_user(self, user_id):
"""Retrieve the user's entry in the User model if it exists"""
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
"""Replacement authentication decorators that work around redirection loops"""
try:
from functools import wraps
except ImportError:
from django.utils.functional import wraps
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.utils.http import urlquote
__all__ = ['login_required', 'permission_required', 'user_passes_test']
def user_passes_test(test_func, login_url=None,
redirect_field_name=REDIRECT_FIELD_NAME):
"""Replacement for django.contrib.auth.decorators.user_passes_test that
returns 403 Forbidden if the user is already logged in.
"""
if not login_url:
from django.conf import settings
login_url = settings.LOGIN_URL
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if test_func(request.user):
return view_func(request, *args, **kwargs)
elif request.user.is_authenticated():
return HttpResponseForbidden('<h1>Permission denied</h1>')
else:
path = '%s?%s=%s' % (login_url, redirect_field_name,
urlquote(request.get_full_path()))
return HttpResponseRedirect(path)
return wrapper
return decorator
def permission_required(perm, login_url=None):
"""Replacement for django.contrib.auth.decorators.permission_required that
returns 403 Forbidden if the user is already logged in.
"""
return user_passes_test(lambda u: u.has_perm(perm), login_url=login_url)
"""CAS authentication middleware"""
from urllib import urlencode
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import login, logout
from django.core.urlresolvers import reverse
from django_cas.views import login as cas_login, logout as cas_logout
__all__ = ['CASMiddleware']
class CASMiddleware(object):
"""Middleware that allows CAS authentication on admin pages"""
def process_request(self, request):
"""Checks that the authentication middleware is installed"""
error = ("The Django CAS middleware requires authentication "
"middleware to be installed. Edit your MIDDLEWARE_CLASSES "
"setting to insert 'django.contrib.auth.middleware."
"AuthenticationMiddleware'.")
assert hasattr(request, 'user'), error
def process_view(self, request, view_func, view_args, view_kwargs):
"""Forwards unauthenticated requests to the admin page to the CAS
login URL, as well as calls to django.contrib.auth.views.login and
logout.
"""
if view_func == login:
return cas_login(request, *view_args, **view_kwargs)
elif view_func == logout:
return cas_logout(request, *view_args, **view_kwargs)
if settings.CAS_ADMIN_PREFIX:
if not request.path.startswith(settings.CAS_ADMIN_PREFIX):
return None
elif not view_func.__module__.startswith('django.contrib.admin.'):
return None
if request.user.is_authenticated():
if request.user.is_staff:
return None
else:
error = ('<h1>Forbidden</h1><p>You do not have staff '
'privileges.</p>')
return HttpResponseForbidden(error)
params = urlencode({REDIRECT_FIELD_NAME: request.get_full_path()})
return HttpResponseRedirect(reverse(cas_login) + '?' + params)
from django.db import models
from django.contrib.auth.models import User
\ No newline at end of file
"""CAS login/logout replacement views"""
from urllib import urlencode
from urlparse import urljoin
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib import messages
__all__ = ['login', 'logout']
def _service_url(request, redirect_to=None):
"""Generates application service URL for CAS"""
protocol = ('http://', 'https://')[request.is_secure()]
host = request.get_host()
service = protocol + host + request.path
if redirect_to:
if '?' in service:
service += '&'
else:
service += '?'
service += urlencode({REDIRECT_FIELD_NAME: redirect_to})
return service
def _redirect_url(request):
"""Redirects to referring page, or CAS_REDIRECT_URL if no referrer is
set.
"""
next = request.GET.get(REDIRECT_FIELD_NAME)
if not next:
if settings.CAS_IGNORE_REFERER:
next = settings.CAS_REDIRECT_URL
else:
next = request.META.get('HTTP_REFERER', settings.CAS_REDIRECT_URL)
prefix = (('http://', 'https://')[request.is_secure()] +
request.get_host())
if next.startswith(prefix):
next = next[len(prefix):]
return next
def _login_url(service):
"""Generates CAS login URL"""
params = {'service': service}
if settings.CAS_EXTRA_LOGIN_PARAMS:
params.update(settings.CAS_EXTRA_LOGIN_PARAMS)
return urljoin(settings.CAS_SERVER_URL, 'login') + '?' + urlencode(params)
def _logout_url(request, next_page=None):
"""Generates CAS logout URL"""
url = urljoin(settings.CAS_SERVER_URL, 'logout')
if next_page:
protocol = ('http://', 'https://')[request.is_secure()]
host = request.get_host()
url += '?' + urlencode({'url': protocol + host + next_page})
return url
def login(request, next_page=None, required=False):
"""Forwards to CAS login URL or verifies CAS ticket"""
if not next_page:
next_page = _redirect_url(request)
if request.user.is_authenticated():
message = "You are logged in as %s." % request.user.username
messages.success(request, message)
return HttpResponseRedirect(next_page)
ticket = request.GET.get('ticket')
service = _service_url(request, next_page)
if ticket:
from django.contrib import auth
user = auth.authenticate(ticket=ticket, service=service, request=request)
if user is not None:
auth.login(request, user)
name = user.first_name or user.username
message = "Login succeeded. Welcome, %s." % name
messages.success(request, message)
return HttpResponseRedirect(next_page)
elif settings.CAS_RETRY_LOGIN or required:
return HttpResponseRedirect(_login_url(service))
else:
error = "<h1>Forbidden</h1><p>Login failed.</p>"
return HttpResponseForbidden(error)
else:
return HttpResponseRedirect(_login_url(service))
def logout(request, next_page=None):
"""Redirects to CAS logout page"""
from django.contrib.auth import logout
logout(request)
if not next_page:
next_page = _redirect_url(request)
if settings.CAS_LOGOUT_COMPLETELY:
return HttpResponseRedirect(_logout_url(request, next_page))
else:
return HttpResponseRedirect(next_page)
Metadata-Version: 1.1
Name: django-gmucas
Version: 1.0.0
Summary: GMU's CAS 1.0/2.0 authentication backend for Django
Home-page: http://git.gmu.edu/bwaters3/gmu-djangocas
Author: bwaters3
Author-email: bwaters3@gmu.edu
License: MIT
Description:
``django_cas`` is a `CAS`_ 1.0 and CAS 2.0 authentication backend for
`Django`_. It allows you to use Django's built-in authentication mechanisms
and ``User`` model while adding support for CAS.
It also includes a middleware that intercepts calls to the original login and
logout pages and forwards them to the CASified versions, and adds CAS support
to the admin interface.
.. _CAS: http://www.ja-sig.org/products/cas/
.. _Django: http://www.djangoproject.com/
Keywords: django cas cas2 authentication middleware backend
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Framework :: Django
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Topic :: Internet :: WWW/HTTP
README.md
setup.py
django_gmucas/__init__.py
django_gmucas/backends.py
django_gmucas/decorators.py
django_gmucas/middleware.py
django_gmucas/models.py
django_gmucas/views.py
django_gmucas/urls.py
django_gmucas.egg-info/PKG-INFO
django_gmucas.egg-info/SOURCES.txt
django_gmucas.egg-info/dependency_links.txt
django_gmucas.egg-info/top_level.txt
"""Django CAS 1.0/2.0 authentication backend"""
from django.conf import settings
__all__ = []
_DEFAULTS = {