Skip to content

Commit 592eb3a

Browse files
committed
[change] Remove WhoIs field if not configured
If who_is is not configured then Orgs should not be allowed to enable/disable the feature from admin. Added test cases for the same and also for the possible scenarios for WhoIs as a feature. Signed-off-by: DragnEmperor <[email protected]>
1 parent 093d3ca commit 592eb3a

File tree

9 files changed

+166
-50
lines changed

9 files changed

+166
-50
lines changed

docs/user/settings.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,7 @@ Allows to enable WhoIs lookup feature.
758758

759759
This feature is used to fetch details of a device based on its last
760760
reported public IP address. The fetched details include ASN, CIDR,
761-
address, timezone and organization name.
761+
address, timezone and name of organization associated with ASN.
762762

763763
This feature is disabled by default and requires setting the
764764
:ref:`OPENWISP_CONTROLLER_GEOIP_ACCOUNT_ID

docs/user/who-is.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ WHOIS Lookup
1919
Both of the settings :ref:`OPENWISP_CONTROLLER_GEOIP_ACCOUNT_ID` and
2020
:ref:`OPENWISP_CONTROLLER_GEOIP_LICENSE_KEY` are required to be set,
2121
to ensure the WhoIs Lookup feature can be enabled/disabled for each
22-
organization. Else, the feature will be disabled globally.
22+
organization. Else, the feature will be disabled globally and the
23+
field will not be available to configure on admin interface.
2324

2425
WhoIs feature includes fetching details of the last public ip address
2526
reported by a device to ensure better device management.

openwisp_controller/config/admin.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,13 @@ class ConfigSettingsInline(admin.StackedInline):
13621362
model = OrganizationConfigSettings
13631363
form = ConfigSettingsForm
13641364

1365+
def get_fields(self, request, obj=None):
1366+
fields = super().get_fields(request, obj)
1367+
# hide the 'who_is_enabled' field if WhoIs is not Configured
1368+
if not app_settings.WHO_IS_CONFIGURED:
1369+
fields = [f for f in fields if f != "who_is_enabled"]
1370+
return fields
1371+
13651372
OrganizationAdmin.save_on_top = True
13661373
OrganizationAdmin.inlines.insert(0, ConfigSettingsInline)
13671374
limits_inline_position = 1

openwisp_controller/config/apps.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ def ready(self, *args, **kwargs):
5353
self.register_dashboard_charts()
5454
self.register_menu_groups()
5555
self.notification_cache_update()
56-
if app_settings.WHO_IS_CONFIGURED:
57-
connect_who_is_handlers()
56+
connect_who_is_handlers()
5857

5958
def __setmodels__(self):
6059
self.device_model = load_model("config", "Device")

openwisp_controller/config/base/device.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,7 @@ class AbstractDevice(OrgMixin, BaseModel):
3030
physical properties of a network device
3131
"""
3232

33-
_changed_checked_fields = [
34-
"name",
35-
"group_id",
36-
"management_ip",
37-
"organization_id",
38-
"last_ip",
39-
]
33+
_changed_checked_fields = ["name", "group_id", "management_ip", "organization_id"]
4034

4135
name = models.CharField(
4236
max_length=64,
@@ -126,6 +120,11 @@ class Meta:
126120

127121
def __init__(self, *args, **kwargs):
128122
super().__init__(*args, **kwargs)
123+
# if WhoIs is configured, we need to look out for changes
124+
# in last_ip field as well
125+
if app_settings.WHO_IS_CONFIGURED:
126+
self._changed_checked_fields.append("last_ip")
127+
129128
self._set_initial_values_for_changed_checked_fields()
130129

131130
def _set_initial_values_for_changed_checked_fields(self):
@@ -287,8 +286,7 @@ def save(self, *args, **kwargs):
287286
self.key = self.generate_key(shared_secret)
288287
state_adding = self._state.adding
289288
super().save(*args, **kwargs)
290-
# We need to check for last_ip when device is created/updated to perform
291-
# WhoIs lookup. This is run only when WhoIs is configured.
289+
# WhoIs Lookup runs only when WhoIs is configured.
292290
if app_settings.WHO_IS_CONFIGURED:
293291
self._check_last_ip()
294292
if state_adding and self.group and self.group.templates.exists():

openwisp_controller/config/base/multitenancy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def get_context(self):
6262
return deepcopy(self.context)
6363

6464
def clean(self):
65-
if self.who_is_enabled and not app_settings.WHO_IS_CONFIGURED:
65+
if not app_settings.WHO_IS_CONFIGURED and self.who_is_enabled:
6666
raise ValidationError(
6767
_(
6868
"GEOIP_ACCOUNT_ID and GEOIP_LICENSE_KEY must be set "

openwisp_controller/config/who_is/handlers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
from django.db.models.signals import post_delete, post_save
22
from swapper import load_model
33

4+
from .. import settings as app_settings
5+
46

57
def connect_who_is_handlers():
8+
# if WHO_IS_CONFIGURED is False, we do not connect the handlers
9+
# and return early
10+
if not app_settings.WHO_IS_CONFIGURED:
11+
return
12+
613
Device = load_model("config", "Device")
714
WhoIsInfo = load_model("config", "WhoIsInfo")
815
OrganizationConfigSettings = load_model("config", "OrganizationConfigSettings")

openwisp_controller/config/who_is/test_who_is.py

Lines changed: 116 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import importlib
12
from unittest import mock
23

3-
from django.core.exceptions import ValidationError
4+
from django.core.exceptions import ImproperlyConfigured, ValidationError
5+
from django.db.models.signals import post_delete, post_save
46
from django.test import TestCase, TransactionTestCase, override_settings
57
from django.urls import reverse
68
from geoip2 import errors
79
from swapper import load_model
810

11+
from ...tests.utils import TestAdminMixin
912
from .. import settings as app_settings
1013
from .handlers import connect_who_is_handlers
1114
from .utils import CreateWhoIsMixin
@@ -18,26 +21,124 @@
1821
notification_qs = Notification.objects.all()
1922

2023

21-
class TestWhoIsInfoModel(CreateWhoIsMixin, TestCase):
22-
def setUp(self):
23-
self.admin = self._get_admin()
24-
# connect the signals related to who_is
25-
connect_who_is_handlers()
24+
class TestWhoIsFeature(CreateWhoIsMixin, TestAdminMixin, TestCase):
25+
@override_settings(
26+
OPENWISP_CONTROLLER_WHO_IS_ENABLED=False,
27+
OPENWISP_CONTROLLER_GEOIP_ACCOUNT_ID=None,
28+
OPENWISP_CONTROLLER_GEOIP_LICENSE_KEY=None,
29+
)
30+
def test_who_is_configuration_setting(self):
31+
# disconnect previously connected signals on app start, if any
32+
self._disconnect_signals()
33+
# reload app_settings to apply the overridden settings
34+
importlib.reload(app_settings)
35+
36+
with self.subTest("Test Signals not connected when WHO_IS_CONFIGURED is False"):
37+
# should not connect any handlers since WHO_IS_CONFIGURED is False
38+
connect_who_is_handlers()
39+
40+
assert not any(
41+
"device.delete_who_is_info" in str(r[0]) for r in post_delete.receivers
42+
)
43+
assert not any(
44+
"invalidate_org_config_cache_on_org_config_save" in str(r[0])
45+
for r in post_save.receivers
46+
)
47+
assert not any(
48+
"invalidate_org_config_cache_on_org_config_delete" in str(r[0])
49+
for r in post_delete.receivers
50+
)
51+
52+
with self.subTest(
53+
"Test WhoIs field hidden on admin when WHO_IS_CONFIGURED is False"
54+
):
55+
self._login()
56+
url = reverse(
57+
"admin:openwisp_users_organization_change",
58+
args=[self._get_org().pk],
59+
)
60+
response = self.client.get(url)
61+
self.assertNotContains(response, 'name="config_settings-0-who_is_enabled"')
2662

27-
def test_who_is_feature(self):
63+
with self.subTest(
64+
"Test ImproperlyConfigured raised when WHO_IS_CONFIGURED is False"
65+
):
66+
with override_settings(OPENWISP_CONTROLLER_WHO_IS_ENABLED=True):
67+
with self.assertRaises(ImproperlyConfigured):
68+
# reload app_settings to apply the overridden settings
69+
importlib.reload(app_settings)
70+
71+
with override_settings(
72+
OPENWISP_CONTROLLER_GEOIP_ACCOUNT_ID="test_account_id",
73+
OPENWISP_CONTROLLER_GEOIP_LICENSE_KEY="test_license_key",
74+
):
75+
importlib.reload(app_settings)
76+
with self.subTest(
77+
"Test WHO_IS_CONFIGURED is True when both settings are set"
78+
):
79+
self.assertTrue(app_settings.WHO_IS_CONFIGURED)
80+
81+
with self.subTest(
82+
"Test WhoIs field visible on admin when WHO_IS_CONFIGURED is True"
83+
):
84+
self._login()
85+
url = reverse(
86+
"admin:openwisp_users_organization_change",
87+
args=[self._get_org().pk],
88+
)
89+
response = self.client.get(url)
90+
self.assertContains(response, 'name="config_settings-0-who_is_enabled"')
91+
92+
def test_who_is_enabled(self):
2893
org = self._get_org()
2994
org_settings_obj = OrganizationConfigSettings(
3095
organization=org, who_is_enabled=True
3196
)
32-
with self.assertRaises(ValidationError):
97+
98+
with self.subTest("Test WhoIs not configured does not allow enabling who_is"):
99+
with mock.patch.object(
100+
app_settings, "WHO_IS_CONFIGURED", False
101+
), self.assertRaises(ValidationError):
102+
org_settings_obj.full_clean()
103+
104+
# create org settings object with WHO_IS_CONFIGURED set to True
105+
with mock.patch.object(app_settings, "WHO_IS_CONFIGURED", True):
33106
org_settings_obj.full_clean()
34107
org_settings_obj.save()
35108

36-
def test_validate_who_is_fields(self):
109+
with self.subTest("Test setting who_is enabled to True"):
110+
org.config_settings.who_is_enabled = True
111+
org.config_settings.save(update_fields=["who_is_enabled"])
112+
org.config_settings.refresh_from_db(fields=["who_is_enabled"])
113+
self.assertEqual(getattr(org.config_settings, "who_is_enabled"), True)
114+
115+
with self.subTest("Test setting who_is enabled to False"):
116+
org.config_settings.who_is_enabled = False
117+
org.config_settings.save(update_fields=["who_is_enabled"])
118+
org.config_settings.refresh_from_db(fields=["who_is_enabled"])
119+
self.assertEqual(getattr(org.config_settings, "who_is_enabled"), False)
120+
121+
with self.subTest(
122+
"Test setting who_is enabled to None fallbacks to global setting"
123+
):
124+
# reload app_settings to ensure latest settings are applied
125+
importlib.reload(app_settings)
126+
org.config_settings.who_is_enabled = None
127+
org.config_settings.save(update_fields=["who_is_enabled"])
128+
org.config_settings.refresh_from_db(fields=["who_is_enabled"])
129+
self.assertEqual(
130+
getattr(org.config_settings, "who_is_enabled"),
131+
app_settings.WHO_IS_ENABLED,
132+
)
133+
134+
135+
class TestWhoIsInfoModel(CreateWhoIsMixin, TestCase):
136+
def test_who_is_model_fields_validation(self):
37137
"""
38138
Test db_constraints and validators for WhoIsInfo model fields.
39139
"""
40140
org = self._get_org()
141+
# using `create` to bypass `clean` method validation
41142
OrganizationConfigSettings.objects.create(organization=org, who_is_enabled=True)
42143

43144
with self.assertRaises(ValidationError):
@@ -55,27 +156,6 @@ def test_validate_who_is_fields(self):
55156
with self.assertRaises(ValidationError):
56157
self._create_who_is_info(asn="InvalidASN")
57158

58-
def test_who_is_enabled(self):
59-
org = self._get_org()
60-
OrganizationConfigSettings.objects.create(organization=org)
61-
62-
with self.subTest("Test who_is enabled set to True"):
63-
org.config_settings.who_is_enabled = True
64-
self.assertEqual(getattr(org.config_settings, "who_is_enabled"), True)
65-
66-
with self.subTest("Test who_is enabled set to False"):
67-
org.config_settings.who_is_enabled = False
68-
self.assertEqual(getattr(org.config_settings, "who_is_enabled"), False)
69-
70-
with self.subTest("Test who_is enabled set to None"):
71-
org.config_settings.who_is_enabled = None
72-
org.config_settings.save(update_fields=["who_is_enabled"])
73-
org.config_settings.refresh_from_db(fields=["who_is_enabled"])
74-
self.assertEqual(
75-
getattr(org.config_settings, "who_is_enabled"),
76-
app_settings.WHO_IS_ENABLED,
77-
)
78-
79159

80160
class TestWhoIsTransaction(CreateWhoIsMixin, TransactionTestCase):
81161
_WHO_IS_GEOIP_CLIENT = (
@@ -89,8 +169,6 @@ class TestWhoIsTransaction(CreateWhoIsMixin, TransactionTestCase):
89169

90170
def setUp(self):
91171
self.admin = self._get_admin()
92-
# connect the signals related to who_is
93-
connect_who_is_handlers()
94172

95173
@mock.patch.object(app_settings, "WHO_IS_CONFIGURED", True)
96174
@mock.patch(
@@ -99,6 +177,7 @@ def setUp(self):
99177
def test_task_called(self, mocked_task):
100178
org = self._get_org()
101179
OrganizationConfigSettings.objects.create(organization=org, who_is_enabled=True)
180+
connect_who_is_handlers()
102181

103182
with self.subTest("task called when last_ip is public"):
104183
device = self._create_device(last_ip="172.217.22.14")
@@ -117,8 +196,9 @@ def test_task_called(self, mocked_task):
117196
mocked_task.assert_not_called()
118197
mocked_task.reset_mock()
119198

120-
with self.subTest("task not called when last_ip is not changed"):
121-
device.last_ip = "10.0.0.1"
199+
with self.subTest("task not called when last_ip has related WhoIsInfo"):
200+
device.last_ip = "172.217.22.10"
201+
self._create_who_is_info(ip_address=device.last_ip)
122202
device.save()
123203
mocked_task.assert_not_called()
124204
mocked_task.reset_mock()
@@ -127,15 +207,15 @@ def test_task_called(self, mocked_task):
127207
Device.objects.all().delete() # Clear existing devices
128208
org.config_settings.who_is_enabled = False
129209
# Invalidates old org config settings cache
130-
org.config_settings.save()
210+
org.config_settings.save(update_fields=["who_is_enabled"])
131211
device = self._create_device(last_ip="172.217.22.14")
132212
mocked_task.assert_not_called()
133213
mocked_task.reset_mock()
134214

135215
with self.subTest("task called via DeviceChecksumView when who_is is enabled"):
136216
org.config_settings.who_is_enabled = True
137217
# Invalidates old org config settings cache
138-
org.config_settings.save()
218+
org.config_settings.save(update_fields=["who_is_enabled"])
139219
# config is required for checksum view to work
140220
self._create_config(device=device)
141221
# setting remote address field to a public IP

openwisp_controller/config/who_is/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from django.db.models.signals import post_delete, post_save
12
from swapper import load_model
23

34
from ..tests.utils import CreateConfigMixin
45

6+
Device = load_model("config", "Device")
57
WhoIsInfo = load_model("config", "WhoIsInfo")
8+
OrganizationConfigSettings = load_model("config", "OrganizationConfigSettings")
69

710

811
class CreateWhoIsMixin(CreateConfigMixin):
@@ -26,3 +29,24 @@ def _create_who_is_info(self, **kwargs):
2629
w.full_clean()
2730
w.save()
2831
return w
32+
33+
# Signals are connected when apps are loaded,
34+
# and if WhoIs is Configured all related WhoIs
35+
# handlers are also connected. Thus we need to
36+
# disconnect them.
37+
def _disconnect_signals(self):
38+
post_delete.disconnect(
39+
WhoIsInfo.device_who_is_info_delete_handler,
40+
sender=Device,
41+
dispatch_uid="device.delete_who_is_info",
42+
)
43+
post_save.disconnect(
44+
WhoIsInfo.invalidate_org_settings_cache,
45+
sender=OrganizationConfigSettings,
46+
dispatch_uid="invalidate_org_config_cache_on_org_config_save",
47+
)
48+
post_delete.disconnect(
49+
WhoIsInfo.invalidate_org_settings_cache,
50+
sender=OrganizationConfigSettings,
51+
dispatch_uid="invalidate_org_config_cache_on_org_config_delete",
52+
)

0 commit comments

Comments
 (0)