-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement access control based on the user's device (GH-15)
- Loading branch information
Showing
6 changed files
with
193 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,14 +3,25 @@ name = django-forbid | |
version = attr: django_forbid.__version__ | ||
author = Artyom Vancyan | ||
author_email = [email protected] | ||
description = Django app for forbidding access to some countries | ||
description = Secure your Django app by controlling the access - grant or deny user access based on device and location, including VPN detection. | ||
long_description = file: README.md | ||
long_description_content_type = text/markdown | ||
url = https://github.com/pysnippet/django-forbid | ||
keywords = | ||
python | ||
django | ||
permit | ||
forbid | ||
access | ||
device | ||
secure | ||
country | ||
control | ||
security | ||
location | ||
territory | ||
vpn | ||
detection | ||
django-forbid | ||
license = MIT | ||
license_file = LICENSE | ||
|
@@ -39,6 +50,7 @@ packages = | |
install_requires = | ||
Django>=2.1 | ||
geoip2 | ||
device_detector | ||
include_package_data = yes | ||
python_requires = >=3.6 | ||
package_dir = | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
__version__ = "0.0.5" | ||
__version__ = "0.0.6" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import re | ||
|
||
from device_detector import DeviceDetector | ||
|
||
from .config import Settings | ||
|
||
|
||
def detect_device(http_ua): | ||
device_aliases = { | ||
"portable media player": "player", | ||
"smart display": "display", | ||
"smart speaker": "speaker", | ||
"feature phone": "phone", | ||
"car browser": "car", | ||
} | ||
|
||
device_detector = DeviceDetector(http_ua) | ||
device_detector = device_detector.parse() | ||
device = device_detector.device_type() | ||
return device_aliases.get(device, device) | ||
|
||
|
||
def normalize(device_type): | ||
"""Removes the "!" prefix from the device type.""" | ||
return device_type[1:] | ||
|
||
|
||
def forbidden(device_type): | ||
"""Checks if the device type is forbidden.""" | ||
return device_type.startswith("!") | ||
|
||
|
||
def permitted(device_type): | ||
"""Checks if the device type is permitted.""" | ||
return not forbidden(device_type) | ||
|
||
|
||
def device_forbidden(device_type): | ||
devices = Settings.get("DEVICES", []) | ||
|
||
# Permit all devices if the | ||
# DEVICES setting is empty. | ||
if not devices: | ||
return False | ||
|
||
# Creates a regular expression in the following form: | ||
# ^(?=PERMITTED_DEVICES)(?:(?!FORBIDDEN_DEVICES)\w)+$ | ||
# where the list of forbidden and permitted devices are | ||
# filtered from the DEVICES setting by the "!" prefix. | ||
permit = r"|".join(filter(permitted, devices)) | ||
forbid = r"|".join(map(normalize, filter(forbidden, devices))) | ||
forbid = r"(?!" + forbid + r")" if forbid else "" | ||
regexp = r"^(?=" + permit + r")(?:" + forbid + r"\w)+$" | ||
|
||
# Regexp designed to match the permitted devices. | ||
# So, this checks if the device is not permitted. | ||
return not re.match(regexp, device_type) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
from django.test import override_settings | ||
|
||
from django_forbid.device import detect_device | ||
from django_forbid.device import device_forbidden | ||
|
||
unknown_ua = "curl/7.47.0" | ||
peripheral_ua = "Mozilla/5.0 (Linux; Android 7.0; SHTRIH-SMARTPOS-F2 Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/51.0.2704.91 Mobile Safari/537.36" | ||
smartphone_ua = "SAMSUNG-GT-S3850/S3850CXKD1 SHP/VPP/R5 Dolfin/2.0 NexPlayer/3.0 SMM-MMS/1.2.0 profile/MIDP-2.1 configuration/CLDC-1.1 OPN-B" | ||
wearable_ua = "Mozilla/5.0 (Linux; Android 8.1.0; KidPhone4G Build/O11019; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36" | ||
phablet_ua = "Mozilla/5.0 (Linux; Android 6.0; GI-626 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36" | ||
desktop_ua = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.28) Gecko/20130316 Songbird/1.12.1 (20140112193149)" | ||
console_ua = "Mozilla/5.0 (Linux; Android 4.1.1; ARCHOS GAMEPAD Build/JRO03H) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19" | ||
display_ua = "Mozilla/5.0 (Linux; U; Android 4.0.4; fr-be; DA220HQL Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30" | ||
speaker_ua = "AlexaMediaPlayer/2.0.201528.0 (Linux;Android 5.1.1) ExoPlayerLib/1.5.9" | ||
camera_ua = "Mozilla/5.0 (Linux; U; Android 2.3.3; ja-jp; COOLPIX S800c Build/CP01_WW) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1" | ||
tablet_ua = "Mozilla/5.0 (iPad3,6; iPad; U; CPU OS 7_1 like Mac OS X; en_US) com.google.GooglePlus/33839 (KHTML, like Gecko) Mobile/P103AP (gzip)" | ||
player_ua = "Mozilla/5.0 (iPod; U; CPU iPhone OS 4_2_1 like Mac OS X; ja-jp) AppleWebKit/533.17.9 (KHTML, like Gecko) Mobile/8C148" | ||
phone_ua = "lephone K10/Dorado WAP-Browser/1.0.0" | ||
car_ua = "Mozilla/5.0 (Linux; Android 4.4.2; CarPad-II-P Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Safari/537.36" | ||
tv_ua = "Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.34 Safari/537.36 WebAppManager" | ||
|
||
devices = ( | ||
peripheral_ua, smartphone_ua, wearable_ua, phablet_ua, desktop_ua, console_ua, | ||
display_ua, speaker_ua, camera_ua, tablet_ua, player_ua, phone_ua, car_ua, tv_ua, | ||
) | ||
|
||
|
||
@override_settings(DJANGO_FORBID={"DEVICES": []}) | ||
def test_access_with_empty_list_of_devices(): | ||
"""Should allow access to all devices if the list is empty, even if the user agent is unknown.""" | ||
for device_ua in devices + (unknown_ua,): | ||
device_type = detect_device(device_ua) | ||
assert not device_forbidden(device_type) | ||
|
||
|
||
@override_settings(DJANGO_FORBID={"DEVICES": ["desktop", "smartphone", "console", "tablet", "tv"]}) | ||
def test_access_desktops_smartphones_consoles_tablets_and_tvs(): | ||
"""Should allow access to desktops, smartphones, consoles, tablets and TVs.""" | ||
for device_ua in devices + (unknown_ua,): | ||
device_type = detect_device(device_ua) | ||
if device_type not in ("desktop", "smartphone", "console", "tablet", "tv"): | ||
# Forbid access to all non-listed devices. | ||
assert device_forbidden(device_type) | ||
continue | ||
assert not device_forbidden(device_type) | ||
|
||
|
||
@override_settings(DJANGO_FORBID={"DEVICES": ["!car", "!speaker", "!wearable"]}) | ||
def test_forbid_access_to_cars_speakers_and_wearables(): | ||
"""Should forbid access to cars, speakers and wearables.""" | ||
for device_ua in devices: | ||
device_type = detect_device(device_ua) | ||
if device_type in ("car", "speaker", "wearable"): | ||
# Forbid access to cars, speakers and wearables. | ||
assert device_forbidden(device_type) | ||
continue | ||
assert not device_forbidden(device_type) | ||
|
||
|
||
@override_settings(DJANGO_FORBID={"DEVICES": ["!phablet", "tablet", "phablet"]}) | ||
def test_forbid_access_if_same_device_is_listed_as_permitted_and_forbidden(): | ||
"""Should forbid access if the same device is listed as permitted and forbidden.""" | ||
for device_ua in devices + (unknown_ua,): | ||
device_type = detect_device(device_ua) | ||
if device_type != "tablet": | ||
# Forbid all non-tablet devices. | ||
assert device_forbidden(device_type) | ||
continue | ||
assert not device_forbidden(device_type) | ||
|
||
|
||
@override_settings(DJANGO_FORBID={"DEVICES": ["smartphone", "phablet", "tablet"]}) | ||
def test_forbid_access_unknown_devices_if_devices_are_set(): | ||
"""Should forbid access to unknown devices if the list of devices is not empty.""" | ||
device_type = detect_device(unknown_ua) | ||
assert device_forbidden(device_type) |