Skip to content
This repository has been archived by the owner on May 29, 2024. It is now read-only.

Commit

Permalink
added cas module
Browse files Browse the repository at this point in the history
  • Loading branch information
dxue2012 committed Apr 4, 2014
1 parent 36d0624 commit b364051
Show file tree
Hide file tree
Showing 15 changed files with 977 additions and 6 deletions.
Empty file added __init__.py
Empty file.
29 changes: 29 additions & 0 deletions cas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""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',
'CAS_GATEWAY': False,
'CAS_PROXY_CALLBACK': None,
'CAS_RESPONSE_CALLBACKS': None,
'CAS_CUSTOM_FORBIDDEN':None
}

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
142 changes: 142 additions & 0 deletions cas/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""CAS authentication backend"""

from urllib import urlencode, urlopen
from urlparse import urljoin

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from cas.models import User, Tgt, PgtIOU
from cas.utils import cas_response_callbacks

__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()
else:
return 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

if settings.CAS_PROXY_CALLBACK:
params = {'ticket': ticket, 'service': service, 'pgtUrl': settings.CAS_PROXY_CALLBACK}
else:
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)

#Useful for debugging
#from xml.dom.minidom import parseString
#from xml.etree import ElementTree
#txt = ElementTree.tostring(tree)
#print parseString(txt).toprettyxml()

if tree[0].tag.endswith('authenticationSuccess'):
if settings.CAS_RESPONSE_CALLBACKS:
cas_response_callbacks(tree)
return tree[0][0].text
else:
return None
finally:
page.close()


def verify_proxy_ticket(ticket, service):
"""Verifies CAS 2.0+ XML-based proxy 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'):
username = tree[0][0].text
proxies = []
if len(tree[0]) > 1:
for element in tree[0][1]:
proxies.append(element.text)
return {"username": username, "proxies": proxies}
else:
return None
finally:
page.close()

_PROTOCOLS = {'1': _verify_cas1, '2': _verify_cas2}

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"""

supports_object_permissions = False
supports_inactive_user = False

def authenticate(self, ticket, service):
"""Verifies CAS ticket and gets or creates User object
NB: Use of PT to identify proxy
"""

username = _verify(ticket, service)
if not username:
return None
try:
user = User.objects.get(username__iexact=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
84 changes: 84 additions & 0 deletions cas/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Replacement authentication decorators that work around redirection loops"""

try:
from functools import wraps
except ImportError:
from django.utils.functional import wraps

from urllib import urlencode
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)


from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

def gateway():
"""Authenticates single sign on session if ticket is available,
but doesn't redirect to sign in url otherwise.
"""
if settings.CAS_GATEWAY == False:
raise ImproperlyConfigured('CAS_GATEWAY must be set to True')
def wrap(func):
def wrapped_f(*args):

from cas.views import login
request = args[0]

if request.user.is_authenticated():
#Is Authed, fine
pass
else:
path_with_params = request.path + '?' + urlencode(request.GET.copy())
if request.GET.get('ticket'):
#Not Authed, but have a ticket!
#Try to authenticate
return login(request, path_with_params, False, True)
else:
#Not Authed, but no ticket
gatewayed = request.GET.get('gatewayed')
if gatewayed == 'true':
pass
else:
#Not Authed, try to authenticate
return login(request, path_with_params, False, True)

return func(*args)
return wrapped_f
return wrap
9 changes: 9 additions & 0 deletions cas/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"CasTicketException, CasConfigException"
from django.core.exceptions import ValidationError

class CasTicketException(ValidationError):
"""The ticket fails to validate"""

class CasConfigException(ValidationError):
"""The config is wrong"""

75 changes: 75 additions & 0 deletions cas/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""CAS authentication middleware"""

from urllib import urlencode

from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import logout as do_logout
from django.contrib.auth.views import login, logout
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.core.exceptions import ImproperlyConfigured

from cas.exceptions import CasTicketException
from 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)

def process_exception(self, request, exception):
"""When we get a CasTicketException, that is probably caused by the ticket timing out.
So logout/login and get the same page again."""
if isinstance(exception, CasTicketException):
do_logout(request)
# This assumes that request.path requires authentication.
return HttpResponseRedirect(request.path)
else:
return None

class ProxyMiddleware(object):

# Middleware used to "fake" the django app that it lives at the Proxy Domain
def process_request(self, request):
proxy = getattr(settings, 'PROXY_DOMAIN', None)
if not proxy:
raise ImproperlyConfigured('To use Proxy Middleware you must set a PROXY_DOMAIN setting.')
else:
request.META['HTTP_HOST'] = proxy
Loading

0 comments on commit b364051

Please sign in to comment.