From b92f4cc0e021027b6b4f958d9acf89d27f3cf0c7 Mon Sep 17 00:00:00 2001
From: Vincent Simonin <vincent.simonin@ext.ec.europa.eu>
Date: Mon, 11 Sep 2023 18:13:07 +0200
Subject: [PATCH] Make alt_name an array of string

---
 .../netbox_cert_plugin/filtersets.py          |  8 ++--
 .../netbox_cert_plugin/forms.py               |  1 +
 .../migrations/0001_initial.py                |  1 +
 .../migrations/0002_new_alt_name_array.py     | 27 ++++++++++++++
 .../netbox_cert_plugin/models.py              | 20 +++++-----
 .../tests/e2e/test_certificate_creation.py    | 37 +++++++++++++++++++
 6 files changed, 81 insertions(+), 13 deletions(-)
 create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0002_new_alt_name_array.py

diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/filtersets.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/filtersets.py
index 51bf487..e64bec8 100644
--- a/plugins/netbox-cert-plugin/netbox_cert_plugin/filtersets.py
+++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/filtersets.py
@@ -1,5 +1,6 @@
 """Filtersets definitions"""
 
+from django_filters import filters
 from netbox.filtersets import NetBoxModelFilterSet
 from django.db.models import Q
 from .models import Certificate
@@ -8,6 +9,8 @@ from .models import Certificate
 class CertificateFilterSet(NetBoxModelFilterSet):
     """Certificate filterset definition class"""
 
+    alt_name = filters.CharFilter(lookup_expr="icontains")
+
     class Meta:
         model = Certificate
         fields = (
@@ -25,7 +28,4 @@ class CertificateFilterSet(NetBoxModelFilterSet):
         """override"""
         if not value.strip():
             return queryset
-        return queryset.filter(
-            Q(cn__icontains=value)
-            | Q(alt_name__icontains=value)
-        )
+        return queryset.filter(Q(cn__icontains=value) | Q(alt_name__icontains=value))
diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/forms.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/forms.py
index 736821f..e458a92 100644
--- a/plugins/netbox-cert-plugin/netbox_cert_plugin/forms.py
+++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/forms.py
@@ -83,4 +83,5 @@ class CertificateImportForm(NetBoxModelImportForm):
         labels = {
             "ca": "Certificate Authority. Can be 'letsencrypt', 'comisign', 'globalsign'",
             "expiration_time": "Expiration time needed. Can be '1m', '3m', '6m', '1y', '3y'",
+            "alt_name": "Alt name separated by commas, encased with double quotes (e.g. \"alt1,alt2,alt3\")"
         }
diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0001_initial.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0001_initial.py
index 9379126..2f7476d 100644
--- a/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0001_initial.py
+++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0001_initial.py
@@ -2,6 +2,7 @@
 # pylint: disable=C0103
 
 from django.db import migrations, models
+from django.contrib.postgres.fields.array import ArrayField
 import taggit.managers
 import utilities.json
 
diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0002_new_alt_name_array.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0002_new_alt_name_array.py
new file mode 100644
index 0000000..eead2a3
--- /dev/null
+++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0002_new_alt_name_array.py
@@ -0,0 +1,27 @@
+"""Migration File"""
+# pylint: disable=C0103
+
+from django.db import migrations, models
+from django.contrib.postgres.fields.array import ArrayField
+
+
+class Migration(migrations.Migration):
+    """Migration Class"""
+
+    initial = True
+
+    dependencies = [("netbox_cert_plugin", "0001_initial")]
+
+    operations = [
+        migrations.AddField(
+            model_name="Certificate",
+            name="new_alt_name",
+            field=ArrayField(models.CharField(max_length=256), blank=True, null=True),
+        ),
+        migrations.RunSQL(
+            sql="UPDATE netbox_cert_plugin_certificate AS t set new_alt_name=ARRAY[t.alt_name]",
+            reverse_sql="UPDATE netbox_cert_plugin_certificate AS t set new_alt_name=NULL",
+        ),
+        migrations.RemoveField(model_name="Certificate", name="alt_name"),
+        migrations.RenameField(model_name="Certificate", new_name="alt_name", old_name="new_alt_name")
+    ]
diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/models.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/models.py
index 7812802..a504910 100644
--- a/plugins/netbox-cert-plugin/netbox_cert_plugin/models.py
+++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/models.py
@@ -2,6 +2,7 @@
 
 from django.db import models
 from django.urls import reverse
+from django.contrib.postgres.fields.array import ArrayField
 from netbox.models import NetBoxModel
 from utilities.choices import ChoiceSet
 
@@ -19,6 +20,7 @@ class CaChoices(ChoiceSet):
         ("globalsign", "GlobalSign", "blue"),
     ]
 
+
 class ExpirationTimeChoices(ChoiceSet):
     """Expiration time choices definition class"""
 
@@ -43,12 +45,16 @@ class Certificate(NetBoxModel):
         blank=False,
         verbose_name="Common Name",
         unique=True,
+        help_text="Unique Common Name"
     )
-    alt_name = models.CharField(
-        max_length=256,
-        blank=True,
+    alt_name = ArrayField(
+        base_field=models.CharField(
+            max_length=256
+        ),
         null=True,
+        blank=True,
         verbose_name="Alt Name",
+        help_text="Alt Name is a list of host separated by commas (e.g. alt1,alt2,alt3)"
     )
     ca = models.CharField(
         max_length=32,
@@ -65,14 +71,10 @@ class Certificate(NetBoxModel):
         verbose_name="Expiration time needed",
     )
     cert_created_at = models.DateField(
-        blank=True,
-        null=True,
-        verbose_name="Effective certificate's creation date"
+        blank=True, null=True, verbose_name="Effective certificate's creation date"
     )
     cert_expired_at = models.DateField(
-        blank=True,
-        null=True,
-        verbose_name="Effective certificate's expiration date"
+        blank=True, null=True, verbose_name="Effective certificate's expiration date"
     )
 
     class Meta:
diff --git a/plugins/netbox-cert-plugin/tests/e2e/test_certificate_creation.py b/plugins/netbox-cert-plugin/tests/e2e/test_certificate_creation.py
index 9378354..bf91abb 100644
--- a/plugins/netbox-cert-plugin/tests/e2e/test_certificate_creation.py
+++ b/plugins/netbox-cert-plugin/tests/e2e/test_certificate_creation.py
@@ -100,6 +100,43 @@ class TestCertificateCreation(unittest.TestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.content, b'{"expiration_time":["\\"10m\\" is not a valid choice."]}')
 
+    def test_that_alt_name_is_an_array(self) -> None:
+        """Test that Alt name is an array"""
+
+        response = requests.post(
+            url=f"http://{HOST}:{PORT}/api/plugins/cert/certificate/",
+            json={
+                "cn": "truc00.com",
+                "ca": "letsencrypt",
+                "expiration_time": "1m",
+                "alt_name": "truc01.com"
+            },
+            headers={"Authorization": f"Token {API_KEY}"},
+            timeout=5,
+        )
+
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.content, b'{"alt_name":["Expected a list of items but got type \\"str\\"."]}')
+
+    def test_that_certificate_is_created_with_alt_name(self) -> None:
+        """Test that certificate is created"""
+
+        response = requests.post(
+            url=f"http://{HOST}:{PORT}/api/plugins/cert/certificate/",
+            json={
+                "cn": "truc00.com",
+                "ca": "letsencrypt",
+                "expiration_time": "1m",
+                "alt_name": ["192.168.1.1", "truc01.com"]
+            },
+            headers={"Authorization": f"Token {API_KEY}"},
+            timeout=5,
+        )
+
+        self.assertEqual(response.status_code, 201)
+
+        self.mapping_id = json.loads(response.content)["id"]
+
     def tearDown(self) -> None:
         """Teardown function"""
 
-- 
GitLab