Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/sslrequiredmixin #171

Merged
merged 19 commits into from
Apr 17, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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