changeset 626:a6bc1e2efa63

Created wiki app to provide integration with MoinMoin. This commit has a working middleware & test.
author Brian Neal <bgneal@gmail.com>
date Wed, 07 Nov 2012 20:17:33 -0600
parents 08d905e38a86
children a4300639c6e7
files sg101/settings/base.py wiki/__init__.py wiki/middleware.py wiki/models.py wiki/signals.py wiki/tests.py
diffstat 5 files changed, 262 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/sg101/settings/base.py	Thu Nov 01 19:35:30 2012 -0500
+++ b/sg101/settings/base.py	Wed Nov 07 20:17:33 2012 -0600
@@ -77,6 +77,7 @@
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
     'core.middleware.InactiveUserMiddleware',
     'core.middleware.WhosOnline',
+    'wiki.middleware.WikiMiddleware',
     'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
 ]
 
@@ -142,6 +143,7 @@
     'shoutbox',
     'smiley',
     'weblinks',
+    'wiki',
     'ygroup',
 ]
 
@@ -279,6 +281,12 @@
 PB_TS3_PORT = 10011
 PB_TS3_VID = 17
 
+# MoinMoin Wiki integration settings
+WIKI_COOKIE_NAME = 'sg101_wiki'
+WIKI_COOKIE_DOMAIN = '.surfguitar101.com'
+WIKI_COOKIE_AGE = SESSION_COOKIE_AGE
+WIKI_REDIS_SET = 'wiki_cookie_keys'
+
 #######################################################################
 # Asynchronous settings (queues, queued_search, redis, celery, etc)
 #######################################################################
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/middleware.py	Wed Nov 07 20:17:33 2012 -0600
@@ -0,0 +1,120 @@
+"""Middleware for wiki integration.
+
+"""
+import datetime
+import hashlib
+import logging
+import random
+import string
+import time
+
+from django.conf import settings
+import redis
+
+from core.services import get_redis_connection
+
+
+SESSION_KEY = 'wiki_redis_key'
+logger = logging.getLogger(__name__)
+
+
+def cookie_value(user, now):
+    """Creates the value for the external wiki cookie."""
+
+    # The key part of the cookie is just a string that would make things
+    # difficult for a spoofer; something that can't be easily made up:
+
+    h = hashlib.sha256()
+    h.update(user.username + user.email)
+    h.update(now.isoformat())
+    h.update(''.join(random.sample(string.printable, 64)))
+    h.update(settings.SECRET_KEY)
+    key = h.hexdigest()
+
+    parts = (user.username, user.email, key)
+    return '#'.join(parts)
+
+
+def create_wiki_session(request, response):
+    """Sets up the session for the external wiki application.
+
+    Creates the external cookie for the Wiki.
+    Updates the Redis set so the Wiki can verify the cookie.
+
+    """
+    now = datetime.datetime.utcnow()
+    value = cookie_value(request.user, now)
+    response.set_cookie(settings.WIKI_COOKIE_NAME,
+            value=value,
+            max_age=settings.WIKI_COOKIE_AGE,
+            domain=settings.WIKI_COOKIE_DOMAIN)
+
+    # Update a sorted set in Redis with a hash of our cookie and a score
+    # of the current time as a timestamp. This allows us to delete old
+    # entries by score periodically. To verify the cookie, the external wiki
+    # application computes a hash of the cookie value and checks to see if
+    # it is in our Redis set.
+
+    h = hashlib.sha256()
+    h.update(value)
+    name = h.hexdigest()
+    score = time.mktime(now.utctimetuple())
+    conn = get_redis_connection()
+
+    try:
+        conn.zadd(settings.WIKI_REDIS_SET, score, name)
+    except redis.RedisError:
+        logger.error("Error adding wiki cookie key")
+
+    # Store the set member name in the session so we can delete it when the
+    # user logs out:
+    request.session[SESSION_KEY] = name
+
+
+def destroy_wiki_session(request, response):
+    """Destroys the session for the external wiki application.
+
+    Delete the external cookie.
+    Deletes the key from the Redis set as this entry is no longer valid.
+
+    """
+    response.delete_cookie(settings.WIKI_COOKIE_NAME,
+                           domain=settings.WIKI_COOKIE_DOMAIN)
+
+    try:
+        key = request.session[SESSION_KEY]
+    except KeyError:
+        # Hmmm, perhaps user logged in before this application was installed.
+        return
+
+    conn = get_redis_connection()
+    try:
+        conn.zrem(settings.WIKI_REDIS_SET, key)
+    except redis.RedisError:
+        logger.error("Error deleting wiki cookie key")
+
+    del request.session[SESSION_KEY]
+
+
+class WikiMiddleware(object):
+    """
+    Check for flags set in the session to determine when to set or delete an
+    external cookie for the wiki application. When creating a cookie, also
+    set an entry in Redis that the wiki application can validate to prevent
+    spoofing.
+
+    """
+
+    def process_response(self, request, response):
+
+        if request.session.get('wiki_set_cookie', False):
+            del request.session['wiki_set_cookie']
+
+            create_wiki_session(request, response)
+
+        elif request.session.get('wiki_delete_cookie', False):
+            del request.session['wiki_delete_cookie']
+
+            destroy_wiki_session(request, response)
+
+        return response
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/models.py	Wed Nov 07 20:17:33 2012 -0600
@@ -0,0 +1,8 @@
+"""The wiki application integrates an external Wiki app with our Django
+application.
+
+The wiki application has no models. It consists of some signals and
+middleware only.
+
+"""
+import wiki.signals
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/signals.py	Wed Nov 07 20:17:33 2012 -0600
@@ -0,0 +1,35 @@
+"""Signal handlers for wiki integration.
+
+"""
+import logging
+
+from django.contrib.auth.signals import user_logged_in, user_logged_out
+
+
+logger = logging.getLogger(__name__)
+
+
+def login_callback(sender, request, user, **kwargs):
+    """Signal callback function for a user logging in.
+
+    Sets a flag for the middleware to create an external cookie.
+
+    """
+    logger.info('User login: %s', user.username)
+
+    request.session['wiki_set_cookie'] = True
+
+
+def logout_callback(sender, request, user, **kwargs):
+    """Signal callback function for a user logging in.
+
+    Sets a flag for the middleware to delete the external cookie.
+
+    """
+    logger.info('User logout: %s', user.username)
+
+    request.session['wiki_delete_cookie'] = True
+
+
+user_logged_in.connect(login_callback, dispatch_uid='wiki.signals.login')
+user_logged_out.connect(logout_callback, dispatch_uid='wiki.signals.logout')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/tests.py	Wed Nov 07 20:17:33 2012 -0600
@@ -0,0 +1,91 @@
+"""
+Tests for the wiki integration application.
+
+"""
+import hashlib
+import datetime
+
+from django.contrib.auth.models import User
+from django.test import TestCase
+from django.test.client import RequestFactory
+from django.http import HttpResponse
+from django.conf import settings
+
+from core.services import get_redis_connection
+from wiki.middleware import WikiMiddleware, SESSION_KEY
+
+
+class MiddleWareTestCase(TestCase):
+
+    def setUp(self):
+        self.factory = RequestFactory()
+        self.user = User.objects.create_user('test_user', 'test@example.com',
+                'password')
+        self.conn = get_redis_connection()
+        self.mw = WikiMiddleware()
+
+    def tearDown(self):
+        self.conn.delete(settings.WIKI_REDIS_SET)
+
+    def test_middleware(self):
+
+        request = self.factory.get('/contact/')
+        request.session = {}
+        request.user = self.user
+        response = HttpResponse()
+
+        request.session['wiki_set_cookie'] = True
+        response = self.mw.process_response(request, response)
+
+        self.assertIsNone(request.session.get('wiki_set_cookie'))
+
+        cookie = response.cookies.get(settings.WIKI_COOKIE_NAME)
+        cookie_val = ''
+        self.assertIsNotNone(cookie)
+        if cookie:
+            self.assertEqual(cookie['domain'], settings.WIKI_COOKIE_DOMAIN)
+            self.assertEqual(cookie['path'], '/')
+            self.assertEqual(cookie['max-age'], settings.WIKI_COOKIE_AGE)
+
+            cookie_val = cookie.value
+            try:
+                user, email, key = cookie_val.split('#')
+            except KeyError:
+                self.fail('invalid cookie value')
+            else:
+                self.assertEqual(user, self.user.username)
+                self.assertEqual(email, self.user.email)
+                self.assertEqual(len(key), 64)
+
+        self.assertEqual(self.conn.zcard(settings.WIKI_REDIS_SET), 1)
+
+        h = hashlib.sha256()
+        h.update(cookie_val)
+        member = h.hexdigest()
+
+        score = self.conn.zscore(settings.WIKI_REDIS_SET, member)
+        now = datetime.datetime.utcnow()
+        session_start = datetime.datetime.fromtimestamp(score)
+        self.assertLess(now - session_start, datetime.timedelta(seconds=2))
+
+        session_key = request.session.get(SESSION_KEY)
+        self.assertTrue(session_key and session_key == member)
+
+        # test the destroy session logic
+
+        request.session['wiki_delete_cookie'] = True
+        response = self.mw.process_response(request, response)
+
+        self.assertIsNone(request.session.get('wiki_delete_cookie'))
+
+        cookie = response.cookies.get(settings.WIKI_COOKIE_NAME)
+        self.assertIsNotNone(cookie)
+        if cookie:
+            self.assertEqual(cookie.value, '')
+            self.assertEqual(cookie['domain'], settings.WIKI_COOKIE_DOMAIN)
+            self.assertEqual(cookie['path'], '/')
+            self.assertEqual(cookie['max-age'], 0)
+            self.assertEqual(cookie['expires'], 'Thu, 01-Jan-1970 00:00:00 GMT')
+
+        self.assertEqual(self.conn.zcard(settings.WIKI_REDIS_SET), 0)
+        self.assertIsNone(request.session.get(SESSION_KEY))