diff --git a/config/settings/base.py b/config/settings/base.py index 0d79af2ba2..cfa3fbb1bf 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -104,6 +104,7 @@ def safe_key() -> str: "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "metadeploy.multitenancy.middleware.CurrentSiteMiddleware", + "metadeploy.multitenancy.iprestrict_middleware.IPRestrictMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", diff --git a/metadeploy/api/models.py b/metadeploy/api/models.py index 12e56cef1c..6026517451 100644 --- a/metadeploy/api/models.py +++ b/metadeploy/api/models.py @@ -1224,6 +1224,7 @@ class SiteProfile(TranslatableModel): show_metadeploy_wordmark = models.BooleanField(default=True) company_logo = models.ImageField(blank=True) favicon = models.ImageField(blank=True) + allowed_ip_addresses = models.JSONField(default=list, blank=True, null=True) @property def welcome_text_markdown(self): diff --git a/metadeploy/api/serializers.py b/metadeploy/api/serializers.py index 261a1488cc..d86069e3e4 100644 --- a/metadeploy/api/serializers.py +++ b/metadeploy/api/serializers.py @@ -734,6 +734,7 @@ class Meta: "show_metadeploy_wordmark", "company_logo", "favicon", + "allowed_ip_addresses", ) diff --git a/metadeploy/multitenancy/iprestrict_middleware.py b/metadeploy/multitenancy/iprestrict_middleware.py new file mode 100644 index 0000000000..15ffdc3d24 --- /dev/null +++ b/metadeploy/multitenancy/iprestrict_middleware.py @@ -0,0 +1,22 @@ +from metadeploy.api.models import SiteProfile +from . import current_site_id +from django.http import HttpResponseForbidden + +class IPRestrictMiddleware: + def getSiteProfile(self): + profile = SiteProfile.objects.filter(site=current_site_id()).first() + return profile + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + client_ip = request.META.get('REMOTE_ADDR', None) + profile = self.getSiteProfile() + + if hasattr(profile, "allowed_ip_addresses") and profile.allowed_ip_addresses: + if client_ip not in profile.allowed_ip_addresses: + return HttpResponseForbidden("You don't have permission to access this resource.") + + response = self.get_response(request) + return response \ No newline at end of file diff --git a/metadeploy/multitenancy/tests/test_iprestrict_middleware.py b/metadeploy/multitenancy/tests/test_iprestrict_middleware.py new file mode 100644 index 0000000000..9739f85378 --- /dev/null +++ b/metadeploy/multitenancy/tests/test_iprestrict_middleware.py @@ -0,0 +1,70 @@ +from metadeploy.multitenancy.iprestrict_middleware import IPRestrictMiddleware +from django.contrib.sites.models import Site +from metadeploy.api.models import SiteProfile +from django.test import RequestFactory, TestCase +from unittest.mock import patch + + +class IPRestrictionMiddlewaretest(TestCase): + def setUp(self): + self.factory = RequestFactory() + + @patch('metadeploy.multitenancy.iprestrict_middleware.IPRestrictMiddleware.getSiteProfile') + def test_ip_restrict_middleware_with_matching_allowed_client_ip(self, mock_site_profile_get): + request = self.factory.get('/test') + request.META["REMOTE_ADDR"] = "127.0.0.1" + + site = Site.objects.create(name="Test") + mock_site_profile = SiteProfile() + mock_site_profile.site = site + mock_site_profile.name = site.name + mock_site_profile.allowed_ip_addresses = '["127.0.0.1"]' + mock_site_profile_get.return_value = mock_site_profile + + response = IPRestrictMiddleware(lambda x: x)(request) + assert response == request + + + @patch('metadeploy.multitenancy.iprestrict_middleware.IPRestrictMiddleware.getSiteProfile') + def test_ip_restrict_middleware_without_matching_allowed_client_ip(self, mock_site_profile_get): + request = self.factory.get('/test') + request.META["REMOTE_ADDR"] = "127.0.0.2" + + site = Site.objects.create(name="Test") + mock_site_profile = SiteProfile() + mock_site_profile.site = site + mock_site_profile.name = site.name + mock_site_profile.allowed_ip_addresses = '["127.0.0.1"]' + mock_site_profile_get.return_value = mock_site_profile + + response = IPRestrictMiddleware(lambda x: x)(request) + assert response.status_code == 403 + + @patch('metadeploy.multitenancy.iprestrict_middleware.IPRestrictMiddleware.getSiteProfile') + def test_ip_restrict_middleware_without_allowed_list(self, mock_site_profile_get): + request = self.factory.get('/test') + request.META["REMOTE_ADDR"] = "127.0.0.1" + + site = Site.objects.create(name="Test") + mock_site_profile = SiteProfile() + mock_site_profile.site = site + mock_site_profile.name = site.name + mock_site_profile_get.return_value = mock_site_profile + + response = IPRestrictMiddleware(lambda x: x)(request) + assert response == request + + @patch('metadeploy.multitenancy.iprestrict_middleware.IPRestrictMiddleware.getSiteProfile') + def test_ip_restrict_middleware_with_allowed_list_none(self, mock_site_profile_get): + request = self.factory.get('/test') + request.META["REMOTE_ADDR"] = "127.0.0.1" + + site = Site.objects.create(name="Test") + mock_site_profile = SiteProfile() + mock_site_profile.site = site + mock_site_profile.name = site.name + mock_site_profile.allowed_ip_addresses = None + mock_site_profile_get.return_value = mock_site_profile + + response = IPRestrictMiddleware(lambda x: x)(request) + assert response == request \ No newline at end of file