Skip to content

Commit 00e6be1

Browse files
authored
Allow modifying is_core via admin interface. (#457)
* Allow modifying is_core via admin interface. If modified, it triggers a new TRC, new keys and certs for all the ISD, and REMOVES all links that the AS had. * linting. * Language. * Test core and non-core modification.
1 parent 4119760 commit 00e6be1

File tree

3 files changed

+140
-6
lines changed

3 files changed

+140
-6
lines changed

scionlab/admin.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from django import urls
1717
from django.utils.html import format_html
1818
from django import forms
19-
from django.contrib import admin
19+
from django.contrib import admin, messages
2020
from django.core.exceptions import ValidationError
2121
from django.db.models import Count
2222
from django.shortcuts import get_object_or_404
@@ -485,10 +485,8 @@ def get_readonly_fields(self, request, obj):
485485
# FIXME(matzf) conceptually, an AS can change the ISD. Not allowed for now
486486
# as I anticipate this may unnecessarily complicate the TRC/certificate
487487
# update logic. Should be revisited.
488-
# TODO(matzf): Changing is_core should also be possible, not yet implemented
489-
# Requires removing core links etc, bump signed certificates
490488
if obj:
491-
return ('isd', 'is_core', 'as_id',)
489+
return ('isd', 'as_id',)
492490
return ()
493491

494492
def get_inline_instances(self, request, obj):
@@ -533,6 +531,16 @@ def update_core_keys(self, request, queryset):
533531
"""
534532
AS.update_core_as_keys(queryset)
535533

534+
def save_model(self, request, obj, form, change):
535+
"""
536+
Custom save model logic, to be able to show a warning message if saving the AS returned one.
537+
"""
538+
# Instead of calling super().save_model(...) we save the object directly and retrieve
539+
# its informational object about its changes.
540+
msg = obj.save_with_message()
541+
if msg is not None:
542+
messages.add_message(request, messages.WARNING, msg)
543+
536544

537545
class VPNCreationForm(_CreateUpdateModelForm):
538546
"""

scionlab/models/core.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,13 @@ def generate_certs(self):
333333
Certificate.objects.create_all_certs(self)
334334

335335
def update_keys_certs(self, not_before=None):
336+
"""
337+
Updates the keys and certs, and saves this AS object.
338+
"""
339+
self._update_keys_certs(not_before)
340+
self.save()
341+
342+
def _update_keys_certs(self, not_before=None):
336343
"""
337344
Generate new keys and certificates (core and non core).
338345
The keys and certs have to be updated simultaneously to avoid getting a nil validity range
@@ -343,7 +350,49 @@ def update_keys_certs(self, not_before=None):
343350
self.generate_keys(not_before)
344351
self.generate_certs()
345352
self.hosts.bump_config()
346-
self.save()
353+
354+
def save(self, **kwargs):
355+
# Do not return the message from `save_with_message` when calling save().
356+
self.save_with_message(**kwargs)
357+
358+
def save_with_message(self, **kwargs):
359+
"""
360+
Returns a message to be displayed when interactively modifying this AS.
361+
It will be used from the ASAdmin class.
362+
"""
363+
msg = None
364+
core_change = False
365+
# Check if is_core has changed: load previous object and compare with this one.
366+
if self.pk is not None: # Check if the object already exists in the database
367+
# Fetch the current value from the database
368+
# old_as = AS.objects.filter(pk=self.pk).values_list('is_core', flat=True).first()
369+
old_as = AS.objects.filter(pk=self.pk).first()
370+
if old_as.is_core != self.is_core:
371+
# This AS' core attribute has changed.
372+
# We must delete all existing links.
373+
interfaces = self.interfaces.all()
374+
links = Link.objects.filter(models.Q(
375+
interfaceA__in=interfaces) | models.Q(interfaceB__in=interfaces))
376+
link_count = links.count()
377+
links.delete()
378+
core_change = True
379+
# Finally, return a message to the caller (possibly from ASAdmin) to let them
380+
# know about the modifications.
381+
msg = f"The CORE property has changed: There was {link_count} link(s) deleted. " + \
382+
"New certificates for this AS, and new TRC for this ISD have been created. " + \
383+
"Please review changes."
384+
385+
if core_change:
386+
# Generate new certificates only for this AS. If this was a core AS and CA for other
387+
# ASes, we would have to manually update certificates and keys of those other ASes.
388+
self._update_keys_certs()
389+
# Save the model.
390+
super().save(**kwargs)
391+
if core_change:
392+
# Because the core AS set has changed, we need a new TRC.
393+
self.isd.trcs.create()
394+
395+
return msg
347396

348397
@staticmethod
349398
def update_cp_as_keys(queryset):
@@ -364,7 +413,7 @@ def update_cp_as_keys(queryset):
364413
def update_core_as_keys(queryset):
365414
"""
366415
All the ASes in the queryset must update their keys and certificates.
367-
Since this some of these ASes could act as CA for other, non-core ASes,
416+
Since this some of these ASes could now act as CA for other (non-core) ASes,
368417
we must re-issue all certificates in that ISD.
369418
So in this function, we just update keys, certificates and TRCs for all
370419
core ASes in all ISDs that are touched by the given queryset.

scionlab/tests/test_models.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import ipaddress
1515

1616
from unittest.mock import patch
17+
from django.db import models
1718
from django.test import TestCase
1819
from scionlab.defines import (
1920
DISPATCHER_PORT,
@@ -119,6 +120,82 @@ def test_update_keys(self):
119120
)
120121
self.assertEqual(new_certificate.version, prev_certificate.version + 1)
121122

123+
def test_is_core_not_modified(self):
124+
# if the is_core property is not modified, do not expect new keys or certs.
125+
as_ = AS.objects.get(as_id='ffaa:0:1304')
126+
prev_certs = list(as_.certificates())
127+
prev_keys = list(as_.keys.all())
128+
as_.mtu = 800 # modify a property but not is_core
129+
as_.save()
130+
131+
got_certs = list(as_.certificates().all())
132+
got_keys = list(as_.keys.all())
133+
self.assertEqual(got_certs, prev_certs)
134+
self.assertEqual(got_keys, prev_keys)
135+
136+
def test_is_core_modification(self):
137+
# Subfunction to perform the repetitive test.
138+
def _change_is_core_and_test(as_, core_state: bool):
139+
# Store state of ffaa:0:1304.
140+
prev_certs = list(as_.certificates())
141+
prev_keys = list(as_.keys.all())
142+
prev_trcs = list(as_.isd.trcs.all())
143+
prev_ifaces = as_.interfaces.all()
144+
prev_links = Link.objects.filter(
145+
models.Q(interfaceA__in=prev_ifaces) |
146+
models.Q(interfaceB__in=prev_ifaces))
147+
self.assertGreater(prev_links.count(), 0)
148+
prev_links = list(prev_links)
149+
prev_ifaces = list(prev_ifaces)
150+
151+
# now promote AS 19-ffaa:0:1304 to core AS. We expect the following to happen:
152+
# - existing links starting or ending at that AS are removed.
153+
# - new keys and certificates are issued (as core) for that AS.
154+
# - all certificates in that ISD are reissued.
155+
# - the ISD issues a new TRC
156+
as_.mtu = 800
157+
as_.is_core = core_state
158+
as_.save()
159+
160+
# Verify the previous links do not exist anymore.
161+
self.assertEqual(Link.objects.filter(pk__in=[
162+
link.pk for link in prev_links]).count(), 0)
163+
164+
# Same with interfaces.
165+
self.assertEqual(Interface.objects.filter(pk__in=[
166+
iface.pk for iface in prev_ifaces]).count(), 0)
167+
168+
# This AS doesn't have interfaces or links.
169+
got_ifaces = as_.interfaces.all()
170+
self.assertEqual(got_ifaces.count(), 0)
171+
got_links = Link.objects.filter(
172+
models.Q(interfaceA__in=got_ifaces) |
173+
models.Q(interfaceB__in=got_ifaces))
174+
self.assertEqual(got_links.count(), 0)
175+
176+
# We have now more certificates and keys.
177+
got_certs = list(as_.certificates().all())
178+
self.assertTrue(set(prev_certs).issubset(got_certs))
179+
got_keys = list(as_.keys.all())
180+
self.assertTrue(set(prev_keys).issubset(got_keys))
181+
182+
# We have a new TRC.
183+
got_trcs = list(as_.isd.trcs.all())
184+
self.assertTrue(set(prev_trcs).issubset(got_trcs))
185+
186+
# Get two ASes from the fixture, one core and one non-core.
187+
as1 = AS.objects.get(as_id='ffaa:0:1301')
188+
as2 = AS.objects.get(as_id='ffaa:0:1304')
189+
# test sanity check: the fixture should have:
190+
# ffaa:0:1301 core.
191+
# ffaa:0:1304 non-core.
192+
self.assertTrue(as1.is_core)
193+
self.assertFalse(as2.is_core)
194+
195+
# Test.
196+
_change_is_core_and_test(as1, False)
197+
_change_is_core_and_test(as2, True)
198+
122199

123200
class LinkModificationTests(TestCase):
124201
fixtures = []

0 commit comments

Comments
 (0)