From 6d89989aa637f84708ecbe2d0606722b00a15911 Mon Sep 17 00:00:00 2001
From: Frank Sauerburger <frank@sauerburger.com>
Date: Fri, 10 Jun 2022 10:35:56 +0200
Subject: [PATCH] Implement simple wireguard listing

---
 .gitignore                                    |  1 +
 app/keys/settings.py                          |  1 +
 app/keys/urls.py                              |  1 +
 app/keys_home/static/keys_home/css/main.scss  | 58 +++++++++++++++++++
 app/keys_home/templates/keys_home/base.html   |  5 ++
 app/keys_home/templates/keys_home/home.html   |  4 ++
 app/wireguard/__init__.py                     |  0
 app/wireguard/admin.py                        |  8 +++
 app/wireguard/apps.py                         |  5 ++
 app/wireguard/forms.py                        |  9 +++
 app/wireguard/migrations/0001_initial.py      | 23 ++++++++
 app/wireguard/migrations/__init__.py          |  0
 app/wireguard/models.py                       |  9 +++
 .../wireguard/wireguardpublickey_create.html  | 22 +++++++
 .../wireguard/wireguardpublickey_item.html    | 20 +++++++
 .../wireguard/wireguardpublickey_list.html    | 33 +++++++++++
 app/wireguard/tests.py                        | 31 ++++++++++
 app/wireguard/urls.py                         |  9 +++
 app/wireguard/views.py                        | 41 +++++++++++++
 19 files changed, 280 insertions(+)
 create mode 100644 app/wireguard/__init__.py
 create mode 100644 app/wireguard/admin.py
 create mode 100644 app/wireguard/apps.py
 create mode 100644 app/wireguard/forms.py
 create mode 100644 app/wireguard/migrations/0001_initial.py
 create mode 100644 app/wireguard/migrations/__init__.py
 create mode 100644 app/wireguard/models.py
 create mode 100644 app/wireguard/templates/wireguard/wireguardpublickey_create.html
 create mode 100644 app/wireguard/templates/wireguard/wireguardpublickey_item.html
 create mode 100644 app/wireguard/templates/wireguard/wireguardpublickey_list.html
 create mode 100644 app/wireguard/tests.py
 create mode 100644 app/wireguard/urls.py
 create mode 100644 app/wireguard/views.py

diff --git a/.gitignore b/.gitignore
index 71fe82a..80c937d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ CACHE
 .env
 app/keys_home/static/react/
 node_modules/
+venv/
diff --git a/app/keys/settings.py b/app/keys/settings.py
index a408d75..6a5a03a 100644
--- a/app/keys/settings.py
+++ b/app/keys/settings.py
@@ -72,6 +72,7 @@ INSTALLED_APPS = [
     'owlca',
     'pgp',
     'ssh',
+    'wireguard',
     'guardian',
     'fontawesome-free',
     'crispy_forms',
diff --git a/app/keys/urls.py b/app/keys/urls.py
index dea577d..859a857 100644
--- a/app/keys/urls.py
+++ b/app/keys/urls.py
@@ -21,6 +21,7 @@ urlpatterns = [
     path('pgp/', include('pgp.urls')),
     path('ssh/', include('ssh.urls')),
     path('pks/', include('hkp.urls')),
+    path('wireguard/', include('wireguard.urls')),
     path('.well-known/openpgpkey/', include('wkd.urls')),
     path('admin/', admin.site.urls),
     path('', include('keys_home.urls')),
diff --git a/app/keys_home/static/keys_home/css/main.scss b/app/keys_home/static/keys_home/css/main.scss
index 3b50237..8b8ee9c 100644
--- a/app/keys_home/static/keys_home/css/main.scss
+++ b/app/keys_home/static/keys_home/css/main.scss
@@ -440,3 +440,61 @@ body {
     @extend .my-3;
   }
 }
+
+/********************************************************/
+/* Wireguard Key                                              */
+/********************************************************/
+.wireguardkey-item {
+  @extend .border, .my-3, .rounded;
+
+  // every (except last) content box should have a border below
+  & > div {
+    @extend .border-bottom, .px-4, .py-2;
+  }
+  & > div:last-child {
+    border-bottom: none !important;
+    @extend .rounded;
+  }
+
+  /********************************/
+  /* Heading                      */
+  /********************************/
+  h3 {
+    // dark heading per key
+    position: relative; // stretched link inside
+    justify-content: space-between;
+    display: flex;
+    @extend .rounded-top, .text-light, .bg-dark, .p-2;
+    margin-bottom: 0;
+    a {
+      @extend .text-light, .stretched-link;
+    }
+  }
+
+  /********************************/
+  /* Fingerprint                  */
+  /********************************/
+  .wireguardkey-fingerprint {
+    @extend .bg-light;
+  }
+
+  .wireguardkey-fingerprint-title {
+    @extend .text-muted, .mb-1;
+    i, svg {
+      @extend .mr-1;
+    }
+  }
+  .wireguardkey-fingerprint-hash {
+    @extend .input-group, .mb-2;
+    input {
+      @extend .form-control, .bg-white;
+      font-family: monospace;
+    }
+    input + div {
+      @extend .input-group-append;
+      span { 
+        @extend .input-group-text;
+      }
+    }
+  }
+}
diff --git a/app/keys_home/templates/keys_home/base.html b/app/keys_home/templates/keys_home/base.html
index e2fd7b3..317efa8 100644
--- a/app/keys_home/templates/keys_home/base.html
+++ b/app/keys_home/templates/keys_home/base.html
@@ -51,6 +51,11 @@
               <i class="fas fa-network-wired"></i> PKI
             </a>
           </li>
+          <li>
+            <a href="{% url 'wireguard-list' %}">
+              <i class="fas fa-dragon"></i> Wireguard
+            </a>
+          </li>
           {% if request.user.is_anonymous %}
           <li class="navbar-login">
             <a href="{% url 'login' %}">
diff --git a/app/keys_home/templates/keys_home/home.html b/app/keys_home/templates/keys_home/home.html
index 17756a6..3c0348f 100644
--- a/app/keys_home/templates/keys_home/home.html
+++ b/app/keys_home/templates/keys_home/home.html
@@ -16,6 +16,10 @@
         <i class="fas fa-network-wired mb-2"></i><br />
         CAs and Certificates
     </a></h3></li>
+    <li><h3><a href="{% url 'wireguard-list' %}">
+        <i class="fas fa-dragon mb-2"></i><br />
+        Wireguard
+    </a></h3></li>
 </ul>
 
 {% endblock %}
diff --git a/app/wireguard/__init__.py b/app/wireguard/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/wireguard/admin.py b/app/wireguard/admin.py
new file mode 100644
index 0000000..09b49fd
--- /dev/null
+++ b/app/wireguard/admin.py
@@ -0,0 +1,8 @@
+from django.contrib import admin
+from guardian.admin import GuardedModelAdmin
+from . import models
+
+class WireguardPublicKeyAdmin(GuardedModelAdmin):
+    pass
+
+admin.site.register(models.WireguardPublicKey, WireguardPublicKeyAdmin)
diff --git a/app/wireguard/apps.py b/app/wireguard/apps.py
new file mode 100644
index 0000000..84ea2f5
--- /dev/null
+++ b/app/wireguard/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class WireguardConfig(AppConfig):
+    name = 'wireguard'
diff --git a/app/wireguard/forms.py b/app/wireguard/forms.py
new file mode 100644
index 0000000..a011c72
--- /dev/null
+++ b/app/wireguard/forms.py
@@ -0,0 +1,9 @@
+
+from django import forms
+from django.core.exceptions import ValidationError
+
+
+class PublicKeyCreateForm(forms.Form):
+    interface = forms.CharField(max_length=128)
+    key = forms.CharField(max_length=128)
+    public = forms.BooleanField(required=False)
diff --git a/app/wireguard/migrations/0001_initial.py b/app/wireguard/migrations/0001_initial.py
new file mode 100644
index 0000000..a4dc94a
--- /dev/null
+++ b/app/wireguard/migrations/0001_initial.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.14 on 2022-06-10 08:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='WireguardPublicKey',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('interface', models.CharField(max_length=128)),
+                ('key', models.CharField(max_length=128)),
+                ('created', models.DateTimeField(auto_now_add=True)),
+            ],
+        ),
+    ]
diff --git a/app/wireguard/migrations/__init__.py b/app/wireguard/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/wireguard/models.py b/app/wireguard/models.py
new file mode 100644
index 0000000..be23f34
--- /dev/null
+++ b/app/wireguard/models.py
@@ -0,0 +1,9 @@
+from django.db import models
+
+class WireguardPublicKey(models.Model):
+    interface = models.CharField(max_length=128)
+    key = models.CharField(max_length=128)
+    created = models.DateTimeField(auto_now_add=True)
+
+    def __str__(self):
+        return f"{self.interface}"
diff --git a/app/wireguard/templates/wireguard/wireguardpublickey_create.html b/app/wireguard/templates/wireguard/wireguardpublickey_create.html
new file mode 100644
index 0000000..40f0216
--- /dev/null
+++ b/app/wireguard/templates/wireguard/wireguardpublickey_create.html
@@ -0,0 +1,22 @@
+{% extends 'keys_home/base.html' %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+<nav class="nav-breadcrumb" aria-label="breadcrumb">
+<ol>
+  <li><a href="{% url 'home' %}">Home</a></li>
+  <li><a href="{% url 'wireguard-list' %}">Wireguard Keys</a></li>
+  <li aria-current="page">
+    <a href="{% url 'wireguard-create' %}">Add</a>
+  </li>
+</ol>
+</nav>
+
+<h2>Add Wireguard Public Key</h2>
+<form action="" method="post">
+  {% csrf_token %}
+  {{ form|crispy }}
+  <button class="btn btn-primary" type="submit">Add public key</button>
+</form>
+
+{% endblock %}
diff --git a/app/wireguard/templates/wireguard/wireguardpublickey_item.html b/app/wireguard/templates/wireguard/wireguardpublickey_item.html
new file mode 100644
index 0000000..acddf35
--- /dev/null
+++ b/app/wireguard/templates/wireguard/wireguardpublickey_item.html
@@ -0,0 +1,20 @@
+<li class="wireguardkey-item">
+  <h3>
+    <div>
+        <i class="fas fa-dragon" title="Interface"></i>
+        {{ publickey.interface }}
+      </a>
+    </div>
+  </h3>
+
+  <div class="wireguardkey-fingerprint">
+    <div class="wireguardkey-fingerprint-title">
+      <i class="fas fa-fingerprint"></i> Public key
+    </div>
+
+    <div class="wireguardkey-fingerprint-hash">
+      <input title="publickey" type="text" readonly value="{{publickey.key }}" />
+      <div><span>Base64</span></div>
+    </div>
+  </div>
+</li>
diff --git a/app/wireguard/templates/wireguard/wireguardpublickey_list.html b/app/wireguard/templates/wireguard/wireguardpublickey_list.html
new file mode 100644
index 0000000..cc8addc
--- /dev/null
+++ b/app/wireguard/templates/wireguard/wireguardpublickey_list.html
@@ -0,0 +1,33 @@
+{% extends 'keys_home/base.html' %}
+
+{% block content %}
+
+<nav class="nav-breadcrumb" aria-label="breadcrumb">
+<ol>
+  <li><a href="{% url 'home' %}">Home</a></li>
+  <li aria-current="page">
+    <a href="{% url 'wireguard-list' %}">Wireguard Keys</a>
+  </li>
+</ol>
+</nav>
+
+<h2 class="h-control">
+  <div>Wireguard Public Keys</div>
+  {% if perms.wireguard.add_wireguardpublickey %}
+  <a class="btn btn-outline-primary" href="{% url 'wireguard-create' %}">
+    <i class="fas fa-plus"></i>
+  </a>
+  {% endif %}
+</h2>
+
+
+{% if wireguardpublickey_list %}
+<ul class="list-unstyled">
+{% for publickey in wireguardpublickey_list %}
+  {% include 'wireguard/wireguardpublickey_item.html' with fingerprint_only=True %}
+{% endfor %}
+</ul>
+{% else %}
+<p>There are not public keys.</p>
+{% endif %}
+{% endblock %}
diff --git a/app/wireguard/tests.py b/app/wireguard/tests.py
new file mode 100644
index 0000000..2978de6
--- /dev/null
+++ b/app/wireguard/tests.py
@@ -0,0 +1,31 @@
+from django.test import TestCase
+from django.urls import reverse
+from django.contrib.auth.models import Group
+from guardian.shortcuts import assign_perm
+from . import models
+
+class KeyDecodeerror(TestCase):
+    """Check pages load even if the key cannot be decoded"""
+
+    def setUp(self):
+        self.toykey = models.SSHPublicKey.objects.create(
+            hostname="example.com",
+            keytype="",
+            key="no such thing",
+        )
+        any_user = Group.objects.get(name="any-user")
+        assign_perm('view_sshpublickey', any_user, self.toykey)
+
+    def test_detail(self):
+        """Check that the detail page loads successfully"""
+        response = self.client.get(
+            reverse('ssh-detail', args=[self.toykey.pk])
+        )
+        self.assertEqual(response.status_code, 200)
+
+    def test_list(self):
+        """Check that the list page loads successfully"""
+        response = self.client.get(
+            reverse('ssh-list')
+        )
+        self.assertEqual(response.status_code, 200)
diff --git a/app/wireguard/urls.py b/app/wireguard/urls.py
new file mode 100644
index 0000000..a161b6d
--- /dev/null
+++ b/app/wireguard/urls.py
@@ -0,0 +1,9 @@
+
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+    path("", views.PublicKeyListView.as_view(), name="wireguard-list"),
+    path("new", views.publickey_create, name="wireguard-create"),
+]
diff --git a/app/wireguard/views.py b/app/wireguard/views.py
new file mode 100644
index 0000000..c6f5799
--- /dev/null
+++ b/app/wireguard/views.py
@@ -0,0 +1,41 @@
+from django.shortcuts import render, reverse
+from django.views.generic import DetailView, ListView, CreateView
+from django.http import HttpResponseRedirect
+from django.contrib.auth.models import Group
+from guardian.mixins import PermissionRequiredMixin, PermissionListMixin
+from guardian.decorators import permission_required
+from guardian.shortcuts import assign_perm
+from . import models
+from . import forms
+
+
+class PublicKeyListView(PermissionListMixin, ListView):
+    model = models.WireguardPublicKey
+    permission_required = ['view_wireguardpublickey']
+
+
+@permission_required('ssh.add_wireguardpublickey')
+def publickey_create(request):
+    if request.method == 'POST':
+        form = forms.PublicKeyCreateForm(request.POST)
+        if form.is_valid():
+            pk = models.WireguardPublicKey()
+            pk.interface = form.cleaned_data['interface']
+            pk.key = form.cleaned_data['key']
+            pk.save()
+
+            assign_perm('view_wireguardpublickey', request.user, pk)
+            assign_perm('change_wireguardpublickey', request.user, pk)
+
+            if form.cleaned_data['public']:
+                any_user = Group.objects.get(name="any-user")
+                assign_perm('view_wireguardpublickey', any_user, pk)
+            return HttpResponseRedirect(reverse('wireguard-list'))
+
+    # if a GET (or any other method) we'll create a blank form
+    else:
+        form = forms.PublicKeyCreateForm()
+
+    return render(request,
+                  'wireguard/wireguardpublickey_create.html',
+                  {'form': form})
-- 
GitLab