From 518871149526dfb5fe44eabd1abe05818d262c95 Mon Sep 17 00:00:00 2001 From: Alessandro De Noia Date: Wed, 21 Mar 2018 10:01:35 +0000 Subject: [PATCH] added missing files --- .coveragerc | 3 + LICENSE | 21 ++ MANIFEST.in | 3 + README.rst | 92 ++++++++ admin_ip_restrictor/middleware.py | 91 ++++++++ requirements_flake8.txt | 11 + setup.cfg | 27 +++ tests/__init__.py | 0 tests/conftest.py | 9 + tests/settings.py | 50 +++++ tests/test_middleware.py | 345 ++++++++++++++++++++++++++++++ tests/urls.py | 7 + tox.ini | 28 +++ 13 files changed, 687 insertions(+) create mode 100644 .coveragerc create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 admin_ip_restrictor/middleware.py create mode 100644 requirements_flake8.txt create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/settings.py create mode 100644 tests/test_middleware.py create mode 100644 tests/urls.py create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..fa4feaf --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = admin_ip_restrictor +omit = .tox/*,.circleci/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..939315b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 UK Trade & Investment (UKTI) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..d0c5460 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include README.rst +prune tests \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..f4607cc --- /dev/null +++ b/README.rst @@ -0,0 +1,92 @@ +Django Admin IP Restrictor +========================== + +.. image:: https://circleci.com/gh/uktrade/django-admin-ip-restrictor/tree/master.svg?style=shield + :target: https://circleci.com/gh/uktrade/django-admin-ip-restrictor/tree/master + +.. image:: https://codecov.io/gh/uktrade/django-admin-ip-restrictor/branch/master/graph/badge.svg + :target: https://codecov.io/gh/uktrade/django-admin-ip-restrictor + +.. image:: https://img.shields.io/pypi/v/django-admin-ip-restrictor.svg + :target: https://pypi.python.org/pypi/django-admin-ip-restrictor + +.. image:: https://img.shields.io/pypi/pyversions/django-admin-ip-restrictor.svg + :target: https://pypi.python.org/pypi/django-admin-ip-restrictor + +.. image:: https://img.shields.io/pypi/l/django-admin-ip-restrictor.svg + :target: https://pypi.python.org/pypi/django-admin-ip-restrictor + +A Django middleware to restrict the access to the Django admin based on incoming IPs + +Requirements +------------ + +* Python >= 3.4 +* Django >= 1.9 +* django-ipware=>2,<3 + + +Usage +----- + +First install the package:: + + $ pip install django-admin-ip-restrictor + +Then add the middleware to your settings:: + + # Django 1.10+ + MIDDLEWARE = [ + ... + 'admin_ip_restrictor.middleware.AdminIPRestrictorMiddleware' + ] + + # Django 1.9 + MIDDLEWARE_CLASSES = [ + ... + 'admin_ip_restrictor.middleware.AdminIPRestrictorMiddleware' + ] + +Set these variables in your `settings.py` to control who has access to the admin (IPV4 and IPV6 can be mixed):: + + RESTRICT_ADMIN=True + ALLOWED_ADMIN_IPS=['127.0.0.1', '::1'] + ALLOWED_ADMIN_IP_RANGES=['127.0.0.0/24', '::/1'] + + +If using environment variables make sure that the variables receive the right type of value. +`django-admin-ip-restrictor` automatically converts the following formats:: + + $ export RESTRICT_ADMIN='true' + $ export ALLOWED_ADMIN_IPS='127.0.0.1,::1' + $ export ALLOWED_ADMIN_IP_RANGES='127.0.0.0/24,::/1' + + +For `RESTRICT_ADMIN` also these values can be used: `True`, `1`, `false`, `False`, `0` + +Run tests +--------- + +Install `tox` and `pyenv`:: + + $ pip install tox pyenv + + +Install Python versions in `pyenv`:: + + $ pyenv install 3.4.4 + $ pyenv install 3.5.3 + $ pyenv install 3.6.2 + +Specify the Python versions you want to test with:: + + $ pyenv local 3.4.4 3.5.3 3.6.2 + +Run tests:: + + $ tox + +Contribute +---------- + +Fork the project and submit a PR! diff --git a/admin_ip_restrictor/middleware.py b/admin_ip_restrictor/middleware.py new file mode 100644 index 0000000..1dea5b1 --- /dev/null +++ b/admin_ip_restrictor/middleware.py @@ -0,0 +1,91 @@ +import ipaddress + +from django.conf import settings +from django.http import Http404 +from ipware.ip2 import get_client_ip + +try: + from django.urls import resolve +except ImportError: # pragma: no cover + from django.core.urlresolvers import resolve + + +class AdminIPRestrictorMiddleware(object): + + def __init__(self, get_response=None): + self.get_response = get_response + restrict_admin = getattr( + settings, + 'RESTRICT_ADMIN', + False + ) + self.restrict_admin = self.parse_bool_envars( + restrict_admin + ) + allowed_admin_ips = getattr( + settings, + 'ALLOWED_ADMIN_IPS', + [] + ) + self.allowed_admin_ips = self.parse_list_envars( + allowed_admin_ips + ) + allowed_admin_ip_ranges = getattr( + settings, + 'ALLOWED_ADMIN_IP_RANGES', + [] + ) + self.allowed_admin_ip_ranges = self.parse_list_envars( + allowed_admin_ip_ranges + ) + + def __call__(self, request): + response = self.process_request(request) + + if not response and self.get_response: + response = self.get_response(request) + + return response + + @staticmethod + def parse_bool_envars(value): + if value in ('true', 'True', '1', 1): + return True + return False + + @staticmethod + def parse_list_envars(value): + if type(value) == list: + return value + else: + return value.split(',') + + def is_blocked(self, ip): + """Determine if an IP address should be considered blocked.""" + blocked = True + + if ip in self.allowed_admin_ips: + blocked = False + + for allowed_range in self.allowed_admin_ip_ranges: + if ipaddress.ip_address(ip) in ipaddress.ip_network(allowed_range): + blocked = False + + return blocked + + def get_ip(self, request): + client_ip, is_routable = get_client_ip(request) + assert client_ip, 'IP not found' + assert is_routable, 'IP is private' + return client_ip + + def process_request(self, request): + if self.restrict_admin: + ip = self.get_ip(request) + is_admin_app = resolve(request.path).app_name == 'admin' + conditions = (is_admin_app, self.is_blocked(ip)) + + if all(conditions): + raise Http404() + + return None diff --git a/requirements_flake8.txt b/requirements_flake8.txt new file mode 100644 index 0000000..d2a6fe3 --- /dev/null +++ b/requirements_flake8.txt @@ -0,0 +1,11 @@ +# Code static analysis +pep8==1.7.0 +flake8==3.5.0 +flake8-blind-except==0.1.1 +flake8-debugger==1.4.0 +flake8-import-order==0.13 +flake8-docstrings==1.1.0 +flake8-print==2.0.2 +flake8-quotes==0.11.0 +flake8-string-format==0.2.3 +pep8-naming==0.4.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0f52456 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,27 @@ +# D203: 1 blank line required before class docstring +# D100: Missing docstring in public module +# D101: Missing docstring in public class +# D102: Missing docstring in public method +# D103: Missing docstring in public function +# D104: Missing docstring in public package +# D105: Missing docstring in magic method +# D106: Missing docstring in public nested class +# D107: Missing docstring in __init__ +# D401: First line should be imperative + +[flake8] +max-line-length = 120 +exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,node_modules,*settings*,manage.py,wsgi.py +ignore = D203, D100, D101, D102, D103, D104, D105, D106, D107, D401 +max-complexity = 7 +application-import-names = money_tracker +import_order_style = smarkets + +[pycodestyle] +max-line-length = 120 +exclude=.tox,.git,*/migrations/*,*/static/CACHE/*,node_modules,*settings*,manage.py,wsgi.py +ignore = D203, D100, D101, D102, D103, D104, D105, D106, D107, D401 + +[tool:pytest] +testpaths = tests/ +norecursedirs = .tox diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2dd320d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import os + +import django + + +def pytest_configure(config): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') + django.setup() + diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..06ab3ec --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,50 @@ +from django import VERSION as DJANGO_VERSION + +SECRET_KEY = 'fake-key' + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.admin', + 'tests' +] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } +} + +ROOT_URLCONF = 'tests.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + + +if DJANGO_VERSION < (1, 10): + MIDDLEWARE_CLASSES = [ + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'admin_ip_restrictor.middleware.AdminIPRestrictorMiddleware' + ] +else: + MIDDLEWARE = [ + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'admin_ip_restrictor.middleware.AdminIPRestrictorMiddleware' + ] diff --git a/tests/test_middleware.py b/tests/test_middleware.py new file mode 100644 index 0000000..46cca2c --- /dev/null +++ b/tests/test_middleware.py @@ -0,0 +1,345 @@ +from unittest import mock + +import pytest +from django.core.urlresolvers import reverse_lazy +from django.test.client import Client + + +@pytest.mark.parametrize( + 'header, incoming_ip, expected', + ( + ('HTTP_X_FORWARDED_FOR', '74.125.224.72', 200), + ('X_FORWARDED_FOR', '74.125.224.72', 200), + ('HTTP_CLIENT_IP', '74.125.224.72', 200), + ('HTTP_X_REAL_IP', '74.125.224.72', 200), + ('HTTP_X_FORWARDED', '74.125.224.72', 200), + ('HTTP_X_CLUSTER_CLIENT_IP', '74.125.224.72', 200), + ('HTTP_FORWARDED_FOR', '74.125.224.72', 200), + ('HTTP_FORWARDED', '74.125.224.72', 200), + ('HTTP_VIA', '74.125.224.72', 200), + ('REMOTE_ADDR', '74.125.224.72', 200) + ), + ids=[ + 'HTTP_X_FORWARDED_FOR allow', + 'X_FORWARDED_FOR allow', + 'HTTP_CLIENT_IP allow', + 'HTTP_X_REAL_IP allow', + 'HTTP_X_FORWARDED allow', + 'HTTP_X_CLUSTER_CLIENT_IP allow', + 'HTTP_FORWARDED_FOR allow', + 'HTTP_FORWARDED allow', + 'HTTP_VIA allow', + 'REMOTE_ADDR allow' + ] +) +def test_admin_no_restriction(header, incoming_ip, expected, settings): + settings.RESTRICT_ADMIN = False + admin_url = reverse_lazy('admin:login') + client = Client() + response = client.get(admin_url, **{header: incoming_ip}) + assert response.status_code == expected + + +@pytest.mark.parametrize( + 'header, incoming_ip, expected', + ( + ('HTTP_X_FORWARDED_FOR', '74.125.224.72', 200), + ('X_FORWARDED_FOR', '74.125.224.72', 200), + ('HTTP_CLIENT_IP', '74.125.224.72', 200), + ('HTTP_X_REAL_IP', '74.125.224.72', 200), + ('HTTP_X_FORWARDED', '74.125.224.72', 200), + ('HTTP_X_CLUSTER_CLIENT_IP', '74.125.224.72', 200), + ('HTTP_FORWARDED_FOR', '74.125.224.72', 200), + ('HTTP_FORWARDED', '74.125.224.72', 200), + ('HTTP_VIA', '74.125.224.72', 200), + ('REMOTE_ADDR', '74.125.224.72', 200), + ('HTTP_X_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('X_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_CLIENT_IP', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_X_REAL_IP', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_X_FORWARDED', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_X_CLUSTER_CLIENT_IP', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_FORWARDED', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_VIA', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('REMOTE_ADDR', '0:0:0:0:0:ffff:4a7d:e048', 200) + ), + ids=[ + 'HTTP_X_FORWARDED_FOR allow', + 'X_FORWARDED_FOR allow', + 'HTTP_CLIENT_IP allow', + 'HTTP_X_REAL_IP allow', + 'HTTP_X_FORWARDED allow', + 'HTTP_X_CLUSTER_CLIENT_IP allow', + 'HTTP_FORWARDED_FOR allow', + 'HTTP_FORWARDED allow', + 'HTTP_VIA allow', + 'REMOTE_ADDR allow', + 'HTTP_X_FORWARDED_FOR ipv6 allow', + 'X_FORWARDED_FOR ipv6 allow', + 'HTTP_CLIENT_IP ipv6 allow', + 'HTTP_X_REAL_IP ipv6 allow', + 'HTTP_X_FORWARDED ipv6 allow', + 'HTTP_X_CLUSTER_CLIENT_IP ipv6 allow', + 'HTTP_FORWARDED_FOR ipv6 allow', + 'HTTP_FORWARDED ipv6 allow', + 'HTTP_VIA ipv6 allow', + 'REMOTE_ADDR ipv6 allow', + ] +) +def test_admin_restricted_allowed_ips(header, incoming_ip, expected, settings): + settings.RESTRICT_ADMIN = True + settings.ALLOWED_ADMIN_IPS = ['74.125.224.72', '0:0:0:0:0:ffff:4a7d:e048'] + admin_url = reverse_lazy('admin:login') + client = Client() + response = client.get(admin_url, **{header: incoming_ip}) + assert response.status_code == expected + + +@pytest.mark.parametrize( + 'header, incoming_ip, expected', + ( + ('HTTP_X_FORWARDED_FOR', '74.125.224.72', 404), + ('X_FORWARDED_FOR', '74.125.224.72', 404), + ('HTTP_CLIENT_IP', '74.125.224.72', 404), + ('HTTP_X_REAL_IP', '74.125.224.72', 404), + ('HTTP_X_FORWARDED', '74.125.224.72', 404), + ('HTTP_X_CLUSTER_CLIENT_IP', '74.125.224.72', 404), + ('HTTP_FORWARDED_FOR', '74.125.224.72', 404), + ('HTTP_FORWARDED', '74.125.224.72', 404), + ('HTTP_VIA', '74.125.224.72', 404), + ('REMOTE_ADDR', '74.125.224.72', 404), + ('HTTP_X_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('X_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_CLIENT_IP', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_X_REAL_IP', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_X_FORWARDED', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_X_CLUSTER_CLIENT_IP', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_FORWARDED', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_VIA', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('REMOTE_ADDR', '0:0:0:0:0:ffff:4a7d:e048', 404) + ), + ids=[ + 'HTTP_X_FORWARDED_FOR block', + 'X_FORWARDED_FOR block', + 'HTTP_CLIENT_IP block', + 'HTTP_X_REAL_IP block', + 'HTTP_X_FORWARDED block', + 'HTTP_X_CLUSTER_CLIENT_IP block', + 'HTTP_FORWARDED_FOR block', + 'HTTP_FORWARDED block', + 'HTTP_VIA block', + 'REMOTE_ADDR block', + 'HTTP_X_FORWARDED_FOR ipv6 block', + 'X_FORWARDED_FOR ipv6 block', + 'HTTP_CLIENT_IP ipv6 block', + 'HTTP_X_REAL_IP ipv6 block', + 'HTTP_X_FORWARDED ipv6 block', + 'HTTP_X_CLUSTER_CLIENT_IP ipv6 block', + 'HTTP_FORWARDED_FOR ipv6 block', + 'HTTP_FORWARDED ipv6 block', + 'HTTP_VIA ipv6 block', + 'REMOTE_ADDR ipv6 block' + ] +) +def test_admin_restricted_blocked_ips(header, incoming_ip, expected, settings): + settings.RESTRICT_ADMIN = True + settings.ALLOWED_ADMIN_IPS = ['8.8.8.9', '0:0:0:0:0:ffff:4a7d:e04b'] + admin_url = reverse_lazy('admin:login') + client = Client() + response = client.get(admin_url, **{header: incoming_ip}) + assert response.status_code == expected + + +@pytest.mark.parametrize( + 'header, incoming_ip, expected', + ( + ('HTTP_X_FORWARDED_FOR', '74.125.224.72', 200), + ('X_FORWARDED_FOR', '74.125.224.72', 200), + ('HTTP_CLIENT_IP', '74.125.224.72', 200), + ('HTTP_X_REAL_IP', '74.125.224.72', 200), + ('HTTP_X_FORWARDED', '74.125.224.72', 200), + ('HTTP_X_CLUSTER_CLIENT_IP', '74.125.224.72', 200), + ('HTTP_FORWARDED_FOR', '74.125.224.72', 200), + ('HTTP_FORWARDED', '74.125.224.72', 200), + ('HTTP_VIA', '74.125.224.72', 200), + ('REMOTE_ADDR', '74.125.224.72', 200), + ('HTTP_X_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('X_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_CLIENT_IP', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_X_REAL_IP', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_X_FORWARDED', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_X_CLUSTER_CLIENT_IP', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_FORWARDED', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('HTTP_VIA', '0:0:0:0:0:ffff:4a7d:e048', 200), + ('REMOTE_ADDR', '0:0:0:0:0:ffff:4a7d:e048', 200) + ), + ids=[ + 'HTTP_X_FORWARDED_FOR allow', + 'X_FORWARDED_FOR allow', + 'HTTP_CLIENT_IP allow', + 'HTTP_X_REAL_IP allow', + 'HTTP_X_FORWARDED allow', + 'HTTP_X_CLUSTER_CLIENT_IP allow', + 'HTTP_FORWARDED_FOR allow', + 'HTTP_FORWARDED allow', + 'HTTP_VIA allow', + 'REMOTE_ADDR allow', + 'HTTP_X_FORWARDED_FOR ipv6 allow', + 'X_FORWARDED_FOR ipv6 allow', + 'HTTP_CLIENT_IP ipv6 allow', + 'HTTP_X_REAL_IP ipv6 allow', + 'HTTP_X_FORWARDED ipv6 allow', + 'HTTP_X_CLUSTER_CLIENT_IP ipv6 allow', + 'HTTP_FORWARDED_FOR ipv6 allow', + 'HTTP_FORWARDED ipv6 allow', + 'HTTP_VIA ipv6 allow', + 'REMOTE_ADDR ipv6 allow', + ] +) +def test_admin_restricted_allowed_ip_ranges(header, incoming_ip, expected, + settings): + settings.RESTRICT_ADMIN = True + settings.ALLOWED_ADMIN_IP_RANGES = [ + '74.125.224.72/32', + '0:0:0:0:0:ffff:4a7d:e048/128' + ] + admin_url = reverse_lazy('admin:login') + client = Client() + response = client.get(admin_url, **{header: incoming_ip}) + assert response.status_code == expected + + +@pytest.mark.parametrize( + 'header, incoming_ip, expected', + ( + ('HTTP_X_FORWARDED_FOR', '74.125.224.72', 404), + ('X_FORWARDED_FOR', '74.125.224.72', 404), + ('HTTP_CLIENT_IP', '74.125.224.72', 404), + ('HTTP_X_REAL_IP', '74.125.224.72', 404), + ('HTTP_X_FORWARDED', '74.125.224.72', 404), + ('HTTP_X_CLUSTER_CLIENT_IP', '74.125.224.72', 404), + ('HTTP_FORWARDED_FOR', '74.125.224.72', 404), + ('HTTP_FORWARDED', '74.125.224.72', 404), + ('HTTP_VIA', '74.125.224.72', 404), + ('REMOTE_ADDR', '74.125.224.72', 404), + ('HTTP_X_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('X_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_CLIENT_IP', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_X_REAL_IP', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_X_FORWARDED', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_X_CLUSTER_CLIENT_IP', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_FORWARDED_FOR', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_FORWARDED', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('HTTP_VIA', '0:0:0:0:0:ffff:4a7d:e048', 404), + ('REMOTE_ADDR', '0:0:0:0:0:ffff:4a7d:e048', 404) + ), + ids=[ + 'HTTP_X_FORWARDED_FOR block', + 'X_FORWARDED_FOR block', + 'HTTP_CLIENT_IP block', + 'HTTP_X_REAL_IP block', + 'HTTP_X_FORWARDED block', + 'HTTP_X_CLUSTER_CLIENT_IP block', + 'HTTP_FORWARDED_FOR block', + 'HTTP_FORWARDED block', + 'HTTP_VIA block', + 'REMOTE_ADDR block', + 'HTTP_X_FORWARDED_FOR ipv6 block', + 'X_FORWARDED_FOR ipv6 block', + 'HTTP_CLIENT_IP ipv6 block', + 'HTTP_X_REAL_IP ipv6 block', + 'HTTP_X_FORWARDED ipv6 block', + 'HTTP_X_CLUSTER_CLIENT_IP ipv6 block', + 'HTTP_FORWARDED_FOR ipv6 block', + 'HTTP_FORWARDED ipv6 block', + 'HTTP_VIA ipv6 block', + 'REMOTE_ADDR ipv6 block', + ] +) +def test_admin_restricted_blocked_ip_ranges(header, incoming_ip, expected, + settings): + settings.RESTRICT_ADMIN = True + settings.ALLOWED_ADMIN_IPS = ['192.168.0.0/24', '0:0:0:0:0:ffff:4a7d:e04b'] + admin_url = reverse_lazy('admin:login') + client = Client() + response = client.get(admin_url, **{header: incoming_ip}) + assert response.status_code == expected + + +@mock.patch('admin_ip_restrictor.middleware.get_client_ip') +def test_client_ip_not_found(mocked_get_client_ip, settings): + settings.RESTRICT_ADMIN = True + mocked_get_client_ip.return_value = None, None + + admin_url = reverse_lazy('admin:login') + client = Client() + with pytest.raises(Exception) as e: + client.get(admin_url) + assert 'IP not found' in str(e.value) + + +@pytest.mark.parametrize( + 'header, incoming_ip', + ( + ('HTTP_X_FORWARDED_FOR', '127.0.0.1'), + ('HTTP_X_FORWARDED_FOR', 'fc00:'), + ), + ids=[ + 'Private IPV4', + 'Private IPV6' + ] +) +def test_client_ip_private(header, incoming_ip, settings): + settings.RESTRICT_ADMIN = True + admin_url = reverse_lazy('admin:login') + client = Client() + with pytest.raises(Exception) as e: + client.get(admin_url, **{header: incoming_ip}) + assert 'IP is private' in str(e.value) + + +@pytest.mark.parametrize( + 'envar, expected', + ( + ('127.0.0.1', ['127.0.0.1']), + ('127.0.0.1,127.0.0.2', ['127.0.0.1', '127.0.0.2']), + ), + ids=[ + 'single entry string', + 'comma separated multiple entry string', + ] +) +def test_list_envars_parsing(envar, expected): + from admin_ip_restrictor.middleware import AdminIPRestrictorMiddleware + assert AdminIPRestrictorMiddleware.parse_list_envars(envar) == expected + + +@pytest.mark.parametrize( + 'envar, expected', + ( + ('True', True), + ('true', True), + ('1', True), + (1, True), + ('bla', False), + ('foo', False), + ('0', False), + (0, False) + ), + ids=[ + 'True', + 'true', + '1', + '1 number', + 'false', + 'False', + '0', + '0 number' + ] +) +def test_bool_envars_parsing(envar, expected): + from admin_ip_restrictor.middleware import AdminIPRestrictorMiddleware + assert AdminIPRestrictorMiddleware.parse_bool_envars(envar) == expected diff --git a/tests/urls.py b/tests/urls.py new file mode 100644 index 0000000..d52422e --- /dev/null +++ b/tests/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url +from django.contrib import admin + + +urlpatterns = [ + url(r'^admin/', admin.site.urls), +] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..8946b4e --- /dev/null +++ b/tox.ini @@ -0,0 +1,28 @@ +[tox] +envlist = + py{34,35}-django19, + py{34,35}-django110, + py{34,35,36}-django111, + py{34,35,36}-django2 +skip_missing_interpreters = True + +[testenv] +commands = coverage run --parallel -m pytest {posargs} +extras = tests +deps = + -e. + django19: Django >= 1.9, < 1.10 + django110: Django >= 1.10, < 1.11 + django111: Django >= 1.11, < 1.12 + django2: Django >= 2.0, <2.1 + + +[testenv:coverage-report] +basepython = python3.5 +deps = coverage +skip_install = true +commands = + coverage combine + coverage report + +