Skip to content

Commit 8ba44be

Browse files
Improve the settings definitions (GH-13)
2 parents 139ea93 + 53a96bc commit 8ba44be

File tree

10 files changed

+125
-107
lines changed

10 files changed

+125
-107
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea
22
.tox
33
.pytest_cache
4-
*.egg-info
4+
*.egg-info
5+
__pycache__

README.md

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
Django app for forbidding access to some countries.
44

55
[![PyPI](https://img.shields.io/pypi/v/django-forbid.svg)](https://pypi.org/project/django-forbid/)
6-
[![Django](https://img.shields.io/badge/django-%3E%3D2.1-blue.svg)](https://pypi.org/project/django-forbid/)
7-
[![Python](https://img.shields.io/pypi/pyversions/django-forbid.svg)](https://pypi.org/project/django-forbid/)
6+
[![Python](https://img.shields.io/pypi/pyversions/django-forbid.svg?logoColor=white)](https://pypi.org/project/django-forbid/)
7+
[![Django](https://img.shields.io/pypi/djversions/django-forbid.svg?color=0C4B33&label=django)](https://pypi.org/project/django-forbid/)
88
[![License](https://img.shields.io/pypi/l/django-forbid.svg)](https://github.com/pysnippet/django-forbid/blob/master/LICENSE)
99
[![Tests](https://github.com/pysnippet/django-forbid/actions/workflows/tests.yml/badge.svg)](https://github.com/pysnippet/django-forbid/actions/workflows/tests.yml)
1010

@@ -42,51 +42,40 @@ configuration.
4242
## Usage
4343

4444
After connecting the Django Forbid to your project, you can define the set of desired zones to be forbidden or allowed.
45-
And there are four setting variables for describing any of your specific needs:
46-
47-
- `WHITELIST_COUNTRIES` and `WHITELIST_TERRITORIES` - Correspondingly, the list of countries and territories that are
48-
allowed to access the site.
49-
- `FORBIDDEN_COUNTRIES` and `FORBIDDEN_TERRITORIES` - Correspondingly, the list of countries and territories that are
50-
forbidden to access the site.
51-
52-
Forbidden countries and territories have a higher priority than allowed ones. If a country or territory is in both
53-
lists, then the user will be forbidden. And if the user is not allowed to access the resource, it will be redirected to
54-
the `FORBIDDEN_URL` page if the variable is set in your Django project's settings.
55-
56-
```python
57-
# Only US, GB, and EU countries are allowed to access the site.
58-
WHITELIST_COUNTRIES = ['US', 'GB']
59-
WHITELIST_TERRITORIES = ['EU']
60-
```
61-
62-
Needs can be different, so you can use any combination of these variables to describe your special needs.
45+
All you need is to set the `DJANGO_FORBID` variable in your project's settings. It should be a dictionary with the
46+
following keys:
47+
48+
- `COUNTRIES` - list of countries to permit or forbid access to
49+
- `TERRITORIES` - list of territories to permit or forbid access to
50+
- `OPTIONS` - a dictionary for additional settings
51+
- `ACTION` - whether to `PERMIT` or `FORBID` access to the listed zones (default is `FORBID`)
52+
- `PERIOD` - time in seconds to check for access again, 0 means on each request
53+
- `VPN` - use VPN detection and forbid access to VPN users
54+
- `URL` - set of URLs to redirect to when the user is located in a forbidden country or using a VPN
55+
- `FORBIDDEN_LOC` - the URL to redirect to when the user is located in a forbidden country
56+
- `FORBIDDEN_VPN` - the URL to redirect to when the user is using a VPN
6357

6458
```python
65-
# Forbid access for African countries and Russia, Belarus, and North Korea.
66-
FORBIDDEN_COUNTRIES = ['RU', 'BY', 'KP']
67-
FORBIDDEN_TERRITORIES = ['AF']
59+
DJANGO_FORBID = {
60+
'COUNTRIES': ['US', 'GB'],
61+
'TERRITORIES': ['EU'],
62+
'OPTIONS': {
63+
'ACTION': 'PERMIT',
64+
'PERIOD': 300,
65+
'VPN': True,
66+
'URL': {
67+
'FORBIDDEN_LOC': 'forbidden_country',
68+
'FORBIDDEN_VPN': 'forbidden_network',
69+
},
70+
},
71+
}
6872
```
6973

7074
The available ISO 3166 alpha-2 country codes are listed in [here](https://www.iban.com/country-codes). And the available
7175
ISO continent codes are: `AF` - Africa, `AN` - Antarctica, `AS` - Asia, `EU` - Europe, `NA` - North America, `OC` -
7276
Oceania and `SA` - South America.
7377

74-
### Check access on timeout
75-
76-
Without additional configuration, the middleware will check the user's access on every request. This can slow down the
77-
site. To avoid this, you can use the `FORBID_TIMEOUT` variable to set the cache timeout in seconds. When the timeout
78-
expires, the middleware will check the user's access again.
79-
80-
```python
81-
# Check the user's access every 10 minutes.
82-
FORBID_TIMEOUT = 60 * 10
83-
```
84-
85-
### Detect usage of a VPN
86-
87-
If you want to detect the usage of a VPN, you can use the `FORBID_VPN` variable. When this variable is set to `True`,
88-
the middleware will check if the user's timezone matches the timezone the IP address belongs to. If the timezones do not
89-
match, the user will be considered in the usage of a VPN and forbidden to access the site.
78+
_None of the settings are required. If you don't specify any settings, the middleware will not do anything._
9079

9180
## Contribute
9281

setup.cfg

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ license = MIT
1616
license_file = LICENSE
1717
platforms = unix, linux, osx, win32
1818
classifiers =
19-
Framework :: Django
2019
Operating System :: OS Independent
20+
Framework :: Django
21+
Framework :: Django :: 2.1
22+
Framework :: Django :: 2.2
23+
Framework :: Django :: 3.1
24+
Framework :: Django :: 3.2
25+
Framework :: Django :: 4.0
26+
Framework :: Django :: 4.1
27+
Programming Language :: Python
2128
Programming Language :: Python :: 3
2229
Programming Language :: Python :: 3.6
2330
Programming Language :: Python :: 3.7

src/django_forbid/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.4"
1+
__version__ = "0.0.5"

src/django_forbid/access.py

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from django.contrib.gis.geoip2 import GeoIP2
33
from geoip2.errors import AddressNotFoundError
44

5+
from .config import Settings
6+
57

68
class Rule:
79
# Key in the geoip2 city object.
@@ -26,22 +28,22 @@ class ContinentRule(Rule):
2628

2729

2830
class Access:
29-
# Variables in the settings module.
30-
# Subclasses should override this.
31-
countries = None
32-
territories = None
31+
countries = "COUNTRIES"
32+
territories = "TERRITORIES"
3333

3434
# Hold the instance of GeoIP2.
3535
geoip = GeoIP2()
3636

3737
def __init__(self):
3838
self.rules = []
3939

40-
for country in getattr(settings, self.countries, []):
41-
self.rules.append(CountryRule(country.upper()))
40+
if Settings.has(self.countries):
41+
for country in Settings.get(self.countries):
42+
self.rules.append(CountryRule(country.upper()))
4243

43-
for territory in getattr(settings, self.territories, []):
44-
self.rules.append(ContinentRule(territory.upper()))
44+
if Settings.has(self.territories):
45+
for territory in Settings.get(self.territories):
46+
self.rules.append(ContinentRule(territory.upper()))
4547

4648
def accessible(self, city):
4749
"""Checks if the IP address is in the white zone."""
@@ -53,23 +55,28 @@ def grants(self, city):
5355

5456

5557
class PermitAccess(Access):
56-
countries = "WHITELIST_COUNTRIES"
57-
territories = "WHITELIST_TERRITORIES"
58-
5958
def grants(self, city):
6059
"""Checks if the IP address is permitted."""
6160
return not self.rules or self.accessible(city)
6261

6362

6463
class ForbidAccess(Access):
65-
countries = "FORBIDDEN_COUNTRIES"
66-
territories = "FORBIDDEN_TERRITORIES"
67-
6864
def grants(self, city):
6965
"""Checks if the IP address is forbidden."""
7066
return not self.rules or not self.accessible(city)
7167

7268

69+
class Factory:
70+
"""Creates an instance of the Access class."""
71+
72+
FORBID = ForbidAccess
73+
PERMIT = PermitAccess
74+
75+
@classmethod
76+
def create_access(cls, action):
77+
return getattr(cls, action)()
78+
79+
7380
def grants_access(request, ip_address):
7481
"""Checks if the IP address is in the white zone."""
7582
try:
@@ -82,18 +89,15 @@ def grants_access(request, ip_address):
8289
timezone = city.get("time_zone")
8390
request.session["tz"] = timezone
8491

85-
# First, checks if the IP address is not
86-
# forbidden. If it is, False is returned
87-
# otherwise, checks if the IP address is
88-
# permitted.
89-
if ForbidAccess().grants(city):
90-
return PermitAccess().grants(city)
91-
return False
92+
# Creates an instance of the Access class
93+
# and checks if the IP address is granted.
94+
action = Settings.get("OPTIONS.ACTION", "FORBID")
95+
return Factory.create_access(action).grants(city)
9296
except (AddressNotFoundError, Exception):
9397
# This happens when the IP address is not
9498
# in the GeoIP2 database. Usually, this
9599
# happens when the IP address is a local.
96100
return not any([
97-
ForbidAccess().rules,
98-
PermitAccess().rules,
101+
Settings.has(Access.countries),
102+
Settings.has(Access.territories),
99103
]) or getattr(settings, "DEBUG", False)

src/django_forbid/config.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.conf import settings
2+
3+
4+
class Settings:
5+
"""A helper class to access settings in a more convenient way."""
6+
7+
@classmethod
8+
def _get(cls, item):
9+
result = getattr(settings, "DJANGO_FORBID", {})
10+
for attr in item.split("."):
11+
result = result[attr]
12+
return result
13+
14+
@classmethod
15+
def has(cls, item):
16+
try:
17+
cls._get(item)
18+
return True
19+
except KeyError:
20+
return False
21+
22+
@classmethod
23+
def get(cls, item, default=None):
24+
try:
25+
return cls._get(item)
26+
except KeyError:
27+
return default

src/django_forbid/detect.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import json
22
import re
33

4-
from django.conf import settings
54
from django.http import HttpResponse
65
from django.http import HttpResponseForbidden
76
from django.shortcuts import redirect
87
from django.shortcuts import render
98

9+
from .config import Settings
10+
1011

1112
def detect_vpn(get_response, request):
1213
response_attributes = ("content", "charset", "status", "reason")
@@ -19,8 +20,8 @@ def erase_response_attributes():
1920
# The session key is checked to avoid
2021
# redirect loops in development mode.
2122
not request.session.has_key("tz"),
22-
# Checks if FORBID_VPN is False or not set.
23-
not getattr(settings, "FORBID_VPN", False),
23+
# Checks if VPN is False or not set.
24+
not Settings.get("OPTIONS.VPN", False),
2425
# Checks if the request is an AJAX request.
2526
not re.search(
2627
r"\w+\/(?:html|xhtml\+xml|xml)",
@@ -34,8 +35,9 @@ def erase_response_attributes():
3435
# one determined by GeoIP API. If so, VPN is used.
3536
if request.POST.get("timezone", "N/A") != request.session.get("tz"):
3637
erase_response_attributes()
37-
if hasattr(settings, "FORBIDDEN_URL"):
38-
return redirect(settings.FORBIDDEN_URL)
38+
# Redirects to the FORBIDDEN_VPN URL if set.
39+
if Settings.has("OPTIONS.URL.FORBIDDEN_VPN"):
40+
return redirect(Settings.get("OPTIONS.URL.FORBIDDEN_VPN"))
3941
return HttpResponseForbidden()
4042

4143
# Restores the response from the session.
@@ -48,7 +50,7 @@ def erase_response_attributes():
4850
# Gets the response and saves attributes in the session to restore it later.
4951
response = get_response(request)
5052
if hasattr(response, "headers"):
51-
# In older versions of Django, HttpResponse does not have headers attribute.
53+
# In older versions of Django, HttpResponse does not have headers.
5254
request.session["headers"] = json.dumps(dict(response.headers))
5355
request.session["content"] = response.content.decode(response.charset)
5456
request.session["charset"] = response.charset

src/django_forbid/middleware.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from datetime import datetime
22

3-
from django.conf import settings
43
from django.http import HttpResponseForbidden
54
from django.shortcuts import redirect
65
from django.utils.timezone import utc
76

87
from .access import grants_access
8+
from .config import Settings
99
from .detect import detect_vpn
1010

1111

@@ -19,12 +19,12 @@ def __call__(self, request):
1919
address = request.META.get("REMOTE_ADDR")
2020
address = request.META.get("HTTP_X_FORWARDED_FOR", address)
2121

22-
# Checks if the timeout variable is set and the user has been granted access.
23-
if hasattr(settings, "FORBID_TIMEOUT") and request.session.has_key("ACCESS"):
22+
# Checks if the PERIOD attr is set and the user has been granted access.
23+
if Settings.has("OPTIONS.PERIOD") and request.session.has_key("ACCESS"):
2424
acss = datetime.utcnow().replace(tzinfo=utc).timestamp()
2525

2626
# Checks if access is not timed out yet.
27-
if acss - request.session.get("ACCESS") < settings.FORBID_TIMEOUT:
27+
if acss - request.session.get("ACCESS") < Settings.get("OPTIONS.PERIOD"):
2828
return detect_vpn(self.get_response, request)
2929

3030
# Checks if access is granted when timeout is reached.
@@ -33,8 +33,8 @@ def __call__(self, request):
3333
request.session["ACCESS"] = acss.timestamp()
3434
return detect_vpn(self.get_response, request)
3535

36-
# Redirects to forbidden page if URL is set.
37-
if hasattr(settings, "FORBIDDEN_URL"):
38-
return redirect(settings.FORBIDDEN_URL)
36+
# Redirects to the FORBIDDEN_LOC URL if set.
37+
if Settings.has("OPTIONS.URL.FORBIDDEN_LOC"):
38+
return redirect(Settings.get("OPTIONS.URL.FORBIDDEN_LOC"))
3939

4040
return HttpResponseForbidden()

0 commit comments

Comments
 (0)