Skip to content

Commit

Permalink
Merge pull request #170 from brack3t/feature/recentloginrequiredmixin
Browse files Browse the repository at this point in the history
RecentLoginRequiredMixin tests and first pass
  • Loading branch information
chrisjones-brack3t committed Apr 17, 2015
2 parents 9541c7f + 33900c1 commit 24bf321
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 6 deletions.
3 changes: 2 additions & 1 deletion braces/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
PermissionRequiredMixin,
StaffuserRequiredMixin,
SuperuserRequiredMixin,
UserPassesTestMixin
UserPassesTestMixin,
RecentLoginRequiredMixin
)
from ._ajax import (
AjaxResponseMixin,
Expand Down
25 changes: 23 additions & 2 deletions braces/views/_access.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import datetime

import six

from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import redirect_to_login
from django.contrib.auth.views import redirect_to_login, logout_then_login
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.http import HttpResponseRedirect
from django.shortcuts import resolve_url
from django.utils.encoding import force_text
from django.utils.timezone import now


class AccessMixin(object):
Expand Down Expand Up @@ -56,7 +59,8 @@ class LoginRequiredMixin(AccessMixin):

def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated():
if self.raise_exception and not self.redirect_unauthenticated_users:
if (self.raise_exception and
not self.redirect_unauthenticated_users):
raise PermissionDenied # return a forbidden response
else:
return redirect_to_login(request.get_full_path(),
Expand Down Expand Up @@ -392,3 +396,20 @@ def dispatch(self, request, *args, **kwargs):

return super(StaffuserRequiredMixin, self).dispatch(
request, *args, **kwargs)


class RecentLoginRequiredMixin(LoginRequiredMixin):
"""
Mixin allows you to require a login to be within a number of seconds.
"""
max_last_login_delta = 1800 # Defaults to 30 minutes

def dispatch(self, request, *args, **kwargs):
resp = super(RecentLoginRequiredMixin, self).dispatch(
request, *args, **kwargs)

delta = datetime.timedelta(seconds=self.max_last_login_delta)
if now() > (request.user.last_login + delta):
return logout_then_login(request, self.get_login_url())
else:
return resp
23 changes: 23 additions & 0 deletions docs/access.rst
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,29 @@ Similar to :ref:`SuperuserRequiredMixin`, this mixin allows you to require a use

template_name = u"path/to/template.html"

.. _RecentLoginRequiredMixin:

RecentLoginRequiredMixin
------------------------

.. versionadded:: 1.8.0

This mixin requires a user to have logged in within a certain number of seconds. This is to prevent stale sessions or to create a session time-out, as is often used for financial applications and the like. This mixin includes the functionality of `LoginRequiredMixin`_, so you don't need to use both on the same view.


::

from django.views.generic import TemplateView

from braces.views import RecentLoginRequiredMixin


class SomeSecretView(RecentLoginRequiredMixin, TemplateView):
max_last_login_delta = 600 # Require a login within the last 10 minutes
template_name = "path/to/template.html"



.. _Daniel Sokolowski: https://github.com/danols
.. _code here: https://github.com/lukaszb/django-guardian/issues/48
.. _user_passes_test: https://docs.djangoproject.com/en/1.6/topics/auth/default/#django.contrib.auth.decorators.user_passes_test
Expand Down
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Changelog
=========

* :feature:`138` New :ref:`RecentLoginRequiredMixin` to require user sessions to have a given freshness.
* :bug:`164` Use `resolve_url` to handle `LOGIN_REDIRECT_URL`s in `settings.py` that are just URL names.
* :bug:`130` New attribute on :ref:`JSONResponseMixin` to allow setting a custom JSON encoder class.
* :bug:`131` New attribute on :ref:`LoginRequiredMixin` so it's possible to redirect unauthenticated users while
Expand Down
31 changes: 30 additions & 1 deletion tests/test_access_mixins.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import

import datetime

from django import test
from django.test.utils import override_settings
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
Expand All @@ -12,7 +14,8 @@
from .views import (PermissionRequiredView, MultiplePermissionsRequiredView,
SuperuserRequiredView, StaffuserRequiredView,
LoginRequiredView, GroupRequiredView, UserPassesTestView,
UserPassesTestNotImplementedView, AnonymousRequiredView)
UserPassesTestNotImplementedView, AnonymousRequiredView,
RecentLoginRequiredView)


class _TestAccessBasicsMixin(TestViewHelper):
Expand Down Expand Up @@ -475,3 +478,29 @@ def test_not_implemented(self):
view.dispatch(
self.build_request(path=self.view_not_implemented_url),
raise_exception=True)


class TestRecentLoginRequiredMixin(test.TestCase):
"""
Tests for RecentLoginRequiredMixin.
"""
view_class = RecentLoginRequiredView
recent_view_url = '/recent_login/'
outdated_view_url = '/outdated_login/'

def test_recent_login(self):
self.view_class.max_last_login_delta = 1800
last_login = datetime.datetime.now()
user = UserFactory(last_login=last_login)
self.client.login(username=user.username, password='asdf1234')
resp = self.client.get(self.recent_view_url)
assert resp.status_code == 200
assert force_text(resp.content) == 'OK'

def test_outdated_login(self):
self.view_class.max_last_login_delta = 0
last_login = datetime.datetime.now() - datetime.timedelta(hours=2)
user = UserFactory(last_login=last_login)
self.client.login(username=user.username, password='asdf1234')
resp = self.client.get(self.outdated_view_url)
assert resp.status_code == 302
4 changes: 4 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@
url(r'all_verbs/$', views.AllVerbsView.as_view()),
url(r'all_verbs_no_handler/$',
views.AllVerbsView.as_view(all_handler=None)),

# RecentLoginRequiredMixin tests
url(r'^recent_login/$', views.RecentLoginRequiredView.as_view()),
url(r'^outdated_login/$', views.RecentLoginRequiredView.as_view()),
)


Expand Down
6 changes: 6 additions & 0 deletions tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,9 @@ class UserPassesTestNotImplementedView(views.UserPassesTestMixin, OkView):
class AllVerbsView(views.AllVerbsMixin, View):
def all(self, request, *args, **kwargs):
return HttpResponse('All verbs return this!')


class RecentLoginRequiredView(views.RecentLoginRequiredMixin, OkView):
"""
A view for testing RecentLoginRequiredMixin.
"""
3 changes: 1 addition & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[tox]
envlist = py26-django{14,15,16},
py27-django14,
envlist = py26-django{15,16},
py{27,33,34}-django{15,16,17,18}
install_command = pip install {opts} {packages}

Expand Down

0 comments on commit 24bf321

Please sign in to comment.