From 70ead288965c44af307fb907255d88ba38bb71e9 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Fri, 16 Jun 2023 23:06:34 +0400 Subject: [PATCH 1/8] GH-32: Enhance the regex for matching country states --- src/django_forbid/skills/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/django_forbid/skills/__init__.py b/src/django_forbid/skills/__init__.py index de26a5c..7beb283 100644 --- a/src/django_forbid/skills/__init__.py +++ b/src/django_forbid/skills/__init__.py @@ -52,13 +52,13 @@ def permitted(cls, attribute_type): def grants(self, attribute_type): # Creates a regular expression in the following form: - # ^(?=PERMITTED_ATTRIBUTES)(?:(?!FORBIDDEN_ATTRIBUTES)\w)+$ - # where the list of forbidden and permitted attributes are - # filtered from the ATTRIBUTES setting by the "!" prefix. + # ^(?=PERMITTED_ATTRIBUTES)(?:(?!FORBIDDEN_ATTRIBUTES)\w+(?::\w+)?)$ + # where the list of forbidden and permitted attributes is determined + # by filtering the particular setting attributes by the "!" prefix. permit = r"|".join(filter(self.permitted, self.attributes)) forbid = r"|".join(map(self.normalize, filter(self.forbidden, self.attributes))) forbid = r"(?!" + forbid + r")" if forbid else "" - regexp = r"^(?=" + permit + r")(?:" + forbid + r"\w)+$" + regexp = r"^(?=" + permit + r")(?:" + forbid + r"\w+(?::\w+)?)$" # Regexp designed to match the permitted attributes. return re.match(regexp, attribute_type) From 29e9ca6d01ecd9b5fc5a9d0525c62cf63974d6d2 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Fri, 16 Jun 2023 23:10:48 +0400 Subject: [PATCH 2/8] GH-32: Implement the location checking by country states --- src/django_forbid/skills/forbid_location.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/django_forbid/skills/forbid_location.py b/src/django_forbid/skills/forbid_location.py index 1edfe05..fa049b4 100644 --- a/src/django_forbid/skills/forbid_location.py +++ b/src/django_forbid/skills/forbid_location.py @@ -30,8 +30,12 @@ def __call__(self, request): countries = Settings.get("COUNTRIES", []) territories = Settings.get("TERRITORIES", []) + country_state_code = city.get("region") + country_identifier = city.get("country_code") + if country_state_code: + country_identifier += ":%s" % country_state_code granted = all([ - Access(countries).grants(city.get("country_code")), + Access(countries).grants(country_identifier), Access(territories).grants(city.get("continent_code")), ]) except (AddressNotFoundError, Exception): From 12e377cb649e4b7baad104432c865599fe3ee59a Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Fri, 16 Jun 2023 23:11:56 +0400 Subject: [PATCH 3/8] GH-32: Add a new US city in the IP list for new tests --- tests/__init__.py | 2 ++ tests/test_primary_middleware.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 4e8e031..afcfa33 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,6 +7,7 @@ class IP: ip_local2 = "127.0.0.1" ip_london = "212.102.63.59" ip_zurich = "146.70.99.178" + ip_dallas = "198.96.95.234" ip_cobain = "104.129.57.189" locals = [ @@ -20,6 +21,7 @@ class IP: ip_london, ip_zurich, ip_cobain, + ip_dallas, ] diff --git a/tests/test_primary_middleware.py b/tests/test_primary_middleware.py index ac3646a..f93d751 100644 --- a/tests/test_primary_middleware.py +++ b/tests/test_primary_middleware.py @@ -51,7 +51,7 @@ def test_should_forbid_users_when_country_in_territories_blacklist(get_response) """Should forbid access to users from territories in blacklist.""" for ip_address in IP.all: request.META["HTTP_X_FORWARDED_FOR"] = ip_address - if ip_address in [*IP.locals, IP.ip_cobain]: + if ip_address in [*IP.locals, IP.ip_cobain, IP.ip_dallas]: assert forbids(get_response, request) continue assert not forbids(get_response, request) From ac9eca9a9c6dacca1e3792ca37ed35eab8aee0ad Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Fri, 16 Jun 2023 23:15:13 +0400 Subject: [PATCH 4/8] GH-32: Add tests specifying certain country states --- tests/test_location_middleware.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_location_middleware.py b/tests/test_location_middleware.py index 1a67499..158e975 100644 --- a/tests/test_location_middleware.py +++ b/tests/test_location_middleware.py @@ -33,6 +33,26 @@ def test_should_forbid_all_when_production_mode(get_response): assert forbids(get_response, ip_address) +@override_settings(DJANGO_FORBID={"COUNTRIES": ["US:WA"]}) +def test_should_allow_users_only_from_washington(get_response): + """Access is granted from Washington.""" + for ip_address in IP.all: + if ip_address != IP.ip_cobain: + assert forbids(get_response, ip_address) + continue + assert not forbids(get_response, ip_address) + + +@override_settings(DJANGO_FORBID={"COUNTRIES": ["!US:TX"]}) +def test_should_forbid_users_only_from_texas(get_response): + """Access is forbidden from Texas.""" + for ip_address in IP.all: + if ip_address in [*IP.locals, IP.ip_dallas]: + assert forbids(get_response, ip_address) + continue + assert not forbids(get_response, ip_address) + + @override_settings(DJANGO_FORBID={"COUNTRIES": ["GB"]}) def test_should_allow_country_when_country_in_countries_whitelist_otherwise_forbid(get_response): """Access is granted from GB when GB is in the counties' whitelist.""" From 33ebe2f16dd9126ac8cda8bb8e4bdd387e196780 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Fri, 16 Jun 2023 23:15:44 +0400 Subject: [PATCH 5/8] Upgrade the version to `0.1.5` --- src/django_forbid/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_forbid/__init__.py b/src/django_forbid/__init__.py index bbab024..1276d02 100644 --- a/src/django_forbid/__init__.py +++ b/src/django_forbid/__init__.py @@ -1 +1 @@ -__version__ = "0.1.4" +__version__ = "0.1.5" From 706163bf1e9b9aec3dd1b3b4bfd92c2f68f6abbd Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Sat, 17 Jun 2023 19:56:27 +0400 Subject: [PATCH 6/8] GH-32: Add a case with mixed settings --- tests/test_location_middleware.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_location_middleware.py b/tests/test_location_middleware.py index 158e975..cae7790 100644 --- a/tests/test_location_middleware.py +++ b/tests/test_location_middleware.py @@ -53,6 +53,16 @@ def test_should_forbid_users_only_from_texas(get_response): assert not forbids(get_response, ip_address) +@override_settings(DJANGO_FORBID={"COUNTRIES": ["!US:TX", "!US:IL", "GB"], "TERRITORIES": ["EU"]}) +def test_should_forbid_users_only_from_texas_and_illinois(get_response): + """Access is forbidden from Texas and Illinois.""" + for ip_address in IP.all: + if ip_address in [*IP.locals, IP.ip_dallas]: + assert forbids(get_response, ip_address) + continue + assert not forbids(get_response, ip_address) + + @override_settings(DJANGO_FORBID={"COUNTRIES": ["GB"]}) def test_should_allow_country_when_country_in_countries_whitelist_otherwise_forbid(get_response): """Access is granted from GB when GB is in the counties' whitelist.""" From a642e7a14fe7cd7faf9ff0d4a57126f479f91b81 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Sat, 17 Jun 2023 19:58:22 +0400 Subject: [PATCH 7/8] GH-32: Fix the setting mixing case --- src/django_forbid/skills/__init__.py | 44 +++++++++++++++++++++ src/django_forbid/skills/forbid_location.py | 10 +++++ 2 files changed, 54 insertions(+) diff --git a/src/django_forbid/skills/__init__.py b/src/django_forbid/skills/__init__.py index 7beb283..418bda6 100644 --- a/src/django_forbid/skills/__init__.py +++ b/src/django_forbid/skills/__init__.py @@ -50,15 +50,59 @@ def permitted(cls, attribute_type): """Checks if the type is permitted.""" return not cls.forbidden(attribute_type) + @staticmethod + def getattr(attribute_type): + """Checks if the type has an attribute and parses it.""" + matching_attribute = re.match(r"^!(\w+):\w+$", attribute_type) + return matching_attribute.group(1) if matching_attribute else None + def grants(self, attribute_type): # Creates a regular expression in the following form: # ^(?=PERMITTED_ATTRIBUTES)(?:(?!FORBIDDEN_ATTRIBUTES)\w+(?::\w+)?)$ # where the list of forbidden and permitted attributes is determined # by filtering the particular setting attributes by the "!" prefix. permit = r"|".join(filter(self.permitted, self.attributes)) + permit = r"\w+" if any(filter(self.getattr, self.attributes)) else permit forbid = r"|".join(map(self.normalize, filter(self.forbidden, self.attributes))) forbid = r"(?!" + forbid + r")" if forbid else "" regexp = r"^(?=" + permit + r")(?:" + forbid + r"\w+(?::\w+)?)$" # Regexp designed to match the permitted attributes. return re.match(regexp, attribute_type) + + +continents_codes = { + 'AF': ['AS'], 'AL': ['EU'], 'AQ': ['AN'], 'DZ': ['AF'], 'AS': ['OC'], 'AD': ['EU'], 'AO': ['AF'], 'AG': ['NA'], + 'AZ': ['EU', 'AS'], 'AR': ['SA'], 'AU': ['OC'], 'AT': ['EU'], 'BS': ['NA'], 'BH': ['AS'], 'BD': ['AS'], + 'AM': ['EU', 'AS'], 'BB': ['NA'], 'BE': ['EU'], 'BM': ['NA'], 'BT': ['AS'], 'BO': ['SA'], 'BA': ['EU'], + 'BW': ['AF'], 'BV': ['AN'], 'BR': ['SA'], 'BZ': ['NA'], 'IO': ['AS'], 'SB': ['OC'], 'VG': ['NA'], 'BN': ['AS'], + 'BG': ['EU'], 'MM': ['AS'], 'BI': ['AF'], 'BY': ['EU'], 'KH': ['AS'], 'CM': ['AF'], 'CA': ['NA'], 'CV': ['AF'], + 'KY': ['NA'], 'CF': ['AF'], 'LK': ['AS'], 'TD': ['AF'], 'CL': ['SA'], 'CN': ['AS'], 'TW': ['AS'], 'CX': ['AS'], + 'CC': ['AS'], 'CO': ['SA'], 'KM': ['AF'], 'YT': ['AF'], 'CG': ['AF'], 'CD': ['AF'], 'CK': ['OC'], 'CR': ['NA'], + 'HR': ['EU'], 'CU': ['NA'], 'CY': ['EU', 'AS'], 'CZ': ['EU'], 'BJ': ['AF'], 'DK': ['EU'], 'DM': ['NA'], + 'DO': ['NA'], 'EC': ['SA'], 'SV': ['NA'], 'GQ': ['AF'], 'ET': ['AF'], 'ER': ['AF'], 'EE': ['EU'], 'FO': ['EU'], + 'FK': ['SA'], 'GS': ['AN'], 'FJ': ['OC'], 'FI': ['EU'], 'AX': ['EU'], 'FR': ['EU'], 'GF': ['SA'], 'PF': ['OC'], + 'TF': ['AN'], 'DJ': ['AF'], 'GA': ['AF'], 'GE': ['EU', 'AS'], 'GM': ['AF'], 'PS': ['AS'], 'DE': ['EU'], + 'GH': ['AF'], 'GI': ['EU'], 'KI': ['OC'], 'GR': ['EU'], 'GL': ['NA'], 'GD': ['NA'], 'GP': ['NA'], 'GU': ['OC'], + 'GT': ['NA'], 'GN': ['AF'], 'GY': ['SA'], 'HT': ['NA'], 'HM': ['AN'], 'VA': ['EU'], 'HN': ['NA'], 'HK': ['AS'], + 'HU': ['EU'], 'IS': ['EU'], 'IN': ['AS'], 'ID': ['AS'], 'IR': ['AS'], 'IQ': ['AS'], 'IE': ['EU'], 'IL': ['AS'], + 'IT': ['EU'], 'CI': ['AF'], 'JM': ['NA'], 'JP': ['AS'], 'KZ': ['EU', 'AS'], 'JO': ['AS'], 'KE': ['AF'], + 'KP': ['AS'], 'KR': ['AS'], 'KW': ['AS'], 'KG': ['AS'], 'LA': ['AS'], 'LB': ['AS'], 'LS': ['AF'], 'LV': ['EU'], + 'LR': ['AF'], 'LY': ['AF'], 'LI': ['EU'], 'LT': ['EU'], 'LU': ['EU'], 'MO': ['AS'], 'MG': ['AF'], 'MW': ['AF'], + 'MY': ['AS'], 'MV': ['AS'], 'ML': ['AF'], 'MT': ['EU'], 'MQ': ['NA'], 'MR': ['AF'], 'MU': ['AF'], 'MX': ['NA'], + 'MC': ['EU'], 'MN': ['AS'], 'MD': ['EU'], 'ME': ['EU'], 'MS': ['NA'], 'MA': ['AF'], 'MZ': ['AF'], 'OM': ['AS'], + 'NA': ['AF'], 'NR': ['OC'], 'NP': ['AS'], 'NL': ['EU'], 'AN': ['NA'], 'CW': ['NA'], 'AW': ['NA'], 'SX': ['NA'], + 'BQ': ['NA'], 'NC': ['OC'], 'VU': ['OC'], 'NZ': ['OC'], 'NI': ['NA'], 'NE': ['AF'], 'NG': ['AF'], 'NU': ['OC'], + 'NF': ['OC'], 'NO': ['EU'], 'MP': ['OC'], 'UM': ['OC', 'NA'], 'FM': ['OC'], 'MH': ['OC'], 'PW': ['OC'], + 'PK': ['AS'], 'PA': ['NA'], 'PG': ['OC'], 'PY': ['SA'], 'PE': ['SA'], 'PH': ['AS'], 'PN': ['OC'], 'PL': ['EU'], + 'PT': ['EU'], 'GW': ['AF'], 'TL': ['AS'], 'PR': ['NA'], 'QA': ['AS'], 'RE': ['AF'], 'RO': ['EU'], 'RW': ['AF'], + 'RU': ['EU', 'AS'], 'BL': ['NA'], 'SH': ['AF'], 'KN': ['NA'], 'AI': ['NA'], 'LC': ['NA'], 'MF': ['NA'], + 'PM': ['NA'], 'VC': ['NA'], 'SM': ['EU'], 'ST': ['AF'], 'SA': ['AS'], 'SN': ['AF'], 'RS': ['EU'], 'SC': ['AF'], + 'SL': ['AF'], 'SG': ['AS'], 'SK': ['EU'], 'VN': ['AS'], 'SI': ['EU'], 'SO': ['AF'], 'ZA': ['AF'], 'ZW': ['AF'], + 'ES': ['EU'], 'SS': ['AF'], 'EH': ['AF'], 'SD': ['AF'], 'SR': ['SA'], 'SJ': ['EU'], 'SZ': ['AF'], 'SE': ['EU'], + 'CH': ['EU'], 'SY': ['AS'], 'TJ': ['AS'], 'TH': ['AS'], 'TG': ['AF'], 'TK': ['OC'], 'TO': ['OC'], 'TT': ['NA'], + 'AE': ['AS'], 'TN': ['AF'], 'TR': ['EU', 'AS'], 'TM': ['AS'], 'TC': ['NA'], 'TV': ['OC'], 'UG': ['AF'], + 'UA': ['EU'], 'MK': ['EU'], 'EG': ['AF'], 'GB': ['EU'], 'GG': ['EU'], 'JE': ['EU'], 'IM': ['EU'], 'TZ': ['AF'], + 'US': ['NA'], 'VI': ['NA'], 'BF': ['AF'], 'UY': ['SA'], 'UZ': ['AS'], 'VE': ['SA'], 'WF': ['OC'], 'WS': ['OC'], + 'YE': ['AS'], 'ZM': ['AF'], 'XX': ['OC'], 'XE': ['AS'], 'XD': ['AS'], 'XS': ['AS'] +} diff --git a/src/django_forbid/skills/forbid_location.py b/src/django_forbid/skills/forbid_location.py index fa049b4..4fdb413 100644 --- a/src/django_forbid/skills/forbid_location.py +++ b/src/django_forbid/skills/forbid_location.py @@ -1,3 +1,5 @@ +import itertools + from django.conf import settings from django.contrib.gis.geoip2 import GeoIP2 from django.http import HttpResponseForbidden @@ -6,6 +8,7 @@ from . import Access from . import Settings +from . import continents_codes class ForbidLocationMiddleware: @@ -32,6 +35,13 @@ def __call__(self, request): territories = Settings.get("TERRITORIES", []) country_state_code = city.get("region") country_identifier = city.get("country_code") + + if territories: + # Adds the continent codes of the countries that are forbidden partially. + territories = list({*territories, *itertools.chain.from_iterable( + map(continents_codes.__getitem__, filter(bool, map(Access.getattr, countries))) + )}) + if country_state_code: country_identifier += ":%s" % country_state_code granted = all([ From a3dbd2174d42d90e0d66f236e691e97a5c56d682 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Sat, 17 Jun 2023 20:26:39 +0400 Subject: [PATCH 8/8] GH-32: Add explanation for specifying location by states --- docs/integration/settings/index.md | 2 +- docs/integration/settings/variables.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/integration/settings/index.md b/docs/integration/settings/index.md index 83a25ae..2be8c30 100644 --- a/docs/integration/settings/index.md +++ b/docs/integration/settings/index.md @@ -40,5 +40,5 @@ DJANGO_FORBID = { ``` In this example, the Django Forbid will permit access to users using the listed devices and forbid entry to users -worldwide except for the US, UK, and EU countries. It will also forbid access to the users who use VPN to lie about +worldwide except for the US, GB, and EU countries. It will also forbid access to the users who use VPN to lie about their geolocation. The settings also define the URLs to redirect to when access is forbidden. diff --git a/docs/integration/settings/variables.md b/docs/integration/settings/variables.md index 05aa6ae..5abd71d 100644 --- a/docs/integration/settings/variables.md +++ b/docs/integration/settings/variables.md @@ -43,6 +43,12 @@ The list of countries to permit or forbid access to. The list accepts country co the ones starting with the `!` prefix are forbidden. The list of all codes can be found [here](https://www.iban.com/country-codes). +Also, you can permit or forbid access to a certain state of the country by specifying the state code after the country +code is joined with a `:` symbol. For example, `!US:TX` will forbid access to Texas by permitting the rest of the +mentioned country states. With the same principle, the `US:CA` will permit access to California only. The list of all US +state codes can be found [here](https://wikipedia.org/wiki/List_of_states_and_territories_of_the_United_States). This +declaration method can be used with all countries having states/districts. + ## Territories - Key: `TERRITORIES`