Skip to content

Commit

Permalink
Implements a means of restricting the toolbar to specific...
Browse files Browse the repository at this point in the history
... IP address or ranges (if iptools is used).

Includes tests and docs.
  • Loading branch information
mkoistinen committed Dec 28, 2015
1 parent acce542 commit 0552df7
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 6 deletions.
47 changes: 42 additions & 5 deletions cms/middleware/toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""
Edit Toolbar middleware
"""

from django.contrib.admin.models import LogEntry, ADDITION, CHANGE
from django.core.urlresolvers import resolve
from django.http import HttpResponse
Expand All @@ -14,6 +15,35 @@
from menus.menu_pool import menu_pool


def get_client_ip(request):
"""
Returns the REMOTE_ADDR (IP address of the calling client). Is aware
of properly configured proxies and uses the X-FOWARDED-FOR header, if
available, in the canonical manner.
:param request:
:return: Client IP address (String)
"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip


def is_allowed_ip_address(request):
# NOTE: This is `settings.CMS_INTERNAL_IPS`, not the Django variant,
# `settings.INTERNAL_IPS`
INTERNAL_IPS = get_cms_setting('INTERNAL_IPS')

# For backwards compatibility, if INTERNAL_IPS is empty, all are allowed.
if not INTERNAL_IPS:
return True

client_ip = get_client_ip(request)
return client_ip in INTERNAL_IPS


def toolbar_plugin_processor(instance, placeholder, rendered_content, original_context):
from cms.plugin_pool import plugin_pool

Expand Down Expand Up @@ -81,12 +111,15 @@ def process_request(self, request):
disable = get_cms_setting('CMS_TOOLBAR_URL__DISABLE')
anonymous_on = get_cms_setting('TOOLBAR_ANONYMOUS_ON')

if disable in request.GET:
request.session['cms_toolbar_disabled'] = True
if edit_on in request.GET: # If we actively enter edit mode, we should show the toolbar in any case
request.session['cms_toolbar_disabled'] = False
allowed_ip = is_allowed_ip_address(request)

if allowed_ip:
if disable in request.GET:
request.session['cms_toolbar_disabled'] = True
if edit_on in request.GET: # If we actively enter edit mode, we should show the toolbar in any case
request.session['cms_toolbar_disabled'] = False

if request.user.is_staff or (anonymous_on and request.user.is_anonymous()):
if allowed_ip and (request.user.is_staff or (anonymous_on and request.user.is_anonymous())):
if edit_on in request.GET and not request.session.get('cms_edit', False):
if not request.session.get('cms_edit', False):
menu_pool.clear()
Expand All @@ -104,6 +137,9 @@ def process_request(self, request):
else:
request.session['cms_build'] = False
request.session['cms_edit'] = False
request.session['cms_toolbar_disabled'] = True
# menu_pool.clear()

if request.user.is_staff:
try:
request.cms_latest_entry = LogEntry.objects.filter(
Expand All @@ -112,6 +148,7 @@ def process_request(self, request):
).only('pk').order_by('-pk')[0].pk
except IndexError:
request.cms_latest_entry = -1

request.toolbar = CMSToolbar(request)

def process_view(self, request, view_func, view_args, view_kwarg):
Expand Down
32 changes: 32 additions & 0 deletions cms/tests/test_toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,38 @@ def test_markup(self):
self.assertContains(response, '<div id="cms-top"')
self.assertContains(response, 'cms.base.css')

# This test assumes that the test environment will use 127.0.0.1, but this
# may not be a valid assumption, in which case, this test can be removed.
@override_settings(CMS_INTERNAL_IPS=['127.0.0.1'])
def test_toolbar_allowed_internal_ip(self):
create_page("toolbar-page", "nav_playground.html", "en", published=True)
superuser = self.get_superuser()
with self.login_user_context(superuser):
response = self.client.get('/en/?%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<div class="cms-toolbar">')

# As of version 3.2.1, an empty list means any IP is allowed.
@override_settings(CMS_INTERNAL_IPS=[])
def test_toolbar_any_ip_allowed(self):
create_page("toolbar-page", "nav_playground.html", "en", published=True)
superuser = self.get_superuser()
with self.login_user_context(superuser):
response = self.client.get('/en/?%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<div class="cms-toolbar">')

# Here we're just using an illegal IP, which essentially means no IP
# is allowed.
@override_settings(CMS_INTERNAL_IPS=['255.255.255.255'])
def test_toolbar_disallowed_internal_ip(self):
create_page("toolbar-page", "nav_playground.html", "en", published=True)
superuser = self.get_superuser()
with self.login_user_context(superuser):
response = self.client.get('/en/?%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, '<div class="cms-toolbar">')

def test_markup_generic_module(self):
create_page("toolbar-page", "col_two.html", "en", published=True)
superuser = self.get_superuser()
Expand Down
1 change: 1 addition & 0 deletions cms/utils/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def wrapper():
'WIZARD_DEFAULT_TEMPLATE': constants.TEMPLATE_INHERITANCE_MAGIC,
'WIZARD_CONTENT_PLUGIN': 'TextPlugin',
'WIZARD_CONTENT_PLUGIN_BODY': 'body',
'INTERNAL_IPS': [],
}


Expand Down
10 changes: 9 additions & 1 deletion cms/utils/setup.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-

from warnings import warn

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

from cms.utils.compat import DJANGO_1_6, DJANGO_1_7
from cms.utils.compat.dj import is_installed as app_is_installed

from cms.utils.conf import get_cms_setting

def validate_dependencies():
"""
Expand Down Expand Up @@ -38,6 +42,10 @@ def validate_settings():
'django.template.context_processors.request' not in context_processors):
raise ImproperlyConfigured("django CMS requires django.template.context_processors.request in "
"'django.template.backends.django.DjangoTemplates' context processors.")
# Warn on empty CMS_INTERNAL_IPS
if not get_cms_setting('INTERNAL_IPS'):
warn('The setting `CMS_INTERNAL_IPS` was not defined or is empty, this '
'may allow any IP address to access the CMS toolbar.')


def setup():
Expand Down
47 changes: 47 additions & 0 deletions docs/reference/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,53 @@ user under which Django will be running.
Advanced Settings
*****************

.. setting:: CMS_INTERNAL_IPS

CMS_INTERNAL_IPS
================

.. note:: New as of version 3.2.1

This setting is used to determine if the current HTTP client is allowed to see
the CMS Toolbar, provided that other checks are satisfied.

For backwards compatibility, the default value of this setting is an empty list
and the default behaviour of the CMS when provided an empty list is to allow all
IP addresses. However, when this setting is not defined or contains an empty
list (evaluates to ``None``), a warning will be emitted to the console on
startup.

Example::

# settings.py
...
CMS_INTERNAL_IPS = [
'10.0.0.1',
'10.0.0.2',
'10.0.0.3',
]
...

If larger set of IP addresses are to be used, consider installing and using
`iptools <https://python-iptools.readthedocs.org/en/latest/>`_, which allows
specifying whole ranges in CIDR notation for IPV4 and IPV6 addresses.

Example using iptools::

# settings.py
...
# This example from the iptools docs at:
# https://python-iptools.readthedocs.org/en/latest/#installation
CMS_INTERNAL_IPS = iptools.IpRangeList(
'127.0.0.1', # single ip
'192.168/16', # CIDR network block
('10.0.0.1', '10.0.0.19'), # arbitrary inclusive range
'::1', # single IPv6 address
'fe80::/10', # IPv6 CIDR block
'::ffff:172.16.0.2' # IPv4-mapped IPv6 address
)
...

.. setting:: CMS_PERMISSION

CMS_PERMISSION
Expand Down

0 comments on commit 0552df7

Please sign in to comment.