From b675e68b5792d43ae8db17de9274025616866f35 Mon Sep 17 00:00:00 2001
From: Vincent Simonin <vincent.simonin@ext.ec.europa.eu>
Date: Mon, 31 Jul 2023 13:13:09 +0200
Subject: [PATCH] Add Mapping Saml configuration UI

---
 .../netbox_rps_plugin/__init__.py             |   2 +-
 .../netbox_rps_plugin/forms.py                |  89 +++++++----
 .../templates/netbox_rps_plugin/mapping.html  | 151 +++++++++++-------
 .../netbox_rps_plugin/saml_config.html        |  41 +++++
 .../netbox_rps_plugin/urls.py                 |  16 +-
 .../netbox_rps_plugin/views.py                |  36 ++++-
 plugins/netbox-rps-plugin/setup.py            |   2 +-
 7 files changed, 236 insertions(+), 101 deletions(-)
 create mode 100644 plugins/netbox-rps-plugin/netbox_rps_plugin/templates/netbox_rps_plugin/saml_config.html

diff --git a/plugins/netbox-rps-plugin/netbox_rps_plugin/__init__.py b/plugins/netbox-rps-plugin/netbox_rps_plugin/__init__.py
index 14b8baf..5629ea1 100644
--- a/plugins/netbox-rps-plugin/netbox_rps_plugin/__init__.py
+++ b/plugins/netbox-rps-plugin/netbox_rps_plugin/__init__.py
@@ -5,7 +5,7 @@ class NetBoxRpsConfig(PluginConfig):
     name = 'netbox_rps_plugin'
     verbose_name = 'NetBox RPS'
     description = 'A Netbox plugin to add RPS resources'
-    version = '0.9.0'
+    version = '0.10.0'
     author = "Vincent Simonin"
     author_email = "vincent.simonin@ext.ec.europa.eu"
     base_url = 'rps'
diff --git a/plugins/netbox-rps-plugin/netbox_rps_plugin/forms.py b/plugins/netbox-rps-plugin/netbox_rps_plugin/forms.py
index aa9eb67..13b9453 100644
--- a/plugins/netbox-rps-plugin/netbox_rps_plugin/forms.py
+++ b/plugins/netbox-rps-plugin/netbox_rps_plugin/forms.py
@@ -1,73 +1,100 @@
 from django import forms
 from django.utils.translation import gettext as _
-from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm
-from .models import Mapping, AuthenticationChoices, HttpHeader, ApplyToChoices
+from netbox.forms import (
+    NetBoxModelForm,
+    NetBoxModelFilterSetForm,
+    NetBoxModelImportForm,
+)
+from .models import Mapping, AuthenticationChoices, HttpHeader, ApplyToChoices, SamlConfig
 from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
 
 
 class MappingForm(NetBoxModelForm):
-
     class Meta:
         model = Mapping
         fields = (
-            'source', 'target', 'authentication', 'webdav', 'testingpage', 'Comment', 'tags',
+            "source",
+            "target",
+            "authentication",
+            "webdav",
+            "testingpage",
+            "Comment",
+            "tags",
         )
-        help_texts = {
-            'target': 'URL-friendly unique shorthand'
-        }
+        help_texts = {"target": "URL-friendly unique shorthand"}
         labels = {
-            'source': 'Source',
-            'target': 'Target',
+            "source": "Source",
+            "target": "Target",
         }
 
 
 class MappingFilterForm(NetBoxModelFilterSetForm):
     model = Mapping
-    source = forms.CharField(max_length=120, min_length=1, required=False, label='Source URL')
-    target = forms.CharField(max_length=120, min_length=1, required=False, label='Target URL')
+    source = forms.CharField(
+        max_length=120, min_length=1, required=False, label="Source URL"
+    )
+    target = forms.CharField(
+        max_length=120, min_length=1, required=False, label="Target URL"
+    )
     authentication = forms.MultipleChoiceField(
         choices=AuthenticationChoices,
         required=False,
     )
     webdav = forms.BooleanField(
-        required=False,
-        widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
+        required=False, widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
+    )
+    testingpage = forms.CharField(
+        max_length=120, min_length=1, required=False, label="Testing URL"
+    )
+    Comment = forms.CharField(
+        max_length=500, min_length=1, required=False, label="Comment"
     )
-    testingpage = forms.CharField(max_length=120, min_length=1, required=False, label='Testing URL')
-    Comment = forms.CharField(max_length=500, min_length=1, required=False, label='Comment')
     tag = TagFilterField(model)
 
 
 class MappingImportForm(NetBoxModelImportForm):
-
     class Meta:
         model = Mapping
-        fields = ('source', 'target', 'authentication', 'testingpage', 'webdav')
+        fields = ("source", "target", "authentication", "testingpage", "webdav")
 
 
 class HttpHeaderForm(NetBoxModelForm):
-
     class Meta:
         model = HttpHeader
-        fields = (
-            'mapping', 'name', 'value', 'apply_to'
-        )
+        fields = ("mapping", "name", "value", "apply_to")
         labels = {
-            'mapping': 'Mapping',
-            'name': 'Name',
-            'value': 'Value',
-            'apply_to': 'Apply to',
+            "mapping": "Mapping",
+            "name": "Name",
+            "value": "Value",
+            "apply_to": "Apply to",
         }
 
+
 class HttpHeaderFilterForm(NetBoxModelFilterSetForm):
     model = HttpHeader
-    name = forms.CharField(max_length=120, min_length=1, required=False, label='Header name')
-    value = forms.CharField(max_length=256, min_length=1, required=False, label='Header value')
-    apply_to = forms.ChoiceField(choices=add_blank_choice(ApplyToChoices), required=False, label='Apply to')
+    name = forms.CharField(
+        max_length=120, min_length=1, required=False, label="Header name"
+    )
+    value = forms.CharField(
+        max_length=256, min_length=1, required=False, label="Header value"
+    )
+    apply_to = forms.ChoiceField(
+        choices=add_blank_choice(ApplyToChoices), required=False, label="Apply to"
+    )
     mapping = DynamicModelMultipleChoiceField(
-        queryset=Mapping.objects.all(),
-        required=False,
-        label=_('Mapping')
+        queryset=Mapping.objects.all(), required=False, label=_("Mapping")
     )
     tag = TagFilterField(model)
+
+
+class SamlConfigForm(NetBoxModelForm):
+    class Meta:
+        model = SamlConfig
+        fields = ("mapping", "acs_url", "logout_url", "force_nauth")
+        labels = {
+            "mapping": "Mapping",
+            "acs_url": "ACS URL",
+            "logout_url": "Logout URL",
+            "force_nauth": "Force AuthnRequest",
+        }
diff --git a/plugins/netbox-rps-plugin/netbox_rps_plugin/templates/netbox_rps_plugin/mapping.html b/plugins/netbox-rps-plugin/netbox_rps_plugin/templates/netbox_rps_plugin/mapping.html
index 980d6d9..49327a7 100644
--- a/plugins/netbox-rps-plugin/netbox_rps_plugin/templates/netbox_rps_plugin/mapping.html
+++ b/plugins/netbox-rps-plugin/netbox_rps_plugin/templates/netbox_rps_plugin/mapping.html
@@ -1,76 +1,105 @@
 {% extends 'generic/object.html' %}
 
 {% block extra_controls %}
-  <a href="{% url 'plugins:netbox_rps_plugin:httpheader_add' %}?mapping={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">
-    <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add HTTP Header
-  </a>
+<a href="{% url 'plugins:netbox_rps_plugin:httpheader_add' %}?mapping={{ object.pk }}&return_url={{ object.get_absolute_url }}"
+  class="btn btn-sm btn-primary">
+  <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add HTTP Header
+</a>
+{% if not object.saml_config %}
+<a href="{% url 'plugins:netbox_rps_plugin:samlconfig_add' %}?mapping={{ object.pk }}&return_url={{ object.get_absolute_url }}"
+  class="btn btn-sm btn-primary">
+  <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add <abbr
+    title="Security Assertion Markup Language">SAML</abbr> Configuration
+</a>
+{% endif %}
 {% endblock %}
 
 {% block content %}
-  <div class="row mb-3">
-    <div class="col col-md-6">
-      <div class="card">
-        <h5 class="card-header">MAPPINGS</h5>
-        <div class="card-body">
-          <table class="table table-hover attr-table">
-            <tr>
-              <th scope="row">Name</th>
-              <td>{{ object.source }}</td>
-            </tr>
-            <tr>
-              <th scope="row">Comment</th>
-              <td>
-                {% if object.Comment %}
-                {{ object.Comment }}
+<div class="row mb-3">
+  <div class="col col-md-6">
+    <div class="card">
+      <h5 class="card-header">MAPPINGS</h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">Name</th>
+            <td>{{ object.source }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Comment</th>
+            <td>
+              {% if object.Comment %}
+              {{ object.Comment }}
               {% else %}
-                {{ ''|placeholder }}
+              {{ ''|placeholder }}
               {% endif %}
             </td>
-            </tr>
-          </table>
-        </div>
+          </tr>
+        </table>
       </div>
-      {% include 'inc/panels/custom_fields.html' %}
-      {% include 'inc/panels/tags.html' %}
     </div>
-    <div class="col col-md-6">
-      <div class="card">
-        <h5 class="card-header">Details</h5>
-        <div class="card-body">
-          <table class="table table-hover attr-table">
-            <tr>
-              <th scope="row">Source</th>
-              <td>{{ object.source }}</td>
-            </tr>
-            <tr>
-              <th scope="row">Target</th>
-              <td>{{ object.target }}</td>
-            </tr>
-            <tr>
-              <th scope="row">Authentication</th>
-              <td>{{ object.authentication }}</td>
-            </tr>
-            <tr>
-              <th scope="row">Testing page</th>
-              <td>{{ object.testingpage }}</td>
-            </tr>
-            <tr>
-              <th scope="row">Webdav</th>
-              <td>{{ object.webdav }}</td>
-            </tr>
-            <tr>
-              <th scope="row">Comment</th>
-              <td>
-                {% if object.Comment %}
-                  {{ object.Comment }}
-                {% else %}
-                  {{ ''|placeholder }}
-                {% endif %}
-              </td>
-            </tr>
-          </table>
-        </div>
+    {% include 'inc/panels/custom_fields.html' %}
+    {% include 'inc/panels/tags.html' %}
+  </div>
+  <div class="col col-md-6">
+    <div class="card">
+      <h5 class="card-header">Details</h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">Source</th>
+            <td>{{ object.source }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Target</th>
+            <td>{{ object.target }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Authentication</th>
+            <td>{{ object.authentication }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Testing page</th>
+            <td>{{ object.testingpage }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Webdav</th>
+            <td>{{ object.webdav }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Comment</th>
+            <td>
+              {% if object.Comment %}
+              {{ object.Comment }}
+              {% else %}
+              {{ ''|placeholder }}
+              {% endif %}
+            </td>
+          </tr>
+        </table>
+      </div>
+    </div>
+    {% if object.saml_config %}
+    <div class="card">
+      <h5 class="card-header">SAML Configuration</h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">ACS URL</th>
+            <td>{{ object.saml_config.acs_url }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Logout URL</th>
+            <td>{{ object.saml_config.logout_url }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Force AuthnRequest</th>
+            <td>{{ object.saml_config.force_nauth }}</td>
+          </tr>
+        </table>
       </div>
     </div>
+    {% endif %}
   </div>
+</div>
 {% endblock content %}
diff --git a/plugins/netbox-rps-plugin/netbox_rps_plugin/templates/netbox_rps_plugin/saml_config.html b/plugins/netbox-rps-plugin/netbox_rps_plugin/templates/netbox_rps_plugin/saml_config.html
new file mode 100644
index 0000000..4d356c8
--- /dev/null
+++ b/plugins/netbox-rps-plugin/netbox_rps_plugin/templates/netbox_rps_plugin/saml_config.html
@@ -0,0 +1,41 @@
+{% extends 'generic/object.html' %}
+{% load buttons %}
+{% load perms %}
+
+{% block content %}
+<div class="row mb-3">
+  <div class="col col-md-6">
+    <div class="card">
+      <h5 class="card-header">SAML Configuration</h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">ACS URL</th>
+            <td>{{ object.saml_config.acs_url }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Logout URL</th>
+            <td>{{ object.saml_config.logout_url }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Force AuthnRequest</th>
+            <td>{{ object.saml_config.force_nauth }}</td>
+          </tr>
+        </table>
+      </div>
+    </div>
+    {% include 'inc/panels/custom_fields.html' %}
+    {% include 'inc/panels/tags.html' %}
+  </div>
+</div>
+<div class="controls">
+  <div class="control-group">
+    {% if request.user|can_change:object.saml_config %}
+      {% edit_button object.saml_config %}
+    {% endif %}
+    {% if request.user|can_delete:object.saml_config %}
+      {% delete_button object.saml_config %}
+    {% endif %}
+  </div>
+</div>
+{% endblock content %}
diff --git a/plugins/netbox-rps-plugin/netbox_rps_plugin/urls.py b/plugins/netbox-rps-plugin/netbox_rps_plugin/urls.py
index dbe1e7d..417b7e9 100644
--- a/plugins/netbox-rps-plugin/netbox_rps_plugin/urls.py
+++ b/plugins/netbox-rps-plugin/netbox_rps_plugin/urls.py
@@ -14,6 +14,7 @@ urlpatterns = (
     path('mappings/<int:pk>/edit/', views.MappingEditView.as_view(), name='mapping_edit'),
     path('mappings/<int:pk>/delete/', views.MappingDeleteView.as_view(), name='mapping_delete'),
     path('mappings/<int:pk>/http-headers/', views.MappingHttpHeadersView.as_view(), name='mapping_httpheader'),
+    path('mappings/<int:pk>/saml-config/', views.MappingSamlConfigView.as_view(), name='mapping_samlconfig'),
     path('mappings/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='mapping_changelog', kwargs={
         'model': models.Mapping
     }),
@@ -32,6 +33,19 @@ urlpatterns = (
         'model': models.HttpHeader
     }),
     path('http-headers/<int:pk>/journal/', ObjectJournalView.as_view(), name='httpheader_journal', kwargs={
-        'model': models.Mapping
+        'model': models.HttpHeader
     }),
+
+    # Saml Config
+    #path('saml-configs/', views.HttpHeaderListView.as_view(), name='samlconfig_list'),
+    path('saml-configs/add/', views.SamlConfigEditView.as_view(), name='samlconfig_add'),
+    #path('saml-configs/<int:pk>/', views.HttpHeaderView.as_view(), name='samlconfig'),
+    path('saml-configs/<int:pk>/edit/', views.SamlConfigEditView.as_view(), name='samlconfig_edit'),
+    path('saml-configs/<int:pk>/delete/', views.SamlConfigDeleteView.as_view(), name='samlconfig_delete'),
+    #path('saml-configs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='samlconfig_changelog', kwargs={
+    #    'model': models.SamlConfig
+    #}),
+    #path('saml-configs/<int:pk>/journal/', ObjectJournalView.as_view(), name='samlconfig_journal', kwargs={
+    #    'model': models.SamlConfig
+    #}),
 )
diff --git a/plugins/netbox-rps-plugin/netbox_rps_plugin/views.py b/plugins/netbox-rps-plugin/netbox_rps_plugin/views.py
index 1990591..bf99e09 100644
--- a/plugins/netbox-rps-plugin/netbox_rps_plugin/views.py
+++ b/plugins/netbox-rps-plugin/netbox_rps_plugin/views.py
@@ -6,13 +6,16 @@ from django.utils.translation import gettext as _
 
 
 class MappingView(generic.ObjectView):
-    queryset = models.Mapping.objects.all().prefetch_related('http_headers')
+    queryset = (
+        models.Mapping.objects.all()
+        .prefetch_related("http_headers")
+        .prefetch_related("saml_config")
+    )
 
 
 class MappingListView(generic.ObjectListView):
-    #queryset = models.Mapping.objects.all()
     queryset = models.Mapping.objects.annotate(
-        httpheader_count=count_related(models.HttpHeader, 'mapping')
+        httpheader_count=count_related(models.HttpHeader, "mapping")
     )
     table = tables.MappingTable
     filterset = filtersets.MappingFilterSet
@@ -39,9 +42,9 @@ class MappingBulkDeleteView(generic.BulkDeleteView):
     table = tables.MappingTable
 
 
-@register_model_view(models.Mapping, 'httpheader')
+@register_model_view(models.Mapping, "httpheader")
 class MappingHttpHeadersView(generic.ObjectChildrenView):
-    queryset = models.Mapping.objects.all().prefetch_related('http_headers')
+    queryset = models.Mapping.objects.all().prefetch_related("http_headers")
     child_model = models.HttpHeader
     table = tables.HttpHeaderTable
     filterset = filtersets.HttpHeaderFilterSet
@@ -49,7 +52,7 @@ class MappingHttpHeadersView(generic.ObjectChildrenView):
     hide_if_empty = False
 
     tab = ViewTab(
-        label=_('HTTP Headers'),
+        label=_("HTTP Headers"),
         badge=lambda obj: obj.http_headers.count(),
         hide_if_empty=False,
     )
@@ -58,6 +61,18 @@ class MappingHttpHeadersView(generic.ObjectChildrenView):
         return parent.http_headers
 
 
+@register_model_view(models.Mapping, "samlconfig")
+class MappingSamlConfigView(generic.ObjectView):
+    base_template = "netbox_rps_plugin/mapping.html"
+    queryset = models.Mapping.objects.all().prefetch_related("saml_config")
+    template_name = "netbox_rps_plugin/saml_config.html"
+
+    tab = ViewTab(
+        label=_("SAML Configuration"),
+        hide_if_empty=True,
+    )
+
+
 class HttpHeaderView(generic.ObjectView):
     queryset = models.HttpHeader.objects.all()
 
@@ -82,3 +97,12 @@ class HttpHeaderBulkDeleteView(generic.BulkDeleteView):
     queryset = models.HttpHeader.objects.all()
     filterset = filtersets.HttpHeaderFilterSet
     table = tables.HttpHeaderTable
+
+
+class SamlConfigEditView(generic.ObjectEditView):
+    queryset = models.SamlConfig.objects.all()
+    form = forms.SamlConfigForm
+
+
+class SamlConfigDeleteView(generic.ObjectDeleteView):
+    queryset = models.SamlConfig.objects.all()
diff --git a/plugins/netbox-rps-plugin/setup.py b/plugins/netbox-rps-plugin/setup.py
index ce13692..d7f062c 100644
--- a/plugins/netbox-rps-plugin/setup.py
+++ b/plugins/netbox-rps-plugin/setup.py
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
 
 setup(
     name='netbox_rps_plugin',
-    version='0.9.0',
+    version='0.10.0',
     description='A Netbox plugin to add RPS resources',
     install_requires=[],
     packages=find_packages(),
-- 
GitLab