# HG changeset patch # User Brian Neal # Date 1352341053 21600 # Node ID a6bc1e2efa63c6e937c91dbfef2152c9e6a9458d # Parent 08d905e38a863cf01a81d05545849d47f0877172 Created wiki app to provide integration with MoinMoin. This commit has a working middleware & test. diff -r 08d905e38a86 -r a6bc1e2efa63 sg101/settings/base.py --- 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) ####################################################################### diff -r 08d905e38a86 -r a6bc1e2efa63 wiki/middleware.py --- /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 diff -r 08d905e38a86 -r a6bc1e2efa63 wiki/models.py --- /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 diff -r 08d905e38a86 -r a6bc1e2efa63 wiki/signals.py --- /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') diff -r 08d905e38a86 -r a6bc1e2efa63 wiki/tests.py --- /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))