Skip to content

Commit

Permalink
Merge pull request #5173 from mkoistinen/feature/restrict_toolbar_to_…
Browse files Browse the repository at this point in the history
…internal_ips

Implement CMS_INTERNAL_IPS
  • Loading branch information
mkoistinen committed Apr 20, 2016
2 parents e34fc26 + f9bdd34 commit 511df90
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.txt
Expand Up @@ -14,6 +14,10 @@
* Removed frontend integration tests (written with Selenium)
* Provides the ability to declare cache expiration periods on a per-plugin basis
* Minor UI improvements
* Adds new setting CMS_INTERNAL_IPS for defining a set of IP address for which
the toolbar will appear for authorized users. If left unset, retains the
existing behavior of allowing toolbar for authorized users at any IP address.


=== 3.2.3 (2016-03-09) ===

Expand Down
9 changes: 9 additions & 0 deletions cms/middleware/toolbar.py
Expand Up @@ -12,8 +12,11 @@
from cms.utils.conf import get_cms_setting
from cms.utils.i18n import force_language
from cms.utils.placeholder import get_toolbar_plugin_struct
from cms.utils.request_ip_resolvers import get_request_ip_resolver
from menus.menu_pool import menu_pool

get_request_ip = get_request_ip_resolver()


def toolbar_plugin_processor(instance, placeholder, rendered_content, original_context):
from cms.plugin_pool import plugin_pool
Expand Down Expand Up @@ -54,6 +57,12 @@ class ToolbarMiddleware(object):

def is_cms_request(self, request):
toolbar_hide = get_cms_setting('TOOLBAR_HIDE')
internal_ips = get_cms_setting('INTERNAL_IPS')

if internal_ips:
client_ip = get_request_ip(request)
if client_ip not in internal_ips:
return False

if not toolbar_hide:
return True
Expand Down
27 changes: 27 additions & 0 deletions cms/tests/test_toolbar.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

import datetime
import iptools
import re

from django.contrib import admin
Expand Down Expand Up @@ -128,6 +130,31 @@ def test_app_setted_show_toolbar_in_cms_urls_subpage(self):
request = self.get_page_request(page, self.get_anon())
self.assertTrue(hasattr(request, 'toolbar'))

def test_cms_internal_ips_unset(self):
with self.settings(CMS_INTERNAL_IPS=[]):
request = self.get_page_request(None, self.get_staff(), '/en/example/')
self.assertTrue(hasattr(request, 'toolbar'))

def test_cms_internal_ips_set_no_match(self):
with self.settings(CMS_INTERNAL_IPS=['123.45.67.89', ]):
request = self.get_page_request(None, self.get_staff(), '/en/example/')
self.assertFalse(hasattr(request, 'toolbar'))

def test_cms_internal_ips_set_match(self):
with self.settings(CMS_INTERNAL_IPS=['127.0.0.0', '127.0.0.1', '127.0.0.2', ]):
request = self.get_page_request(None, self.get_staff(), '/en/example/')
self.assertTrue(hasattr(request, 'toolbar'))

def test_cms_internal_ips_iptools(self):
with self.settings(CMS_INTERNAL_IPS=iptools.IpRangeList(('127.0.0.0', '127.0.0.255'))):
request = self.get_page_request(None, self.get_staff(), '/en/example/')
self.assertTrue(hasattr(request, 'toolbar'))

def test_cms_internal_ips_iptools_bad_range(self):
with self.settings(CMS_INTERNAL_IPS=iptools.IpRangeList(('128.0.0.0', '128.0.0.255'))):
request = self.get_page_request(None, self.get_staff(), '/en/example/')
self.assertFalse(hasattr(request, 'toolbar'))


@override_settings(CMS_PERMISSION=False)
class ToolbarTests(ToolbarTestBase):
Expand Down
2 changes: 2 additions & 0 deletions cms/utils/conf.py
Expand Up @@ -65,6 +65,8 @@ def wrapper():
'WIZARD_DEFAULT_TEMPLATE': constants.TEMPLATE_INHERITANCE_MAGIC,
'WIZARD_CONTENT_PLUGIN': 'TextPlugin',
'WIZARD_CONTENT_PLUGIN_BODY': 'body',
'INTERNAL_IPS': settings.INTERNAL_IPS, # Django default is []
'REQUEST_IP_RESOLVER': 'cms.utils.request_ip_resolvers.default_request_ip_resolver',
}


Expand Down
76 changes: 76 additions & 0 deletions cms/utils/request_ip_resolvers.py
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-

import importlib

from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import ugettext as _

from cms.utils.conf import get_cms_setting


def get_request_ip_resolver():
"""
This is the recommended method for obtaining the specified
CMS_REQUEST_IP_RESOLVER as it also does some basic import validation.
Returns the resolver or raises an ImproperlyConfigured exception.
"""
module, attribute = get_cms_setting('REQUEST_IP_RESOLVER').rsplit('.', 1)
try:
ip_resolver_module = importlib.import_module(module)
ip_resolver = getattr(ip_resolver_module, attribute)
except ImportError:
raise ImproperlyConfigured(
_('Unable to find the specified CMS_REQUEST_IP_RESOLVER module: '
'"%s".') % module)
except AttributeError:
raise ImproperlyConfigured(
_('Unable to find the specified CMS_REQUEST_IP_RESOLVER function: '
'"%s" in module "%s".') % (attribute, module, ))
return ip_resolver


def default_request_ip_resolver(request):
"""
This is a hybrid request IP resolver that attempts should address most
cases. Order is important here. A 'REAL_IP' header supersedes an
'X_FORWARDED_FOR' header which supersedes a 'REMOTE_ADDR' header.
"""
return (
real_ip(request) or
x_forwarded_ip(request) or
remote_addr_ip(request)
)


def real_ip(request):
"""
Returns the IP Address contained in the HTTP_X_REAL_IP headers, if
present. Otherwise, `None`.
Should handle Nginx and some other WSGI servers.
"""
return request.META.get('HTTP_X_REAL_IP')


def remote_addr_ip(request):
"""
Returns the IP Address contained in the 'REMOTE_ADDR' header, if
present. Otherwise, `None`.
Should be suitable for local-development servers and some HTTP servers.
"""
return request.META.get('REMOTE_ADDR')


def x_forwarded_ip(request):
"""
Returns the IP Address contained in the 'HTTP_X_FORWARDED_FOR' header, if
present. Otherwise, `None`.
Should handle properly configured proxy servers.
"""
ip_address_list = request.META.get('HTTP_X_FORWARDED_FOR')
if ip_address_list:
ip_address_list = ip_address_list.split(',')
return ip_address_list[0]
41 changes: 41 additions & 0 deletions docs/reference/configuration.rst
Expand Up @@ -717,6 +717,47 @@ user under which Django will be running.
Advanced Settings
*****************

.. setting:: CMS_INTERNAL_IPS

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

default
settings.INTERNAL_IPS

By default ``CMS_INTERNAL_IPS`` takes the same value as the Django setting
``INTERNAL_IPS`` which has a default value of an empty list.

If left as an empty list (or anything Falsey, really), this setting does not
add any restrictions to the toolbar. However, if set, the toolbar will only
appear for client IP addresses that are in this list.

This setting may also be set to an `IpRangeList` from the external package
``iptools``. This package allows convenient syntax for defining complex IP
address ranges.

The client IP address is obtained via the :setting:`CMS_REQUEST_IP_RESOLVER`
in the ``cms.middleware.toolbar.ToolbarMiddleware`` middleware.


.. setting:: CMS_REQUEST_IP_RESOLVER

CMS_REQUEST_IP_RESOLVER
=======================

default
'cms.utils.request_ip_resolvers.default_request_ip_resolver'

This setting is used system-wide to provide a consistent and plug-able means
of extracting a client IP address from the HTTP request. The default
implementation should work for most project architectures, but if not, the
administrator can provide their own method to handle the project's
specific circumstances.

The supplied method should accept a single argument `request` and return an
IP address String.


.. setting:: CMS_PERMISSION

CMS_PERMISSION
Expand Down
1 change: 1 addition & 0 deletions test_requirements/requirements_base.txt
Expand Up @@ -20,6 +20,7 @@ django-classy-tags>=0.7.2
-e git+git://github.com/divio/djangocms-link.git#egg=djangocms-link
https://github.com/ojii/django-better-test/archive/8aa2407d097fe3789b74682f0e6bd7d15d449416.zip#egg=django-better-test
https://github.com/ojii/django-app-manage/archive/65da18ef234a4e985710c2c0ec760023695b40fe.zip#egg=django-app-manage
iptools
sphinx
sphinxcontrib-spelling
pyflakes
Expand Down

0 comments on commit 511df90

Please sign in to comment.