diff --git a/.gitignore b/.gitignore index 71fe82ac49a540cf810a901a45a1c318025f7fa5..80c937db63ab263c4e838b12a6025ce160770f61 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 a408d755dc32c42863cec4a18437ddd5c4279369..6a5a03a51a8b3ce42daddd2e580e9a5833a0138d 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 dea577d3d143706f64f8a442d389ce67a6f8ebd8..859a857ad4d75042ed0968288e7dfcaa1cd0ec71 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 3b50237254cbcf9c08b73c10defd3809c260f5ea..8b8ee9c0984c46e6be1d6d7784b98096b265a898 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 e2fd7b3f7a0751a793d0ceac57c3df1cb5bf2421..317efa893ab7ba1875165960bd063e5af7006e59 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 17756a63c554bf9556ef3a03b695621bb521fdab..3c0348fa5696fa8eb343d8eca75cee842cc88658 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/wireguard/admin.py b/app/wireguard/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..09b49fdda78c5ccd6d92d71982074f049f585e7b --- /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 0000000000000000000000000000000000000000..84ea2f5d755257488cb08c8d728587c83e1b872e --- /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 0000000000000000000000000000000000000000..a011c727549a7376dbc2882985eddcb487db6bf3 --- /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 0000000000000000000000000000000000000000..a4dc94a97c0541377f008d19e727603ec01014de --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/wireguard/models.py b/app/wireguard/models.py new file mode 100644 index 0000000000000000000000000000000000000000..be23f34f2adef3d4d2cde449ce10df8f4737430d --- /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 0000000000000000000000000000000000000000..40f021610c56171bfb62411bfc664c681a6fee9a --- /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 0000000000000000000000000000000000000000..acddf35400374cb0b059efa7c76c6eb425baf041 --- /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 0000000000000000000000000000000000000000..cc8addc595ac2c10b735e9379c81ec8cc6ec60b8 --- /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 0000000000000000000000000000000000000000..2978de64290c690e7cc08ae478e562c1546b4b49 --- /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 0000000000000000000000000000000000000000..a161b6d7d5c7f16656489073550dd32f87cb41c3 --- /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 0000000000000000000000000000000000000000..c6f5799cec54aa4f3e6f5961c2450039070cc001 --- /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})