Skip to content

Commit 946a1f7

Browse files
committed
[fix] Ensure VpnClient post_delete signals fire on removal openwisp#1221
Signed-off-by: prakash-kalwaniya <kalwaniyaprakash1@gmail.com>
1 parent f3c99c4 commit 946a1f7

File tree

3 files changed

+67
-5
lines changed

3 files changed

+67
-5
lines changed

openwisp_controller/config/api/serializers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,14 @@ def _update_config(self, device, config_data):
202202
with transaction.atomic():
203203
vpn_list = config.templates.filter(type="vpn").values_list("vpn")
204204
if vpn_list:
205-
config.vpnclient_set.exclude(vpn__in=vpn_list).delete()
205+
# Use per-instance deletion to ensure post_delete
206+
# signals fire (peer cache, cert revocation, etc.)
207+
for vpnclient in (
208+
config.vpnclient_set.select_related("vpn", "cert", "ip")
209+
.exclude(vpn__in=vpn_list)
210+
.iterator()
211+
):
212+
vpnclient.delete()
206213
config.templates.set(config_templates, clear=True)
207214
config.save()
208215
except ValidationError as error:

openwisp_controller/config/base/config.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,14 @@ def manage_vpn_clients(cls, action, instance, pk_set, **kwargs):
338338
if instance.is_deactivating_or_deactivated():
339339
# If the device is deactivated or in the process of deactivating, then
340340
# delete all vpn clients and return.
341-
instance.vpnclient_set.all().delete()
341+
# Use per-instance deletion to ensure post_delete signals fire,
342+
# which handles peer cache invalidation, cert revocation,
343+
# ZeroTier member removal, and IP address release.
344+
with transaction.atomic():
345+
for vpnclient in instance.vpnclient_set.select_related(
346+
"vpn", "cert", "ip"
347+
).iterator():
348+
vpnclient.delete()
342349
return
343350

344351
vpn_client_model = cls.vpn.through
@@ -370,9 +377,18 @@ def manage_vpn_clients(cls, action, instance, pk_set, **kwargs):
370377
# signal is triggered again—after all templates, including the required
371378
# ones, have been fully added. At that point, we can identify and
372379
# delete VpnClient objects not linked to the final template set.
373-
instance.vpnclient_set.exclude(
374-
template_id__in=instance.templates.values_list("id", flat=True)
375-
).delete()
380+
# Use per-instance deletion to ensure post_delete signals fire,
381+
# which handles peer cache invalidation, cert revocation,
382+
# ZeroTier member removal, and IP address release.
383+
with transaction.atomic():
384+
for vpnclient in (
385+
instance.vpnclient_set.select_related("vpn", "cert", "ip")
386+
.exclude(
387+
template_id__in=instance.templates.values_list("id", flat=True)
388+
)
389+
.iterator()
390+
):
391+
vpnclient.delete()
376392

377393
if action == "post_add":
378394
for template in templates.filter(type="vpn"):

openwisp_controller/config/tests/test_vpn.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,45 @@ def _assert_vpn_client_cert(cert, vpn_client, cert_ct, vpn_client_ct):
286286
vpnclient.save()
287287
_assert_vpn_client_cert(cert, vpnclient, 1, 0)
288288

289+
def test_vpn_client_post_delete_on_template_removal(self):
290+
"""Regression test for #1221: VpnClient.post_delete must fire
291+
when a VPN template is removed so that peer cache is invalidated
292+
and certificates are properly revoked."""
293+
org = self._get_org()
294+
vpn = self._create_vpn()
295+
t = self._create_template(name="vpn-test", type="vpn", vpn=vpn, auto_cert=True)
296+
c = self._create_config(organization=org)
297+
c.templates.add(t)
298+
vpnclient = c.vpnclient_set.first()
299+
self.assertIsNotNone(vpnclient)
300+
cert_pk = vpnclient.cert.pk
301+
with mock.patch.object(Vpn, "_invalidate_peer_cache") as mock_invalidate:
302+
c.templates.remove(t)
303+
mock_invalidate.assert_called()
304+
self.assertFalse(VpnClient.objects.filter(pk=vpnclient.pk).exists())
305+
# Certificate should be revoked (auto_cert=True)
306+
self.assertTrue(Cert.objects.get(pk=cert_pk).revoked)
307+
308+
def test_vpn_client_post_delete_on_device_deactivation(self):
309+
"""Regression test for #1221: VpnClient.post_delete must fire
310+
when a device is deactivated so that peer cache is invalidated
311+
and certificates are properly revoked."""
312+
org = self._get_org()
313+
vpn = self._create_vpn()
314+
t = self._create_template(name="vpn-test", type="vpn", vpn=vpn, auto_cert=True)
315+
d = self._create_device(organization=org)
316+
c = self._create_config(device=d)
317+
c.templates.add(t)
318+
vpnclient = c.vpnclient_set.first()
319+
self.assertIsNotNone(vpnclient)
320+
cert_pk = vpnclient.cert.pk
321+
with mock.patch.object(Vpn, "_invalidate_peer_cache") as mock_invalidate:
322+
d.deactivate()
323+
mock_invalidate.assert_called()
324+
self.assertFalse(VpnClient.objects.filter(pk=vpnclient.pk).exists())
325+
# Certificate should be revoked (auto_cert=True)
326+
self.assertTrue(Cert.objects.get(pk=cert_pk).revoked)
327+
289328
def test_vpn_client_get_common_name(self):
290329
vpn = self._create_vpn()
291330
d = self._create_device()

0 commit comments

Comments
 (0)