Commits

Chris Beaven committed 8ae35a6

Give forms the optional ability to authenticate users (allowing for revoking a users preview rights by changing the passwords)

Comments (0)

Files changed (4)

 * Once Django 1.2 ships with signed cookies (hopefully), replace
   contrib.sessions dependency with a signed cookie.
 
-* Make session authorization expire if settings are changed (password,
-  custom form)
-

lockdown/forms.py

 from lockdown import settings
 
 
-class LockdownForm(forms.Form):
+class BaseLockdownForm(forms.Form):
+    def generate_token(self):
+        """
+        Generate a token which can be used to authenticate the user for future
+        requests.
+        
+        """
+        return True
+
+    def authenticate(self, token_value):
+        """
+        Authenticate the user from a stored token value. If the ``token_value``
+        is ``None``, then no token was retrieved.
+         
+        """
+        return token_value is True
+
+
+class LockdownForm(BaseLockdownForm):
     password = forms.CharField(widget=forms.PasswordInput(render_value=False))
 
     def clean_password(self):
-        if self.cleaned_data['password'] in settings.PASSWORDS:
-            return self.cleaned_data['password']
-        raise forms.ValidationError('Incorrect password.')
+        """
+        Check that the password appears the LOCKDOWN_PASSWORDS setting.
+        
+        """
+        value = self.cleaned_data.get('password')
+        if not value in settings.PASSWORDS:
+            raise forms.ValidationError('Incorrect password.')
+        return value
+
+    def generate_token(self):
+        """
+        Save the password as the authentication token.
+        
+        It's acceptable to store the password raw, as it is stored server-side
+        in the user's session.
+        
+        """
+        return self.cleaned_data['password']
+
+    def authenticate(self, token_value):
+        """
+        Check that the password is still in the LOCKDOWN_PASSWORDS setting.
+        
+        This allows for revoking of a user's preview rights by changing the
+        passwords.
+        
+        """
+        return token_value in settings.PASSWORDS

lockdown/middleware.py

 from lockdown import settings
 
 
-# an extra layer of indirection here so the tests can force this to be
-# recalculated
-_compiled_url_exceptions = ()
+# An extra layer of indirection here so the tests can force this to be
+# recalculated.
 def _compile_url_exceptions():
-    global _compiled_url_exceptions
-    _compiled_url_exceptions = [re.compile(p)
-                                for p in settings.URL_EXCEPTIONS]
-_compile_url_exceptions()
+    return [re.compile(p) for p in settings.URL_EXCEPTIONS]
 
+_url_exceptions = _compile_url_exceptions()
 
-_lockdown_form = None
-def _get_lockdown_form():
-    global _lockdown_form
-    path = settings.FORM
-    if path is None:
-        if settings.PASSWORD:
-            path = 'lockdown.forms.LockdownForm'
-        else:
-            return None
-    i = path.rfind('.')
-    module, attr = path[:i], path[i + 1:]
+
+def get_lockdown_form(form_path):
+    form_path = settings.FORM
+    if not form_path or '.' not in form_path:
+        raise ImproperlyConfigured('The form module path was not provided.')
+    last_dot = form_path.rfind('.')
+    module, attr = form_path[:last_dot], form_path[last_dot + 1:]
     try:
         mod = import_module(module)
-    except ImportError, e:
+    except (ImportError, ValueError), e:
         raise ImproperlyConfigured('Error importing LOCKDOWN_FORM %s: "%s"'
-                                   % (path, e))
-    except ValueError, e:
-        raise ImproperlyConfigured('Error import LOCKDOWN_FORM %s: "%s"'
-                                   % (path, e))
+                                   % (form_path, e))
     try:
         form = getattr(mod, attr)
     except AttributeError:
         raise ImproperlyConfigured('Module "%s" does not define a "%s" form.'
                                    % (module, attr))
-    _lockdown_form = form
-_get_lockdown_form()
+    return form
+
+_lockdown_form = get_lockdown_form(settings.FORM)
 
 
 class LockdownMiddleware(object):
             raise ImproperlyConfigured('django-lockdown requires the Django '
                                        'sessions framework')
 
-        # Check if the user is already authorized for previewing.
-        if session.get(settings.SESSION_KEY, False):
-            return None
-
-        # check if the URL matches an exception pattern
-        for pattern in _compiled_url_exceptions:
+        # Don't lock down if the URL matches an exception pattern.
+        for pattern in _url_exceptions:
             if pattern.search(request.path):
                 return None
 
-        # validate form data, if form is in use and submitted
-        if _lockdown_form:
-            if request.method == 'POST':
-                form = _lockdown_form(request.POST)
-                if form.is_valid():
-                    session[settings.SESSION_KEY] = True
-                    return HttpResponseRedirect(request.path)
+        form_data = request.method == 'POST' and request.POST
+        form = _lockdown_form(data=form_data)
+
+        # Don't lock down if the user is already authorized for previewing.
+        token = session.get(settings.SESSION_KEY)
+        if hasattr(form, 'authenticate'):
+            if self.form.authenticate(token):
+                return None
+        elif token is True:
+            return None
+
+        if form.is_valid():
+            if hasattr(form, 'generate_token'):
+                token = form.generate_token()
             else:
-                form = _lockdown_form()
-        else:
-            form = None
+                token = True
+            session[settings.SESSION_KEY] = token
+            return HttpResponseRedirect(request.path)
+
         return render_to_response('lockdown/form.html',
                                   {'form': form},
                                   context_instance=RequestContext(request))
-        return None
 from django.test import TestCase, Client
 from django.conf import settings as django_settings
 
-from lockdown import settings
-from lockdown.middleware import _compile_url_exceptions, _get_lockdown_form
+from lockdown import settings, middleware
+
 
 class DecoratorTests(TestCase):
     _url = '/locked/view/'
     _contents = 'A locked view.'
     
     def setUp(self):
-        self._old_pw = settings.PASSWORD
-        settings.PASSWORD = 'letmein'
-        _get_lockdown_form()
+        self._old_pw = settings.PASSWORDS
+        settings.PASSWORDS = 'letmein'
+        self._old_form = settings.FORM
+        settings.FORM = 'lockdown.forms.LockdownForm'
+        middleware._lockdown_form = middleware.get_lockdown_form(settings.FORM)
         self.client = Client()
 
     def tearDown(self):
-        settings.PASSWORD = self._old_pw
+        settings.PASSWORDS = self._old_pw
+        settings.FORM = self._old_form
+        middleware._lockdown_form = middleware.get_lockdown_form(settings.FORM)
 
     def test_lockdown_template_used(self):
         response = self.client.get(self._url)
     def test_url_exceptions(self):
         _old_url_exceptions = settings.URL_EXCEPTIONS
         settings.URL_EXCEPTIONS = (r'/view/$',)
-        _compile_url_exceptions()
+        middleware._url_exceptions = middleware._compile_url_exceptions()
 
         response = self.client.get(self._url)
         self.assertContains(response, self._contents)
 
         settings.URL_EXCEPTIONS = _old_url_exceptions
-        _compile_url_exceptions()
+        middleware._url_exceptions = middleware._compile_url_exceptions()
         
     def test_submit_password(self):
         response = self.client.post(self._url, {'password': 'letmein'},
     def test_custom_form(self):
         _old_form = settings.FORM
         settings.FORM = 'tests.forms.CustomLockdownForm'
-        _get_lockdown_form()
+        middleware._lockdown_form = middleware.get_lockdown_form(settings.FORM)
         
         response = self.client.post(self._url, {'answer': '42'},
                                     follow=True)
         self.assertContains(response, self._contents)
                                     
         settings.FORM = _old_form
-        _get_lockdown_form()
+        middleware._lockdown_form = middleware.get_lockdown_form(settings.FORM)
     
 
 class MiddlewareTests(DecoratorTests):
     
     def setUp(self):
         self._old_middleware_classes = django_settings.MIDDLEWARE_CLASSES
-        django_settings.MIDDLEWARE_CLASSES += ('lockdown.middleware.LockdownMiddleware',)
+        django_settings.MIDDLEWARE_CLASSES += (
+            'lockdown.middleware.LockdownMiddleware',
+        )
         super(MiddlewareTests, self).setUp()
 
     def tearDown(self):