Skip to content

Commit b136c1f

Browse files
authored
Merge branch 'master' into updated-multitenant-tests
2 parents 73bc66f + 85eee35 commit b136c1f

File tree

5 files changed

+185
-1
lines changed

5 files changed

+185
-1
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
pull_request:
1010
branches:
1111
- master
12+
- "1.1"
1213

1314
jobs:
1415
build:

openwisp_controller/config/base/config.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import collections
22
import logging
33
import re
4+
from collections import defaultdict
45

56
from cache_memoize import cache_memoize
67
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
@@ -185,6 +186,60 @@ def _get_templates_from_pk_set(cls, pk_set):
185186
templates = pk_set
186187
return templates
187188

189+
@classmethod
190+
def clean_duplicate_vpn_client_templates(
191+
cls, action, instance, templates, raw_data=None
192+
):
193+
"""
194+
Multiple VPN client templates related to the same VPN server are not allowed:
195+
it hardly makes sense, preventing it keeps things simple and avoids headaches.
196+
Raises a ValidationError if duplicates are found.
197+
"""
198+
if action != "pre_add":
199+
return
200+
201+
def format_template_list(names):
202+
quoted = [f'"{name}"' for name in names]
203+
if len(quoted) == 2:
204+
return " and ".join(quoted)
205+
return ", ".join(quoted[:-1]) + " and " + quoted[-1]
206+
207+
def add_vpn_templates(templates_queryset):
208+
for template in templates_queryset.filter(type="vpn"):
209+
if template.name not in vpn_templates[template.vpn.name]:
210+
vpn_templates[template.vpn.name].append(template.name)
211+
212+
raw_data = raw_data or {}
213+
vpn_templates = defaultdict(list)
214+
if not raw_data:
215+
# When raw_data is present, validation is triggered by a
216+
# ConfigForm submission.
217+
# In this case, the "templates" queryset already contains only the templates
218+
# that are intended to be assigned. Templates that would be removed
219+
# (e.g., in a pre_clear action) have already been excluded from the
220+
# queryset.
221+
add_vpn_templates(instance.templates)
222+
add_vpn_templates(templates)
223+
224+
error_lines = [
225+
_(
226+
"You cannot select multiple VPN client templates related to"
227+
" the same VPN server."
228+
)
229+
]
230+
for vpn_name, template_names in vpn_templates.items():
231+
if len(template_names) < 2:
232+
continue
233+
template_list = format_template_list(sorted(template_names))
234+
error_lines.append(
235+
_(
236+
"The templates {template_list} are all linked"
237+
' to the same VPN server: "{vpn_name}".'
238+
).format(template_list=template_list, vpn_name=vpn_name)
239+
)
240+
if len(error_lines) > 1:
241+
raise ValidationError("\n".join(str(line) for line in error_lines))
242+
188243
@classmethod
189244
def clean_templates(cls, action, instance, pk_set, raw_data=None, **kwargs):
190245
"""
@@ -203,6 +258,9 @@ def clean_templates(cls, action, instance, pk_set, raw_data=None, **kwargs):
203258
)
204259
if not templates:
205260
return
261+
cls.clean_duplicate_vpn_client_templates(
262+
action, instance, templates, raw_data=raw_data
263+
)
206264
backend = instance.get_backend_instance(template_instances=templates)
207265
try:
208266
cls.clean_netjsonconfig_backend(backend)

openwisp_controller/config/tests/test_api.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,6 +1602,76 @@ def test_device_detail_api_change_config(self):
16021602
self.assertEqual(response.status_code, 200)
16031603
self.assertEqual(device.config.templates.count(), 0)
16041604

1605+
def test_multiple_vpn_client_templates_same_vpn(self):
1606+
"""
1607+
Assigning multiple templates of type 'vpn' referencing the same VPN
1608+
to a device's config raises error.
1609+
"""
1610+
org = self._get_org()
1611+
vpn = self._create_vpn(organization=org)
1612+
# Create two templates of type 'vpn' referencing the same VPN
1613+
vpn_template1 = self._create_template(
1614+
type="vpn", vpn=vpn, organization=org, name="VPN Client 1"
1615+
)
1616+
vpn_template2 = self._create_template(
1617+
type="vpn", vpn=vpn, organization=org, name="VPN Client 2"
1618+
)
1619+
device = self._create_device(organization=org)
1620+
config = self._create_config(device=device)
1621+
path = reverse("config_api:device_detail", args=[device.pk])
1622+
data = {
1623+
"name": device.name,
1624+
"organization": str(org.id),
1625+
"mac_address": device.mac_address,
1626+
"config": {
1627+
"backend": "netjsonconfig.OpenWrt",
1628+
"context": {"lan_ip": "192.168.1.1"},
1629+
"config": {"interfaces": [{"name": "wlan0", "type": "wireless"}]},
1630+
},
1631+
}
1632+
expected_error_message = (
1633+
"You cannot select multiple VPN client templates related"
1634+
" to the same VPN server.\n"
1635+
'The templates "VPN Client 1" and "VPN Client 2" are all '
1636+
'linked to the same VPN server: "test".'
1637+
)
1638+
1639+
with self.subTest("Add both templates at once"):
1640+
data["config"]["templates"] = [str(vpn_template1.pk), str(vpn_template2.pk)]
1641+
response = self.client.put(path, data, content_type="application/json")
1642+
self.assertEqual(response.status_code, 400)
1643+
self.assertEqual(
1644+
str(response.data["config"][0]),
1645+
expected_error_message,
1646+
)
1647+
self.assertEqual(config.templates.count(), 0)
1648+
self.assertEqual(config.vpnclient_set.count(), 0)
1649+
1650+
with self.subTest("Add one template at a time"):
1651+
data["config"]["templates"] = [str(vpn_template1.pk)]
1652+
response = self.client.put(path, data, content_type="application/json")
1653+
self.assertEqual(response.status_code, 200)
1654+
self.assertEqual(config.templates.count(), 1)
1655+
self.assertEqual(config.vpnclient_set.count(), 1)
1656+
1657+
# Now add the second template
1658+
data["config"]["templates"] = [str(vpn_template1.pk), str(vpn_template2.pk)]
1659+
response = self.client.put(path, data, content_type="application/json")
1660+
self.assertEqual(response.status_code, 400)
1661+
self.assertEqual(
1662+
str(response.data["config"][0]),
1663+
expected_error_message,
1664+
)
1665+
self.assertEqual(config.templates.filter(id=vpn_template1.id).count(), 1)
1666+
self.assertEqual(config.vpnclient_set.count(), 1)
1667+
1668+
with self.subTest("Change existing template with another"):
1669+
data["config"]["templates"] = [str(vpn_template2.pk)]
1670+
response = self.client.put(path, data, content_type="application/json")
1671+
self.assertEqual(response.status_code, 200)
1672+
self.assertEqual(config.templates.filter(id=vpn_template2.id).count(), 1)
1673+
self.assertEqual(config.vpnclient_set.count(), 1)
1674+
16051675
def test_device_patch_with_templates_of_same_org(self):
16061676
org1 = self._create_org(name="testorg")
16071677
d1 = self._create_device(name="org1-config", organization=org1)

openwisp_controller/config/tests/test_config.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,61 @@ class TestTransactionConfig(
892892
TestVpnX509Mixin,
893893
TransactionTestCase,
894894
):
895+
def test_multiple_vpn_client_templates_same_vpn(self):
896+
vpn1 = self._create_vpn(name="vpn1")
897+
vpn2 = self._create_vpn(name="vpn2")
898+
vpn1_template1 = self._create_template(
899+
name="vpn1-template1", type="vpn", vpn=vpn1
900+
)
901+
vpn1_template2 = self._create_template(
902+
name="vpn1-template2", type="vpn", vpn=vpn1
903+
)
904+
vpn2_template1 = self._create_template(
905+
name="vpn2-template1", type="vpn", vpn=vpn2
906+
)
907+
vpn2_template2 = self._create_template(
908+
name="vpn2-template2", type="vpn", vpn=vpn2
909+
)
910+
vpn2_template3 = self._create_template(
911+
name="vpn2-template3", type="vpn", vpn=vpn2
912+
)
913+
config = self._create_config(device=self._create_device())
914+
config.templates.add(vpn1_template1)
915+
with self.subTest("Adding duplicate vpn-client template one at time"):
916+
with self.assertRaises(ValidationError) as context_manager:
917+
config.templates.add(vpn1_template2)
918+
try:
919+
self.assertEqual(
920+
context_manager.exception.message,
921+
"You cannot select multiple VPN client templates related to the"
922+
" same VPN server.\n"
923+
'The templates "vpn1-template1" and "vpn1-template2" are all'
924+
' linked to the same VPN server: "vpn1".',
925+
)
926+
except AssertionError:
927+
self.fail("ValidationError not raised")
928+
929+
with self.subTest("Add multiple vpn client templates for multiple VPN"):
930+
config.refresh_from_db()
931+
self.assertEqual(config.templates.count(), 1)
932+
self.assertEqual(config.vpnclient_set.count(), 1)
933+
with self.assertRaises(ValidationError) as context_manager:
934+
config.templates.add(
935+
vpn1_template2, vpn2_template1, vpn2_template2, vpn2_template3
936+
)
937+
try:
938+
self.assertEqual(
939+
context_manager.exception.message,
940+
"You cannot select multiple VPN client templates related to the"
941+
" same VPN server.\n"
942+
'The templates "vpn1-template1" and "vpn1-template2" are all'
943+
' linked to the same VPN server: "vpn1".\n'
944+
'The templates "vpn2-template1", "vpn2-template2" and'
945+
' "vpn2-template3" are all linked to the same VPN server: "vpn2".',
946+
)
947+
except AssertionError:
948+
self.fail("ValidationError not raised")
949+
895950
def test_certificate_renew_invalidates_checksum_cache(self):
896951
config = self._create_config(organization=self._get_org())
897952
vpn_template = self._create_template(

openwisp_controller/config/tests/test_vpn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ def test_vpn_client_deletion(self):
249249
def _assert_vpn_client_cert(cert, vpn_client, cert_ct, vpn_client_ct):
250250
self.assertEqual(Cert.objects.filter(pk=cert.pk).count(), 1)
251251
self.assertEqual(VpnClient.objects.filter(pk=vpn_client.pk).count(), 1)
252-
vpnclient.delete()
252+
c.templates.remove(t)
253253
self.assertEqual(
254254
Cert.objects.filter(pk=cert.pk, revoked=False).count(), cert_ct
255255
)

0 commit comments

Comments
 (0)