Skip to content

Commit

Permalink
Merge pull request #171 from brack3t/feature/sslrequiredmixin
Browse files Browse the repository at this point in the history
Feature/sslrequiredmixin
  • Loading branch information
kennethlove committed Apr 17, 2015
2 parents 24bf321 + fc5addd commit 4da32df
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 18 deletions.
5 changes: 4 additions & 1 deletion braces/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
StaffuserRequiredMixin,
SuperuserRequiredMixin,
UserPassesTestMixin,
SSLRequiredMixin,
RecentLoginRequiredMixin
)
from ._ajax import (
Expand Down Expand Up @@ -65,5 +66,7 @@
'SuccessURLRedirectListMixin',
'SuperuserRequiredMixin',
'UserFormKwargsMixin',
'UserPassesTestMixin'
'UserPassesTestMixin',
'SSLRequiredMixin',
'RecentLoginRequiredMixin'
]
32 changes: 30 additions & 2 deletions braces/views/_access.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import datetime

import re
import six

from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
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.http import (HttpResponseRedirect, HttpResponsePermanentRedirect,
Http404)
from django.shortcuts import resolve_url
from django.utils.encoding import force_text
from django.utils.timezone import now
Expand Down Expand Up @@ -398,6 +399,33 @@ def dispatch(self, request, *args, **kwargs):
request, *args, **kwargs)


class SSLRequiredMixin(object):
"""
Simple mixin that allows you to force a view to be accessed
via https.
"""
raise_exception = False # Default whether to raise an exception to none

def dispatch(self, request, *args, **kwargs):
if getattr(settings, 'DEBUG', False):
return super(SSLRequiredMixin, self).dispatch(
request, *args, **kwargs)

if not request.is_secure():
if self.raise_exception:
raise Http404

return HttpResponsePermanentRedirect(
self._build_https_url(request))

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

def _build_https_url(self, request):
""" Get the full url, replace http with https """
url = request.build_absolute_uri(request.get_full_path())
return re.sub(r'^http', 'https', url)


class RecentLoginRequiredMixin(LoginRequiredMixin):
"""
Mixin allows you to require a login to be within a number of seconds.
Expand Down
48 changes: 45 additions & 3 deletions docs/access.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ Multiple Groups Possible Usage
::

from django.views import TemplateView

from braces.views import GroupRequiredMixin


Expand Down Expand Up @@ -303,7 +303,7 @@ Similar to :ref:`SuperuserRequiredMixin`, this mixin allows you to require a use
::

from django.views import TemplateView

from braces import views


Expand All @@ -313,6 +313,49 @@ Similar to :ref:`SuperuserRequiredMixin`, this mixin allows you to require a use

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


.. _SSLRequiredMixin
SSLRequiredMixin
----------------

.. versionadded:: 1.8.0

Simple view mixin that requires the incoming request to be secure by checking
Django's `request.is_secure()` method. By default the mixin will return a
permanent (301) redirect to the https verison of the current url. Optionally
you can set `raise_exception=True` and a 404 will be raised.

Standard Django Usage
^^^^^^^^^^^^^^^^^^^^^

::

from django.views import TemplateView

from braces.views import SSLRequiredMixin


class SomeSecureView(SSLRequiredMixin, TemplateView):
""" Redirects from http -> https """
template_name = "path/to/template.html"

Standard Django Usage
^^^^^^^^^^^^^^^^^^^^^

::

from django.views import TemplateView

from braces.views import SSLRequiredMixin


class SomeSecureView(SSLRequiredMixin, TemplateView):
""" http request would raise 404. https renders view """
raise_exception = True
template_name = "path/to/template.html"


.. _RecentLoginRequiredMixin:

RecentLoginRequiredMixin
Expand All @@ -335,7 +378,6 @@ This mixin requires a user to have logged in within a certain number of seconds.
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
4 changes: 3 additions & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
Changelog
=========

* :release:`1.8.0 <2015-04-16>`
* :feature:`171` New :ref:`SSLRequiredMixin`. Redirect http -> https.
* :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.
Expand All @@ -23,7 +25,7 @@ Changelog
* :feature:`89` Added new :ref:`AnonymousRequiredMixin` which redirects authenticated users to another view.
* :feature:`104` Added new :ref:`AllVerbsMixin` which allows a single method to response to all HTTP verbs.
* :bug:`- major` Provided ``JSONRequestResponseMixin`` as a mirror of :ref:`JsonRequestResponseMixin` because we're not PHP.
* :feature:`107` :ref:`FormValidMessageMixin`, :ref:`FormInvalidMessageMixin`, and :ref:`FormMessagesMixin` all allow ``ugettext_lazy``-wrapped strings.
* :feature:`107` :ref:`FormValidMessageMixin`, :ref:`FormInvalidMessageMixin`, and :ref:`FormMessagesMixin` all allow ``ugettext_lazy``-wrapped strings.
* :feature:`67` Extended :ref:`PermissionRequiredMixin` and :ref:`MultiplePermissionsRequiredMixin` to accept django-guardian-style custom/object permissions.
* :release:`1.3.1 <2014-01-04>`
* :bug:`95` Removed accidentally-added breakpoint.
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@

# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
html_theme = 'alabaster'

# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
Expand Down
63 changes: 59 additions & 4 deletions tests/test_access_mixins.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import

import pytest
import datetime

from django import test
from django import VERSION as DJANGO_VERSION
from django.test.utils import override_settings
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.core.urlresolvers import reverse_lazy
Expand All @@ -15,7 +17,7 @@
SuperuserRequiredView, StaffuserRequiredView,
LoginRequiredView, GroupRequiredView, UserPassesTestView,
UserPassesTestNotImplementedView, AnonymousRequiredView,
RecentLoginRequiredView)
SSLRequiredView, RecentLoginRequiredView)


class _TestAccessBasicsMixin(TestViewHelper):
Expand Down Expand Up @@ -152,9 +154,8 @@ def test_authenticated(self):

def test_anonymous_redirects(self):
resp = self.dispatch_view(
self.build_request(path=self.view_url),
raise_exception=True,
redirect_unauthenticated_users=True)
self.build_request(path=self.view_url), raise_exception=True,
redirect_unauthenticated_users=True)
assert resp.status_code == 302
assert resp['Location'] == '/accounts/login/?next=/login_required/'

Expand Down Expand Up @@ -480,6 +481,60 @@ def test_not_implemented(self):
raise_exception=True)


class TestSSLRequiredMixin(test.TestCase):
view_class = SSLRequiredView
view_url = '/sslrequired/'

@pytest.mark.skipif(DJANGO_VERSION[:2] < (1, 7),
reason='Djanog 1.6 and below behave this differently')
def test_ssl_redirection_django_17_up(self):
self.view_class.raise_exception = False
resp = self.client.get(self.view_url)
self.assertRedirects(resp, self.view_url, status_code=301)
resp = self.client.get(self.view_url, follow=True)
self.assertEqual(200, resp.status_code)
self.assertEqual('https', resp.request.get('wsgi.url_scheme'))

@pytest.mark.skipif(DJANGO_VERSION[:2] > (1, 6),
reason='Django 1.7 and above behave differently')
def test_ssl_redirection_django_16_down(self):
self.view_class.raise_exception = False
resp = self.client.get(self.view_url)
self.assertEqual(301, resp.status_code)
resp = self.client.get(self.view_url, follow=True)
self.assertEqual(200, resp.status_code)
self.assertEqual('https', resp.request.get('wsgi.url_scheme'))

def test_raises_exception(self):
self.view_class.raise_exception = True
resp = self.client.get(self.view_url)
self.assertEqual(404, resp.status_code)

@override_settings(DEBUG=True)
def test_debug_bypasses_redirect(self):
self.view_class.raise_exception = False
resp = self.client.get(self.view_url)
self.assertEqual(200, resp.status_code)

@pytest.mark.skipif(
DJANGO_VERSION[:2] < (1, 7),
reason='Djanog 1.6 and below does not have the secure=True option')
def test_https_does_not_redirect_django_17_up(self):
self.view_class.raise_exception = False
resp = self.client.get(self.view_url, secure=True)
self.assertEqual(200, resp.status_code)
self.assertEqual('https', resp.request.get('wsgi.url_scheme'))

@pytest.mark.skipif(
DJANGO_VERSION[:2] > (1, 6),
reason='Django 1.7 and above have secure=True option, below does not')
def test_https_does_not_redirect_django_16_down(self):
self.view_class.raise_exception = False
resp = self.client.get(self.view_url, **{'wsgi.url_scheme': 'https'})
self.assertEqual(200, resp.status_code)
self.assertEqual('https', resp.request.get('wsgi.url_scheme'))


class TestRecentLoginRequiredMixin(test.TestCase):
"""
Tests for RecentLoginRequiredMixin.
Expand Down
6 changes: 3 additions & 3 deletions tests/test_other_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import mock
import pytest

import django
from django import VERSION as DJANGO_VERSION
from django.contrib import messages
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.messages.storage.base import Message
Expand Down Expand Up @@ -589,7 +589,7 @@ def get(self, request):
self.get_request_response(TestView.as_view())

@pytest.mark.skipif(
django.VERSION < (1, 5),
DJANGO_VERSION < (1, 5),
reason='Some features of MessageMixin are only available in '
'Django >= 1.5')
def test_wrapper_available_in_dispatch(self):
Expand Down Expand Up @@ -618,7 +618,7 @@ def test_API(self):
# This test is designed to break when django.contrib.messages.api
# changes (items being added or removed).
excluded_API = set()
if django.VERSION >= (1, 7):
if DJANGO_VERSION >= (1, 7):
excluded_API.add('MessageFailure')
self.assertEqual(
_MessageAPIWrapper.API | excluded_API,
Expand Down
3 changes: 3 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@
url(r'all_verbs_no_handler/$',
views.AllVerbsView.as_view(all_handler=None)),

# SSLRequiredMixin tests
url(r'^sslrequired/$', views.SSLRequiredView.as_view()),

# RecentLoginRequiredMixin tests
url(r'^recent_login/$', views.RecentLoginRequiredView.as_view()),
url(r'^outdated_login/$', views.RecentLoginRequiredView.as_view()),
Expand Down
7 changes: 6 additions & 1 deletion tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ class ArticleListViewWithCustomQueryset(views.SelectRelatedMixin, ListView):
"""
Another list view for articles, required to test SelectRelatedMixin.
"""
queryset = Article.objects.select_related('author').prefetch_related('article_set')
queryset = Article.objects.select_related('author').prefetch_related(
'article_set')
template_name = 'blank.html'
select_related = ()

Expand Down Expand Up @@ -335,6 +336,10 @@ def all(self, request, *args, **kwargs):
return HttpResponse('All verbs return this!')


class SSLRequiredView(views.SSLRequiredMixin, OkView):
pass


class RecentLoginRequiredView(views.RecentLoginRequiredMixin, OkView):
"""
A view for testing RecentLoginRequiredMixin.
Expand Down
2 changes: 0 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ deps =
factory_boy
coverage
argparse
django14: Django>=1.4,<1.5
django15: Django>=1.5,<1.6
django16: Django>=1.6,<1.7
django17: Django>=1.7,<1.8
django18: Django>=1.8,<1.9

0 comments on commit 4da32df

Please sign in to comment.