Commit aad4f470 by Sebastian Annies

added template method configurable in settings for populating the user object

parent 2c3bf9e1
......@@ -3,6 +3,7 @@
== Version 2.1.1 ==
* Bug Fix Release
* Added a template method for populating user object
== Version 2.1.0 ==
......
......@@ -42,6 +42,7 @@ Optional settings include:
* 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.
* CAS_PROXY_CALLBACK: The URL given to the CAS server in order to initialize a proxy ticket. The ticket granting ticket will be sent to this URL. The url must be registered in urls.py and handled by django_cas.views.proxy_callback, e.g: ``(r'^accounts/login/casProxyCallback$', 'django_cas.views.proxy_callback')``
* CAS_USER_DETAILS_RESOLVER: template method for populating the user object.
Make sure your project knows how to log users in and out by adding these to your URL mappings:
......@@ -50,33 +51,34 @@ Make sure your project knows how to log users in and out by adding these to your
Users should now be able to log into your site (and staff into the administration interface) using CAS.
Managing Access to the Admin Interface
--------------------------------------
Populating The User Object From CAS 2.0 Attributes
--------------------------------------------------
At the moment, the best way to give a user access to the admin interface is by doing one of the following:
Since there are manyfold ways transmitting user attributes in CAS and even more ways to map them
on django.auth.User the mapping is done via a template method.
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.
The template method is defined via the ``CAS_USER_DETAILS_RESOLVER`` setting::
CAS_USER_DETAILS_RESOLVER = cas_integration.populate_user
and an example method would be::
CAS_URI = 'http://www.yale.edu/tp/cas'
NSMAP = {'cas': CAS_URI}
CAS = '{%s}' % CAS_URI
def populate_user(user, authentication_response):
if authentication_response.find(CAS + 'authenticationSuccess/' + CAS + 'attributes' , namespaces=NSMAP) is not None:
attr = authentication_response.find(CAS + 'authenticationSuccess/' + CAS + 'attributes' , namespaces=NSMAP)
if attr.find(CAS + 'is_superuser', NSMAP) is not None:
user.is_superuser = attr.find(CAS + 'is_superuser', NSMAP).text.upper() == 'TRUE'
if attr.find(CAS + 'is_staff', NSMAP) is not None:
user.is_staff = attr.find(CAS + 'is_staff', NSMAP).text.upper() == 'TRUE'
pass
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
-----------------------------
......@@ -127,11 +129,9 @@ Version 2.0 of django_cas breaks compatibility in some small ways, in order simp
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.
CAS_USER_DETAILS_RESOLVER: template method for populating user object
Ed Crewe 2 Dec 2010
====================
Add proxy authentication
------------------------
......
Index: setup.py
===================================================================
--- setup.py (revision 1175)
+++ setup.py (arbetskopia)
@@ -37,5 +37,5 @@
name='django_cas',
packages=['django_cas'],
url='http://code.google.com/p/django-cas/',
- version='2.0.3',
+ version='2.0.3-KTH-2',
)
Index: django_cas/backends.py
===================================================================
--- django_cas/backends.py (revision 1175)
+++ django_cas/backends.py (arbetskopia)
@@ -4,9 +4,9 @@
from urlparse import urljoin
from django.conf import settings
+from django.core.exceptions import ObjectDoesNotExist
+from django_cas.models import User, Tgt, PgtIOU
-from django_cas.models import User
-
__all__ = ['CASBackend']
def _verify_cas1(ticket, service):
@@ -40,21 +40,71 @@
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)
+ if tree[0].tag.endswith('authenticationSuccess'):
+ username = tree[0][0].text
+ if len(tree[0]) >= 2 and tree[0][1].tag.endswith('proxyGrantingTicket'):
+ pgtIou = PgtIOU.objects.get(pgtIou = tree[0][1].text)
+ try:
+ tgt = Tgt.objects.get(username = username)
+ tgt.tgt = pgtIou.tgt
+ tgt.save()
+ except ObjectDoesNotExist:
+ Tgt.objects.create(username = username, tgt = pgtIou.tgt)
+
+ pgtIou.delete()
+ return username
+ 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'):
- return tree[0][0].text
+ username = tree[0][0].text
+ proxies = []
+ 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:
Index: django_cas/middleware.py
===================================================================
--- django_cas/middleware.py (revision 1175)
+++ django_cas/middleware.py (arbetskopia)
@@ -1,13 +1,13 @@
"""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 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_cas.exceptions import CasTicketException
from django_cas.views import login as cas_login, logout as cas_logout
__all__ = ['CASMiddleware']
@@ -50,3 +50,13 @@
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
Index: django_cas/views.py
===================================================================
--- django_cas/views.py (revision 1175)
+++ django_cas/views.py (arbetskopia)
@@ -3,9 +3,10 @@
from urllib import urlencode
from urlparse import urljoin
-from django.http import get_host, HttpResponseRedirect, HttpResponseForbidden
+from django.http import get_host, HttpResponseRedirect, HttpResponseForbidden, HttpResponse
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
+from django_cas.models import PgtIOU
__all__ = ['login', 'logout']
@@ -76,6 +77,7 @@
if ticket:
from django.contrib import auth
user = auth.authenticate(ticket=ticket, service=service)
+
if user is not None:
auth.login(request, user)
name = user.first_name or user.username
@@ -102,3 +104,18 @@
return HttpResponseRedirect(_logout_url(request, next_page))
else:
return HttpResponseRedirect(next_page)
+
+def proxy_callback(request):
+ """Handles CAS 2.0+ XML-based proxy callback call.
+
+ Stores the proxy granting ticket in the database for
+ future use.
+ """
+ pgtIou = request.GET.get('pgtIou')
+ tgt = request.GET.get('pgtId')
+
+ if not (pgtIou and tgt):
+ return HttpResponse()
+
+ PgtIOU.objects.create(tgt = tgt, pgtIou = pgtIou)
+ return HttpResponse()
Index: django_cas/__init__.py
===================================================================
--- django_cas/__init__.py (revision 1175)
+++ django_cas/__init__.py (arbetskopia)
@@ -11,6 +11,7 @@
'CAS_LOGOUT_COMPLETELY': True,
'CAS_REDIRECT_URL': '/',
'CAS_RETRY_LOGIN': False,
+ 'CAS_PROXY_CALLBACK': None,
'CAS_SERVER_URL': None,
'CAS_VERSION': '2',
}
Index: django_cas/exceptions.py
===================================================================
--- django_cas/exceptions.py (revision 0)
+++ django_cas/exceptions.py (revision 1216)
@@ -0,0 +1,13 @@
+class CasTicketException(Exception):
+ def __init__(self, error):
+ self.error = error
+
+ def __str__(self):
+ return repr(self.error)
+
+class CasConfigException(Exception):
+ def __init__(self, error):
+ self.error = error
+
+ def __str__(self):
+ return repr(self.error)
Index: django_cas/models.py
===================================================================
--- django_cas/models.py (revision 1175)
+++ django_cas/models.py (arbetskopia)
@@ -1,2 +1,55 @@
+from urlparse import urljoin
+from urllib import urlencode, urlopen
from django.db import models
-from django.contrib.auth.models import User
\ No newline at end of file
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.exceptions import ObjectDoesNotExist
+from django_cas.exceptions import CasTicketException, CasConfigException
+
+class Tgt(models.Model):
+ username = models.CharField(max_length = 255, unique = True)
+ tgt = models.CharField(max_length = 255)
+
+ def get_proxy_ticket_for(self, service):
+ """Verifies CAS 2.0+ XML-based authentication ticket.
+
+ Returns username on success and None on failure.
+ """
+ if not settings.CAS_PROXY_CALLBACK:
+ raise CasConfigException("No proxy callback set in settings")
+
+ try:
+ from xml.etree import ElementTree
+ except ImportError:
+ from elementtree import ElementTree
+
+ params = {'pgt': self.tgt, 'targetService': service}
+
+ url = (urljoin(settings.CAS_SERVER_URL, 'proxy') + '?' +
+ urlencode(params))
+
+ page = urlopen(url)
+
+ try:
+ response = page.read()
+ tree = ElementTree.fromstring(response)
+ if tree[0].tag.endswith('proxySuccess'):
+ return tree[0][0].text
+ else:
+ raise CasTicketException("Failed to get proxy ticket")
+ finally:
+ page.close()
+
+class PgtIOU(models.Model):
+ pgtIou = models.CharField(max_length = 255, unique = True)
+ tgt = models.CharField(max_length = 255)
+ timestamp = models.DateTimeField(auto_now = True)
+
+def get_tgt_for(user):
+ if not settings.CAS_PROXY_CALLBACK:
+ raise CasConfigException("No proxy callback set in settings")
+
+ try:
+ return Tgt.objects.get(username = user.username)
+ except ObjectDoesNotExist:
+ raise CasTicketException("no ticket found for user " + user.username)
Index: NEWS.txt
===================================================================
--- NEWS.txt (revision 1175)
+++ NEWS.txt (arbetskopia)
@@ -1,5 +1,9 @@
= Release Notes =
+== Version X.X.X ==
+
+ * Added support for CAS proxy authentication.
+
== Version 2.0.3 ==
* Added `CAS_EXTRA_LOGIN_PARAMS` setting (patched contributed by frasern).
Index: README.txt
===================================================================
--- README.txt (revision 1175)
+++ README.txt (arbetskopia)
@@ -43,7 +43,11 @@
http://sso.some.edu/cas/).
Optional settings include:
-
+ * `CAS_PROXY_CALLBACK`: The URL given to the CAS server in order to
+ initialize a proxy ticket. The ticket granting ticket will be sent
+ to this URL. The url must be registered in urls.py and handled
+ by django_cas.views.proxy_callback, e.g:
+ (r'^accounts/login/casProxyCallback$', 'django_cas.views.proxy_callback')
* `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`.
@@ -125,7 +129,23 @@
For more information see http://code.djangoproject.com/ticket/4617.
+== CAS proxy tickets ==
+Using CAS proxy tickets is quite a bit less trivial than ordinary tickets.
+First of all the CAS server requires that the Django site can be accessed
+via https, and it MUST have a properly signed certificate that the CAS
+server can verify.
+
+For the test-server this can be achieved using a tunneling application
+such as stunnel. However this is not enough. The CAS proxy auhentication
+requires that both the web browser and the CAS server simoultaneously can
+make requests to the Django server, which the Django test server does not
+support.
+
+However, there is a Django app you can use to be able to start a threaded
+test server hosted here:
+http://github.com/jaylett/django_concurrent_test_server
+
== Customizing the 403 Error Page ==
Django doesn't provide a simple way to customize 403 error pages, so you'll
......@@ -25,9 +25,9 @@ def _verify_cas1(ticket, service):
try:
verified = page.readline().strip()
if verified == 'yes':
return page.readline().strip()
return page.readline().strip(), None
else:
return None
return None, None
finally:
page.close()
......@@ -71,9 +71,9 @@ def _verify_cas2(ticket, service):
Tgt.objects.create(username = username, tgt = pgtIou.tgt)
pgtIou.delete()
return username
return username, tree
else:
return None
return None, tree
def verify_proxy_ticket(ticket, service):
......@@ -117,6 +117,9 @@ if settings.CAS_VERSION not in _PROTOCOLS:
_verify = _PROTOCOLS[settings.CAS_VERSION]
_CAS_USER_DETAILS_RESOLVER = getattr(settings, 'CAS_USER_DETAILS_RESOLVER', None)
class CASBackend(object):
"""CAS authentication backend"""
......@@ -124,15 +127,18 @@ class CASBackend(object):
"""Verifies CAS ticket and gets or creates User object
NB: Use of PT to identify proxy
"""
username = _verify(ticket, service)
username, authentication_response = _verify(ticket, service)
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()
user, created = User.objects.get_or_create(username=username)
if created:
user.set_unusable_password()
if authentication_response and _CAS_USER_DETAILS_RESOLVER:
_CAS_USER_DETAILS_RESOLVER(user, authentication_response)
user.save()
return user
def get_user(self, user_id):
......@@ -142,3 +148,4 @@ class CASBackend(object):
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment