├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── authors.rst ├── cached_auth ├── __init__.py └── models.py ├── setup.py └── test_project ├── __init__.py ├── manage.py ├── test_app ├── __init__.py ├── models.py ├── tests.py └── views.py ├── test_app_custom_user ├── __init__.py ├── models.py ├── tests.py └── views.py └── test_project ├── __init__.py ├── custom_user_settings.py ├── settings.py ├── urls.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | - "3.3" 7 | - "3.4" 8 | - "pypy" 9 | 10 | env: 11 | - DJANGO="django>=1.4,<1.5" 12 | - DJANGO="django>=1.5,<1.6" 13 | - DJANGO="django>=1.6,<1.7" 14 | - DJANGO="django>=1.7,<1.8" 15 | - DJANGO="django>=1.8,<1.9" 16 | 17 | install: 18 | - pip install $DJANGO 19 | - python setup.py install 20 | 21 | script: 22 | - "DJANGO_SETTINGS_MODULE=test_project.settings python test_project/manage.py test test_app" 23 | - "if [ $DJANGO != 'django>=1.4,<1.5' ]; then DJANGO_SETTINGS_MODULE=test_project.custom_user_settings python test_project/manage.py test test_app_custom_user; fi" 24 | 25 | matrix: 26 | exclude: 27 | - python: "2.6" 28 | env: DJANGO="django>=1.7,<1.8" 29 | - python: "2.6" 30 | env: DJANGO="django>=1.8,<1.9" 31 | - python: "3.3" 32 | env: DJANGO="django>=1.4,<1.5" 33 | - python: "3.4" 34 | env: DJANGO="django>=1.4,<1.5" 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Selwin Ong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.rst 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-cached_authentication_middleware is a drop in replacement for 2 | ``django.contrib.auth``'s built in ``AuthenticationMiddleware``. It tries to 3 | populate ``request.user`` by fetching user data from cache before falling back 4 | to the database. 5 | 6 | Installation 7 | ------------ 8 | 9 | |Build Status| 10 | 11 | 12 | * Install via pypi:: 13 | 14 | pip install django-cached_authentication_middleware 15 | 16 | * Configure ``CACHES`` in django's ``settings.py``:: 17 | 18 | CACHES = { 19 | 'default': { 20 | 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache', 21 | 'LOCATION': '127.0.0.1:11211', 22 | 'TIMEOUT': 36000, 23 | } 24 | } 25 | 26 | * Replace ``django.contrib.auth.middleware.AuthenticationMiddleware`` with 27 | ``cached_auth.Middleware`` in ``settings.py``:: 28 | 29 | MIDDLEWARE_CLASSES = ( 30 | 'django.middleware.common.CommonMiddleware', 31 | 'django.contrib.sessions.middleware.SessionMiddleware', 32 | #'django.contrib.auth.middleware.AuthenticationMiddleware' 33 | 'cached_auth.Middleware', 34 | ) 35 | 36 | And you're done! 37 | 38 | Cached Auth Preprocessor 39 | ------------------------ 40 | 41 | Sometimes you want to preprocess to ``User`` instance before storing 42 | it into cache. ``cached_auth`` allows you to define 43 | ``settings.CACHED_AUTH_PREPROCESSOR``, a callable that takes two arguments, ``user`` & ``request`` and returns a ``User`` instance. 44 | 45 | A classic example of this would be to attach ``Profile`` data 46 | to ``User`` object so calling ``request.user.profile`` does not incur a 47 | database hit. Here's how we can implement it. 48 | 49 | .. code-block:: python 50 | 51 | def attach_profile(user, request): 52 | try: 53 | user.get_profile() 54 | # Handle exception for user with no profile and AnonymousUser 55 | except (Profile.DoesNotExist, AttributeError): 56 | pass 57 | return user 58 | 59 | 60 | # In settings.py: 61 | CACHED_AUTH_PREPROCESSOR = 'path.to.module.attach_profile' 62 | 63 | Running Tests 64 | ------------- 65 | 66 | To run the test suite:: 67 | 68 | python tests/runtests.py 69 | 70 | To run the test suite with Django custom user (this will run only on Django 1.5):: 71 | 72 | python tests/runtests_custom_user.py 73 | 74 | Changelog 75 | --------- 76 | 77 | Version 0.2.1 78 | ============= 79 | * Better Django 1.8 compatibility. 80 | 81 | Version 0.2.0 82 | ============= 83 | 84 | * Added support for Django 1.5's customer user model 85 | * Added ``CACHED_AUTH_PREPROCESSOR`` setting 86 | 87 | Version 0.1.1 88 | ============= 89 | 90 | * Fixed an error where middleware tries to call "get_profile" on AnonymousUser 91 | 92 | Version 0.1 93 | =========== 94 | 95 | * Initial release 96 | 97 | 98 | .. |Build Status| image:: https://travis-ci.org/ui/django-cached_authentication_middleware.png?branch=master 99 | :target: https://travis-ci.org/ui/django-cached_authentication_middleware -------------------------------------------------------------------------------- /authors.rst: -------------------------------------------------------------------------------- 1 | Author: 2 | * Selwin Ong 3 | 4 | Contributors: 5 | * Gilang Chandrasa -------------------------------------------------------------------------------- /cached_auth/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 2, 2) 2 | 3 | from django.conf import settings 4 | from django.contrib.auth import get_user, SESSION_KEY 5 | from django.core.cache import cache 6 | from django.db.models.signals import post_save, post_delete 7 | from django.utils.functional import SimpleLazyObject 8 | 9 | from django.contrib.auth.models import AnonymousUser 10 | 11 | try: 12 | from django.contrib.auth import get_user_model 13 | except ImportError: 14 | from django.contrib.auth.models import User 15 | get_user_model = lambda: User 16 | 17 | CACHE_KEY = 'cached_auth_middleware:%s' 18 | 19 | 20 | try: 21 | from django.apps import apps 22 | get_model = apps.get_model 23 | except ImportError: 24 | from django.db.models import get_model 25 | 26 | 27 | try: 28 | app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.') 29 | profile_model = get_model(app_label, model_name) 30 | except (ValueError, AttributeError): 31 | profile_model = None 32 | 33 | 34 | def profile_preprocessor(user, request): 35 | """ Cache user profile """ 36 | if profile_model: 37 | try: 38 | user.get_profile() 39 | # Handle exception for user with no profile and AnonymousUser 40 | except (profile_model.DoesNotExist, AttributeError): 41 | pass 42 | return user 43 | 44 | 45 | user_preprocessor = None 46 | if hasattr(settings, 'CACHED_AUTH_PREPROCESSOR'): 47 | tmp = settings.CACHED_AUTH_PREPROCESSOR.split(".") 48 | module_name, function_name = ".".join(tmp[0:-1]), tmp[-1] 49 | func = getattr(__import__(module_name, fromlist=['']), function_name) 50 | if callable(func): 51 | user_preprocessor = func 52 | else: 53 | raise Exception("CACHED_AUTH_PREPROCESSOR must be callable with 2 arguments user and request") 54 | else: 55 | user_preprocessor = profile_preprocessor 56 | 57 | 58 | def invalidate_cache(sender, instance, **kwargs): 59 | if isinstance(instance, get_user_model()): 60 | key = CACHE_KEY % instance.id 61 | else: 62 | key = CACHE_KEY % instance.user_id 63 | cache.delete(key) 64 | 65 | 66 | def get_cached_user(request): 67 | if not hasattr(request, '_cached_user'): 68 | try: 69 | key = CACHE_KEY % request.session[SESSION_KEY] 70 | user = cache.get(key) 71 | except KeyError: 72 | user = AnonymousUser() 73 | if user is None: 74 | user = get_user(request) 75 | if user_preprocessor: 76 | user = user_preprocessor(user, request) 77 | cache.set(key, user) 78 | request._cached_user = user 79 | return request._cached_user 80 | 81 | 82 | class MiddlewareMixin(object): 83 | """ 84 | Ported from Django 1.11: django.utils.deprecation.MiddlewareMixin 85 | """ 86 | def __init__(self, get_response=None): 87 | self.get_response = get_response 88 | super(MiddlewareMixin, self).__init__() 89 | 90 | def __call__(self, request): 91 | response = None 92 | if hasattr(self, 'process_request'): 93 | response = self.process_request(request) 94 | if not response: 95 | response = self.get_response(request) 96 | if hasattr(self, 'process_response'): 97 | response = self.process_response(request, response) 98 | return response 99 | 100 | 101 | class Middleware(MiddlewareMixin): 102 | 103 | def __init__(self, *args, **kwargs): 104 | super(Middleware, self).__init__(*args, **kwargs) 105 | 106 | post_save.connect(invalidate_cache, sender=get_user_model()) 107 | post_delete.connect(invalidate_cache, sender=get_user_model()) 108 | if profile_model: 109 | post_save.connect(invalidate_cache, sender=profile_model) 110 | post_delete.connect(invalidate_cache, sender=profile_model) 111 | 112 | def process_request(self, request): 113 | assert hasattr(request, 'session'), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." 114 | request.user = SimpleLazyObject(lambda: get_cached_user(request)) 115 | -------------------------------------------------------------------------------- /cached_auth/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui/django-cached_authentication_middleware/47b6e5e528139037da3326825c42e0abd374e17b/cached_auth/models.py -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup 3 | 4 | 5 | setup( 6 | name='django-cached_authentication_middleware', 7 | version='0.2.2', 8 | author='Selwin Ong', 9 | author_email='selwin.ong@gmail.com', 10 | packages=['cached_auth'], 11 | url='https://github.com/ui/django-cached_authentication_middleware', 12 | license='MIT', 13 | description="A drop in replacement for django's built in AuthenticationMiddleware that utilizes caching.", 14 | long_description=open('README.rst').read(), 15 | zip_safe=False, 16 | include_package_data=True, 17 | package_data={'': ['README.rst']}, 18 | install_requires=['django'], 19 | classifiers=[ 20 | 'Development Status :: 4 - Beta', 21 | 'Environment :: Web Environment', 22 | 'Framework :: Django', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 2', 28 | 'Programming Language :: Python :: 2.6', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.3', 32 | 'Programming Language :: Python :: 3.4', 33 | 'Programming Language :: Python :: Implementation :: CPython', 34 | 'Programming Language :: Python :: Implementation :: PyPy', 35 | 'Topic :: Internet :: WWW/HTTP', 36 | 'Topic :: Software Development :: Libraries :: Python Modules', 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui/django-cached_authentication_middleware/47b6e5e528139037da3326825c42e0abd374e17b/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals 4 | 5 | import sys 6 | 7 | 8 | if __name__ == '__main__': 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /test_project/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui/django-cached_authentication_middleware/47b6e5e528139037da3326825c42e0abd374e17b/test_project/test_app/__init__.py -------------------------------------------------------------------------------- /test_project/test_app/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui/django-cached_authentication_middleware/47b6e5e528139037da3326825c42e0abd374e17b/test_project/test_app/models.py -------------------------------------------------------------------------------- /test_project/test_app/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.core.cache import cache 5 | from django.core.urlresolvers import reverse 6 | from django.test import TestCase 7 | from django.test.client import Client 8 | from django.test.utils import override_settings 9 | 10 | import cached_auth 11 | 12 | try: 13 | from django.contrib.auth import get_user_model 14 | except ImportError: 15 | from django.contrib.auth.models import User 16 | get_user_model = lambda: User 17 | 18 | try: 19 | # Python 3.4+ includes reload in importlib 20 | from importlib import reload 21 | except ImportError: 22 | try: 23 | # Python 3.3 includes reload in imp 24 | from imp import reload 25 | except ImportError: 26 | # Python 2 includes reload as a builtin 27 | pass 28 | 29 | 30 | class MiddlewareTest(TestCase): 31 | 32 | def setUp(self): 33 | user_model = get_user_model() 34 | self.user = user_model.objects.create_user(username='test', password='a') 35 | self.user.is_superuser = True 36 | self.user.is_staff = True 37 | self.user.save() 38 | cache.clear() 39 | 40 | def test_anonymous(self): 41 | # Anonymous user doesn't cause cache to be set 42 | client = Client() 43 | key = cached_auth.CACHE_KEY % self.user.id 44 | client.get(reverse('admin:index')) 45 | self.assertEqual(cache.get(key), None) 46 | 47 | def test_cached_middleware(self): 48 | client = Client() 49 | key = cached_auth.CACHE_KEY % self.user.id 50 | self.assertEqual(cache.get(key), None) 51 | 52 | # Visiting admin causes the cache to be populated 53 | client.login(username='test', password='a') 54 | client.get(reverse('admin:index')) 55 | self.assertEqual(cache.get(key), self.user) 56 | 57 | # Changing user model invalidates cache 58 | self.user.save() 59 | self.assertEqual(cache.get(key), None) 60 | 61 | # Deleting user invalidates cache 62 | client.get(reverse('admin:index')) 63 | self.assertEqual(cache.get(key), self.user) 64 | self.user.delete() 65 | self.assertEqual(cache.get(key), None) 66 | 67 | @override_settings(CACHED_AUTH_PREPROCESSOR='test_project.utils.auth_preprocessor') 68 | def test_cached_auth_preprocessor_function(self): 69 | reload(cached_auth) 70 | client = Client() 71 | key = cached_auth.CACHE_KEY % self.user.id 72 | self.assertEqual(cache.get(key), None) 73 | 74 | client.login(username='test', password='a') 75 | client.get(reverse('admin:index')) 76 | user = cache.get(key) 77 | self.assertEqual(user.username, 'test_auth') 78 | -------------------------------------------------------------------------------- /test_project/test_app/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui/django-cached_authentication_middleware/47b6e5e528139037da3326825c42e0abd374e17b/test_project/test_app/views.py -------------------------------------------------------------------------------- /test_project/test_app_custom_user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui/django-cached_authentication_middleware/47b6e5e528139037da3326825c42e0abd374e17b/test_project/test_app_custom_user/__init__.py -------------------------------------------------------------------------------- /test_project/test_app_custom_user/models.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.core import validators 4 | from django.core.mail import send_mail 5 | from django.db import models 6 | from django.contrib.auth.models import (AbstractBaseUser, PermissionsMixin, 7 | UserManager) 8 | from django.utils.translation import ugettext_lazy as _ 9 | from django.utils import timezone 10 | from django.utils.http import urlquote 11 | 12 | 13 | class User(AbstractBaseUser, PermissionsMixin): 14 | """ 15 | Username, password and email are required. 16 | """ 17 | username = models.CharField(_('username'), max_length=30, unique=True, 18 | help_text=_('Required. 30 characters or fewer. Letters, numbers and ' 19 | '@/./+/-/_ characters'), 20 | validators=[ 21 | validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid') 22 | ]) 23 | first_name = models.CharField(_('first name'), max_length=30, blank=True) 24 | last_name = models.CharField(_('last name'), max_length=30, blank=True) 25 | email = models.EmailField(_('email address'), max_length=254) # RFC 5321 26 | is_staff = models.BooleanField(_('staff status'), default=False, 27 | help_text=_('Designates whether the user can log into this admin ' 28 | 'site.')) 29 | is_active = models.BooleanField(_('active'), default=True, 30 | help_text=_('Designates whether this user should be treated as ' 31 | 'active. Unselect this instead of deleting accounts.')) 32 | date_joined = models.DateTimeField(_('date joined'), default=timezone.now) 33 | 34 | objects = UserManager() 35 | 36 | USERNAME_FIELD = 'username' 37 | REQUIRED_FIELDS = ['email'] 38 | 39 | class Meta: 40 | verbose_name = _('user') 41 | verbose_name_plural = _('users') 42 | 43 | def get_absolute_url(self): 44 | return "/users/%s/" % urlquote(self.username) 45 | 46 | def get_full_name(self): 47 | """ 48 | Returns the first_name plus the last_name, with a space in between. 49 | """ 50 | full_name = '%s %s' % (self.first_name, self.last_name) 51 | return full_name.strip() 52 | 53 | def get_short_name(self): 54 | "Returns the first_name for the user." 55 | return self.first_name 56 | 57 | def email_user(self, subject, message, from_email=None): 58 | """ 59 | Sends an email to this User. 60 | """ 61 | send_mail(subject, message, from_email, [self.email]) 62 | -------------------------------------------------------------------------------- /test_project/test_app_custom_user/tests.py: -------------------------------------------------------------------------------- 1 | from test_app.tests import * -------------------------------------------------------------------------------- /test_project/test_app_custom_user/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui/django-cached_authentication_middleware/47b6e5e528139037da3326825c42e0abd374e17b/test_project/test_app_custom_user/views.py -------------------------------------------------------------------------------- /test_project/test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui/django-cached_authentication_middleware/47b6e5e528139037da3326825c42e0abd374e17b/test_project/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/test_project/custom_user_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from .settings import * 5 | 6 | 7 | INSTALLED_APPS += ( 8 | 'test_app_custom_user', 9 | ) 10 | 11 | AUTH_USER_MODEL = 'test_app_custom_user.User' 12 | -------------------------------------------------------------------------------- /test_project/test_project/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | 5 | DEBUG = True 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.sqlite3', 10 | } 11 | } 12 | 13 | INSTALLED_APPS = ( 14 | 'django.contrib.auth', 15 | 'django.contrib.contenttypes', 16 | 'django.contrib.sessions', 17 | 'django.contrib.admin', 18 | 'cached_auth', 19 | 'test_app', 20 | ) 21 | 22 | CACHES = { 23 | 'default': { 24 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 25 | 'LOCATION': '127.0.0.1:11211', 26 | 'TIMEOUT': 36000, 27 | } 28 | } 29 | 30 | MIDDLEWARE_CLASSES = ( 31 | 'django.contrib.sessions.middleware.SessionMiddleware', 32 | 'cached_auth.Middleware', 33 | ) 34 | 35 | ROOT_URLCONF = 'test_project.urls' 36 | 37 | SECRET_KEY = 'django-cached-authentication-middleware' 38 | -------------------------------------------------------------------------------- /test_project/test_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | 5 | admin.autodiscover() 6 | 7 | 8 | urlpatterns = patterns('', 9 | url(r'^admin/', include(admin.site.urls)), 10 | ) 11 | -------------------------------------------------------------------------------- /test_project/test_project/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | 5 | def auth_preprocessor(user, request): 6 | user.username = 'test_auth' 7 | return user 8 | --------------------------------------------------------------------------------