Skip to content
This repository has been archived by the owner on May 29, 2019. It is now read-only.

added specific support for incoming cloudfront, ignoring ips which ar… #16

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
23 changes: 18 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,21 @@ Configuration

Turning on or off, and configuring the IP whitelist is done either via variables in your Django settings, or via environment variables. Values in Django settings take preference over values in the environment.

Turning on/off the middleware is done via ``RESTRICT_IPS``, and the default value is False. Either set this variable to True in Django settings, or set a truthy value (e.g. 'true', '1') in your environment.
Turning on/off the middleware is done via ``RESTRICT_IPS``, and the default value is False. Either set this variable to True in Django settings, or set a truthy value (e.g. 'true', '1') in your environment.

Individual IPs can be whitelisted via ``ALLOWED_IPS``, which is either a list of IP strings in Django settings, or a comma-separated list of IPs in the environment, e.g the following 2 are equivalent::

# in bash (spaces are disregarded, trailing commas are OK)
export ALLOWED_IPS='192.168.0.1, 192.168.0.2,192.168.0.3,'

# in settings.py (will override the above environment variable)
ALLOWED_IPS = ['192.168.0.1', '192.168.0.2', '192.168.0.3']

IP ranges can be whitelisted via ``ALLOWED_IP_RANGES``, which is either a list of IP range strings (CIDR notation) in Django settings, or a comma-separated list of IP ranges in the environment, e.g.::

# in bash
export ALLOWED_IP_RANGES='192.168.0.0/8, 127.0.0.0/2'

# in settings.py
ALLOWED_IPS = ['192.168.0.0/8', '127.0.0.0/2']

Expand All @@ -66,6 +66,19 @@ Regardless of the IP addresses/rages that are in the whitelist, access to the ad

Setting both ``ALLOW_ADMIN`` *and* ``ALLOW_AUTHENTICATED`` to true is recommended, and will allow any user that can log in, to first access only the admin interface in order to authenitcate, and from then have access to all URLs for the project.

The following two settings relate to CloudFront and the way forwarded IPs are provided as well as
the additional security of the secret token.

Setting ``SECRET_HEADER_NAME`` and ``SECRET_HEADER_VALUE`` will cause requests to be rejected
if the value of a header specified in SECRET_HEADER_NAME does not match the SECRET_HEADER_VALUE provided.

``REAL_IP_POSITION`` can be provided to support scenarios using CloudFront where the real IP is positioned
in a list of IPs provided, and is zero based and counted from the end. For example, with a forwarded ip list of:

`x_forwarded_for:"193.240.177.2, 193.240.177.99, 216.137.62.70, 127.0.0.1"`

The real IP is the 3rd from the left, and the 4th one is assumed to be a spoofing attempt. The value is therefore
set to 2.

Restict Admin views only
------------------------
Expand Down Expand Up @@ -99,7 +112,7 @@ TODO
====

* Allow the IP restriction to work in a blacklisting mode, rather than just a whitelisting mode
* Get continuous integration to run on multiple python versions from 3.0+
* Get continuous integration to run on multiple python versions from 3.0+
- Currently only running on 3.5.0
- Utilise parallelism
* Run tests on multiple Django versions
Expand Down
25 changes: 20 additions & 5 deletions ip_restriction/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@ def __init__(self, get_response=None):
self.RESTRICT_ADMIN_BY_IPS = self._get_config_var('RESTRICT_ADMIN_BY_IPS', bool)
self.ALLOWED_ADMIN_IPS = self._get_config_var('ALLOWED_ADMIN_IPS', list)
self.ALLOWED_ADMIN_IP_RANGES = self._get_config_var('ALLOWED_ADMIN_IP_RANGES', list)
# Cloudfront settings
self.SECRET_HEADER_NAME = self._get_config_var('SECRET_HEADER_NAME', str)
self.SECRET_HEADER_VALUE = self._get_config_var('SECRET_HEADER_VALUE', str)
self.REAL_IP_POSITION = self._get_config_var('REAL_IP_POSITION', int)

def __call__(self, request):
response = self.process_request(request)

if not response and self.get_response:
response = self.get_response(request)

return response

def _get_config_var(self, name, vartype):
Expand All @@ -54,6 +58,8 @@ def _get_config_var(self, name, vartype):
return setting_val is True
else:
return env_val.lower() == 'true' or env_val == '1'
elif vartype in (int, str):
return vartype(env_val) if env_val is not None else None
else:
if env_val is None:
return setting_val if setting_val is not None else []
Expand All @@ -68,10 +74,9 @@ def get_client_ip_list(self, request):
"""

x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')

if x_forwarded_for:
ips = x_forwarded_for.split(',')
ips = map(str.strip, ips)
ips = list(map(str.strip, ips))
else:
ips = [request.META.get('REMOTE_ADDR')]

Expand All @@ -80,10 +85,20 @@ def get_client_ip_list(self, request):
def is_blocked_ip(self, request, allowed_ips, allowed_ip_ranges):
# Default blocked
block_request = True
cloudfront = False
if self.SECRET_HEADER_NAME and self.SECRET_HEADER_VALUE:
if request.META.get(self.SECRET_HEADER_NAME) != self.SECRET_HEADER_VALUE:
return block_request
else:
cloudfront = True

# Get the incoming IP address
request_ips = self.get_client_ip_list(request)

# Account for CloudFront forwarded IPs. Dilute the ip list only to the original ip
if cloudfront and self.REAL_IP_POSITION and len(request_ips) > self.REAL_IP_POSITION:
request_ips = [request_ips[-1 * self.REAL_IP_POSITION + 1]]

for request_ip_str in request_ips:
request_ip = ipaddress.ip_address(request_ip_str)

Expand All @@ -105,7 +120,7 @@ def is_blocked_ip(self, request, allowed_ips, allowed_ip_ranges):
break

if block_request is False:
break
break

return block_request

Expand Down
46 changes: 46 additions & 0 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ def _get_response_code_for_header(self, ips, url=example_url, login=False):
resp = client.get(url)
return resp.status_code

def _get_response_code_for_cloudfront(self, ips, url=example_url, login=False):
# Helper function to set an originating IP address to the given IP, and request
# our one example view, returning the response's status code
client = Client(
HTTP_X_FORWARDED_FOR=ips,
HTTP_X_CDN_SECRET='secret')
if login:
client.login(username=self.user.username, password=self.password)

resp = client.get(url)
return resp.status_code

def test_default_no_ip_restriction(self):
# By default, without specifying in settings or environment variables
# requests should be allowed and not interefed with by the middleware
Expand Down Expand Up @@ -189,3 +201,37 @@ def test_allow_all_views_restrict_admin_views(self):
url=admin_url
)
self.assertEqual(response_code, 404)

@override_environment(
RESTRICT_IPS=True,
ALLOWED_IPS=['127.0.0.1', '192.168.0.1'],
SECRET_HEADER_NAME='HTTP_X_CDN_SECRET',
SECRET_HEADER_VALUE='secret',
REAL_IP_POSITION=2)
def test_cloudfront_allowed_ips(self):
print("CLOUDFRONT")
# Restict IPs to a list of known IPs and check that they are allowed, but other IPs
# are forbidden
code = self._get_response_code_for_cloudfront('199.168.0.1, 192.168.0.1, 216.137.62.70, 127.0.0.1')
self.assertEqual(code, 200)

code = self._get_response_code_for_cloudfront('192.168.0.1')
self.assertEqual(code, 200)

code = self._get_response_code_for_cloudfront('127.0.0.2')
self.assertEqual(code, 403)

code = self._get_response_code_for_cloudfront('192.168.0.2')
self.assertEqual(code, 403)

# Check multiple IPs in the header, first 2 bad IPs
# code = self._get_response_code_for_header('127.0.0.2, 192.168.0.2')
# self.assertEqual(code, 403)

# # Check a blocked one, and an allowed one
# code = self._get_response_code_for_header('127.0.0.2, 192.168.0.1')
# self.assertEqual(code, 200)

# # Check an allowed one, and a blocked one
# code = self._get_response_code_for_header('127.0.0.1, 192.168.0.2')
# self.assertEqual(code, 200)