New CAS client
authorŁukasz Rekucki <lrekucki@gmail.com>
Fri, 5 Mar 2010 02:08:51 +0000 (03:08 +0100)
committerŁukasz Rekucki <lrekucki@gmail.com>
Fri, 5 Mar 2010 02:08:51 +0000 (03:08 +0100)
apps/django_cas/__init__.py [new file with mode: 0755]
apps/django_cas/backends.py [new file with mode: 0755]
apps/django_cas/decorators.py [new file with mode: 0755]
apps/django_cas/middleware.py [new file with mode: 0755]
apps/django_cas/models.py [new file with mode: 0755]
apps/django_cas/views.py [new file with mode: 0755]
apps/wiki/views.py
platforma/settings.py
platforma/templates/registration/head_login.html
platforma/templates/wiki/document_details.html
platforma/urls.py

diff --git a/apps/django_cas/__init__.py b/apps/django_cas/__init__.py
new file mode 100755 (executable)
index 0000000..14f71be
--- /dev/null
@@ -0,0 +1,25 @@
+"""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
diff --git a/apps/django_cas/backends.py b/apps/django_cas/backends.py
new file mode 100755 (executable)
index 0000000..f14619d
--- /dev/null
@@ -0,0 +1,93 @@
+"""CAS authentication backend"""
+
+from urllib import urlencode, urlopen
+from urlparse import urljoin
+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()
+        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 lxml import etree as ElementTree
+    except ImportError:
+        from elementtree import ElementTree
+
+    params = {'ticket': ticket, 'service': service}
+    url = (urljoin(settings.CAS_SERVER_URL, 'serviceValidate') + '?' +
+           urlencode(params))
+    page = urlopen(url)
+    try:
+        response = page.read()
+        tree = ElementTree.fromstring(response)
+        if tree[0].tag.endswith('authenticationSuccess'):
+            return tree[0][0].text
+        else:
+            return None
+    except:
+        import traceback
+        traceback.print_exc()
+        print "****"
+        print response
+        print "****"
+    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"""
+
+    def authenticate(self, ticket, service):
+        """Verifies CAS ticket and gets or creates User object"""
+
+        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
diff --git a/apps/django_cas/decorators.py b/apps/django_cas/decorators.py
new file mode 100755 (executable)
index 0000000..54a3734
--- /dev/null
@@ -0,0 +1,45 @@
+"""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)
diff --git a/apps/django_cas/middleware.py b/apps/django_cas/middleware.py
new file mode 100755 (executable)
index 0000000..e09f063
--- /dev/null
@@ -0,0 +1,52 @@
+"""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)
diff --git a/apps/django_cas/models.py b/apps/django_cas/models.py
new file mode 100755 (executable)
index 0000000..7658df9
--- /dev/null
@@ -0,0 +1,2 @@
+from django.db import models
+from django.contrib.auth.models import User
\ No newline at end of file
diff --git a/apps/django_cas/views.py b/apps/django_cas/views.py
new file mode 100755 (executable)
index 0000000..110c646
--- /dev/null
@@ -0,0 +1,111 @@
+"""CAS login/logout replacement views"""
+
+from urllib import urlencode
+from urlparse import urljoin
+
+from django.http import get_host, HttpResponseRedirect, HttpResponseForbidden
+from django.conf import settings
+from django.contrib.auth import REDIRECT_FIELD_NAME
+
+__all__ = ['login', 'logout']
+
+def _service_url(request, redirect_to = None):
+    """Generates application service URL for CAS"""
+
+    protocol = ('http://', 'https://')[request.is_secure()]
+    host = get_host(request)
+    service = protocol + host + request.path
+    if redirect_to:
+        if '?' in service:
+            service += '&'
+        else:
+            service += '?'
+        service += urlencode({REDIRECT_FIELD_NAME: redirect_to.encode('utf-8')})
+    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()] +
+                  get_host(request))
+        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 = get_host(request)
+        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"""
+
+    print "LOGIN original NEXT_PAGE:", next_page
+    print request.GET
+    if not next_page:
+        next_page = _redirect_url(request)
+    print "LOGIN redirect NEXT_PAGE:", next_page
+
+    if request.user.is_authenticated():
+        message = "You are logged in as %s." % request.user.username
+        request.user.message_set.create(message = message)
+        return HttpResponseRedirect(next_page)
+    ticket = request.GET.get('ticket')
+    service = _service_url(request, next_page)
+    print "TICKET", ticket
+    print "SERVICE", service
+    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
+            message = "Login succeeded. Welcome, %s." % name
+            user.message_set.create(message = 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)
index eff1f1d..4f808f0 100644 (file)
@@ -40,7 +40,7 @@ def document_detail(request, name, template_name = 'wiki/document_details.html')
 def document_gallery(request, directory):
     try:
         base_dir = os.path.join(settings.MEDIA_ROOT, settings.FILEBROWSER_DIRECTORY, directory)
-        images = ['%s%s%s/%s' % (settings.MEDIA_URL, settings.FILEBROWSER_DIRECTORY, directory, f) for f in os.listdir(base_dir) if os.path.splitext(f)[1].lower() in ('.jpg', '.jpeg', '.png')]
+        images = [u'%s%s%s/%s' % (settings.MEDIA_URL, settings.FILEBROWSER_DIRECTORY, directory, f) for f in os.listdir(base_dir) if os.path.splitext(f)[1].lower() in (u'.jpg', u'.jpeg', u'.png')]
         images.sort()
         return HttpResponse(json.dumps(images))
     except (IndexError, OSError), e:
index 2faab1b..c7898e8 100755 (executable)
@@ -79,16 +79,31 @@ MIDDLEWARE_CLASSES = (
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django_cas.middleware.CASMiddleware',
     'django.middleware.doc.XViewMiddleware',
     'maintenancemode.middleware.MaintenanceModeMiddleware',
 )
 
+AUTHENTICATION_BACKENDS = (
+    'django.contrib.auth.backends.ModelBackend',
+    'django_cas.backends.CASBackend',
+)
+
 ROOT_URLCONF = 'urls'
 
 TEMPLATE_DIRS = (
     PROJECT_ROOT + '/templates',
 )
 
+
+#
+# Central Auth System
+#
+## Set this to where the CAS server lives 
+# CAS_SERVER_URL = "http://cas.fnp.pl/
+CAS_ADMIN_PREFIX = "/admin/"
+CAS_LOGOUT_COMPLETELY = True
+
 # CSS and JS files to compress
 # COMPRESS_CSS = {
 #     'all': {
@@ -124,6 +139,11 @@ INSTALLED_APPS = (
     'toolbar',
 )
 
+
+#
+# Nose tests
+#
+
 TEST_RUNNER = 'django_nose.run_tests'
 TEST_MODULES = ('wiki', 'toolbar', 'vstorage')
 NOSE_ARGS = (
@@ -155,12 +175,9 @@ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(messag
 ch.setFormatter(formatter)
 log.addHandler(ch)
 
-
 # Import localsettings file, which may override settings defined here
 try:
-    EXTRA_INSTALLED_APPS = tuple()
     from localsettings import *
-    INSTALLED_APPS += EXTRA_INSTALLED_APPS
 except ImportError:
     pass
 
index ca2c4e1..ee20ef2 100644 (file)
@@ -2,10 +2,10 @@
 
 {% if user.is_authenticated %}
 <span class="user_name">{{ user.username }}</span> | 
-<a href='{% url logout %}?next={{request.get_full_path|urlencode}}'>{% trans "Log Out" %}</a>
+<a href='{% url logout %}'>{% trans "Log Out" %}</a>
 {% else %}
 {% url login as login_url %}
 {% ifnotequal login_url request.path %}
-    <a href='{{ login_url }}?next={{request.get_full_path|urlencode}}'>{% trans "Log In" %}</a>
+    <a href='{{ login_url }}'>{% trans "Log In" %}</a>
 {% endifnotequal %}
 {% endif %}
index ead1382..43e5dfd 100644 (file)
         </div>
         <div id="header">
             <div id="tools" style="float: right;">
-                {% if user.is_authenticated %}
-                    Zalogowany jako <span id="username">{{ user }}</span> | <a href="{% url logout %}">Wyloguj</a> |
-                {% else %}
-                    <a href="{% url login %}?next={{ request.path }}">Zaloguj się</a> |
-                {% endif %}
+                {% include "registration/head_login.html" %}
                 Wersja: <span id="document-revision">{{ document.revision }}</span> <button style="margin-left: 6px" id="save-button">Zapisz</button></div>
             <h1><a href="{% url wiki.views.document_list %}">Platforma</a></h1>
             <ol id="tabs">
index 1abc899..bd296a9 100755 (executable)
@@ -6,18 +6,13 @@ from django.conf import settings
 
 admin.autodiscover()
 
-if 'cas_consumer' in settings.INSTALLED_APPS:
-    auth_views = 'cas_consumer.views'
-else:
-    auth_views = 'django.contrib.auth.views'
-
 urlpatterns = patterns('',
     url(r'^$', 'wiki.views.document_list'),
     url(r'^gallery/(?P<directory>[^/]+)$', 'wiki.views.document_gallery'),
 
     # Auth
-    url(r'^accounts/login/$', auth_views + '.login', name = 'login'),
-    url(r'^accounts/logout/$', auth_views + '.logout', name = 'logout'),
+    url(r'^accounts/login/$', 'django_cas.views.login', name = 'login'),
+    url(r'^accounts/logout/$', 'django_cas.views.logout', name = 'logout'),
 
     # Admin panel
     (r'^admin/filebrowser/', include('filebrowser.urls')),