CAS authentication.
authorgfawcett <gfawcett@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Wed, 18 Aug 2010 02:46:17 +0000 (02:46 +0000)
committergfawcett <gfawcett@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Wed, 18 Aug 2010 02:46:17 +0000 (02:46 +0000)
See: http://code.google.com/p/django-cas/

To use CAS authentication, you must "easy_install django-cas", then add these
to your local_settings.py:

CAS_AUTHENTICATION = True
CAS_SERVER_URL     = 'https://my.cas.server.example.net/cas/'

You will probably also want to define two customization hooks:
external_person_lookup and user_needs_decoration. See:
conifer/syrup/integration.py.

git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@969 6d9bc8c9-1ec2-4278-b937-99fde70a366f

12 files changed:
.gitignore
conifer/integration/cas.py [new file with mode: 0644]
conifer/integration/uwindsor.py
conifer/plumbing/hooksystem.py
conifer/settings.py
conifer/syrup/integration.py
conifer/syrup/views/_common.py
conifer/syrup/views/genshi_namespace.py
conifer/templates/browse_index.xhtml
conifer/templates/master.xhtml
conifer/templates/search_results.xhtml
conifer/urls.py

index 63dd581..4def612 100644 (file)
@@ -13,3 +13,4 @@ private_local_settings.py
 /conifer/remodel.sqlite3
 *~
 /conifer/test.db
+/conifer/syrup/test.db
diff --git a/conifer/integration/cas.py b/conifer/integration/cas.py
new file mode 100644 (file)
index 0000000..c0a9fd3
--- /dev/null
@@ -0,0 +1,53 @@
+# CAS authentication. See http://code.google.com/p/django-cas/
+#
+# To use CAS authentication, you must "easy_install django-cas", then add these
+# to your local_settings.py:
+#
+# CAS_AUTHENTICATION = True
+# CAS_SERVER_URL     = 'https://my.cas.server.example.net/cas/'
+#
+# You will probably also want to define two customization hooks:
+# external_person_lookup and user_needs_decoration. See:
+# conifer/syrup/integration.py.
+
+from conifer.plumbing.hooksystem import gethook, callhook
+import django_cas.backends
+
+
+class CASBackend(django_cas.backends.CASBackend):
+
+    def authenticate(self, ticket, service):
+        """Authenticates CAS ticket and retrieves user data"""
+
+        user = super(CASBackend, self).authenticate(ticket, service)
+        if user and gethook('external_person_lookup'):
+            decorate_user(user)
+        return user
+
+
+# TODO is this really CAS specific? Wouldn't linktool (for example)
+# also need such a decorator?
+
+def decorate_user(user):
+    dectest = gethook('user_needs_decoration', default=_user_needs_decoration)
+    if not dectest(user):
+        return
+
+    dir_entry = callhook('external_person_lookup', user.username)
+    if dir_entry is None:
+        return
+
+    user.first_name = dir_entry['given_name']
+    user.last_name  = dir_entry['surname']
+    user.email      = dir_entry.get('email', user.email)
+    user.save()
+
+    if 'patron_id' in dir_entry:
+        # note, we overrode user.get_profile() to automatically create
+        # missing profiles. See models.py.
+        user.get_profile().ils_userid = dir_entry['patron_id']
+        profile.save()
+
+
+def _user_needs_decoration(user):
+    return user.last_name is not None
index b094531..0419ced 100644 (file)
@@ -135,7 +135,8 @@ def external_person_lookup(userid):
     Given a userid, return either None (if the user cannot be found),
     or a dictionary representing the user. The dictionary must contain
     the keys ('given_name', 'surname') and should contain 'email' if
-    an email address is known.
+    an email address is known, and 'patron_id' if a library-system ID
+    is known.
     """
     return uwindsor_campus_info.call('person_lookup', userid)
 
index 5144499..935b592 100644 (file)
@@ -6,8 +6,6 @@ import conifer.syrup.integration as HOOKS
 __all__ = ['callhook', 'callhook_required', 'gethook']
 
 def gethook(name, default=None):
-    print dir(HOOKS)
-    print (name, getattr(HOOKS, name))
     return getattr(HOOKS, name) or default
 
 def callhook_required(name, *args, **kwargs):
@@ -19,3 +17,5 @@ def callhook(name, *args, **kwargs):
     f = getattr(HOOKS, name)
     if f:
         return f(*args, **kwargs)
+    else:
+        return None
index 3783636..07c6f16 100644 (file)
@@ -87,8 +87,10 @@ INSTALLED_APPS = [
     'conifer.syrup',
 ]
 
-AUTH_PROFILE_MODULE = 'syrup.UserProfile'
+LOGIN_URL  = '/accounts/login/'
+LOGOUT_URL = '/accounts/logout'
 
+AUTH_PROFILE_MODULE = 'syrup.UserProfile'
 
 AUTHENTICATION_BACKENDS = [
     'django.contrib.auth.backends.ModelBackend'
@@ -97,6 +99,10 @@ AUTHENTICATION_BACKENDS = [
 EVERGREEN_AUTHENTICATION = False
 LINKTOOL_AUTHENTICATION  = False
 
+# CAS authentication requires 'django-cas', 
+# http://code.google.com/p/django-cas/
+CAS_AUTHENTICATION       = False  
+
 #---------------------------------------------------------------------------
 # local_settings.py
 
@@ -126,3 +132,8 @@ if EVERGREEN_AUTHENTICATION:
 if LINKTOOL_AUTHENTICATION:
     AUTHENTICATION_BACKENDS.append(
         'conifer.integration.linktool.backend.LinktoolAuthBackend')
+
+if CAS_AUTHENTICATION:
+    AUTHENTICATION_BACKENDS.append('conifer.integration.cas.CASBackend')
+    LOGIN_URL  = '/cas/login'
+    LOGOUT_URL = '/cas/logout'
index 7ca2e3c..eff6b98 100644 (file)
@@ -109,3 +109,14 @@ def external_person_lookup(userid):
     an email address is known.
     """
 
+@disable
+def user_needs_decoration(user_obj):
+    """
+    User objects are sometimes created automatically, with only a
+    username. This function determines whether it would be fruitful to
+    "decorate" the User object with, e.g., a given name, surname, and
+    email address. It doesn't perform the decoration, it simply tests
+    whether the current user object is "incomplete." Another hook
+    'external_person_lookup,' is used by Syrup to fetch the personal
+    information when needed.
+    """
index 00fba9a..2161816 100644 (file)
@@ -4,9 +4,11 @@
 # is a module which acts as a global namespace when expanding a Genshi
 # template.
 
+from .                               import genshi_namespace
 from conifer.here                    import HERE
 from conifer.plumbing.genshi_support import TemplateSet
-from .                               import genshi_namespace
+from django.conf                     import settings
+
 
 g = TemplateSet(HERE('templates'), genshi_namespace)
 
@@ -48,10 +50,11 @@ __builtins__['_'] = translation.ugettext
 def _access_denied(request, message):
     if request.user.is_anonymous():
         # then take them to login screen....
-        dest = (request.META['SCRIPT_NAME'] + \
-                    '/accounts/login/?next=%s%s' % (
-                request.META['SCRIPT_NAME'],
-                request.META['PATH_INFO']))
+        dest = (request.META['SCRIPT_NAME'] + 
+                settings.LOGIN_URL +
+                '?next=' +
+                request.META['SCRIPT_NAME'] +
+                request.META['PATH_INFO'])
         return HttpResponseRedirect(dest)
     else:
         return simple_message(_('Access denied.'), message,
index 5186685..431202c 100644 (file)
@@ -13,4 +13,5 @@ import urllib
 
 from conifer.plumbing.hooksystem import gethook, callhook
 from conifer.syrup               import models
+from django.conf                 import settings
 from django.utils.translation    import ugettext as _
index e31edf5..814bbb3 100644 (file)
@@ -15,7 +15,7 @@ blocks = itertools.groupby(sites, lambda s: s.course.department)
   <h1>${title}</h1> 
   <div py:if="user.is_anonymous()">
     (Note: some reserve materials may require you
-    to <a href="${ROOT}/accounts/login/?next=${ROOT}/">log in</a>)
+    to <a href="${ROOT}${settings.LOGIN_URL}?next=${ROOT}/">log in</a>)
   </div>
 
     
index c885ebe..fe0eed6 100644 (file)
@@ -51,12 +51,12 @@ import os
         </div>
       <div id="welcome" py:if="user.is_authenticated()">
        <strong style="padding-right: 18px;">Welcome, ${user.first_name or user.username}!</strong>
-       <a href="${ROOT}/accounts/logout">Log Out</a>
+       <a href="${ROOT}${settings.LOGOUT_URL}">Log Out</a>
        &bull; <a href="${ROOT}/prefs/">Preferences</a>
       </div>
       <div id="welcome" py:if="not user.is_authenticated()">
        <strong style="padding-right: 18px;">Welcome!</strong>
-       <a class="loginbutton" href="${ROOT}/accounts/login/">Log In</a>
+       <a class="loginbutton" href="${ROOT}${settings.LOGIN_URL}">Log In</a>
        &bull; <a href="${ROOT}/prefs/">Preferences</a>
       </div>
     </div>
index 2a27f2c..bb0b8e1 100644 (file)
@@ -53,7 +53,7 @@ title = _('Search Results')
 
     <div py:if="user.is_anonymous()">
       Your searches may return more results if you <a
-      href="${ROOT}/accounts/login/?next=${ROOT}/">log in</a> before
+      href="${ROOT}${settings.LOGIN_URL}?next=${ROOT}/">log in</a> before
       searching.
     </div>
 
index ec9578c..4ee3e13 100644 (file)
@@ -47,3 +47,9 @@ if settings.LINKTOOL_AUTHENTICATION:
         (r'^linktool-welcome/copy_old$', 'linktool_copy_old'),
         (r'^linktool-welcome/associate$', 'linktool_associate'),
         )
+
+if settings.CAS_AUTHENTICATION:
+    urlpatterns += patterns(
+        'django_cas.views',
+    (r'^%s$' % settings.LOGIN_URL[1:],  'login'),
+    (r'^%s$' % settings.LOGOUT_URL[1:], 'logout'))