diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d7563a5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +FROM docker.io/python:3.12.10-alpine3.21 + +ENV \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + # python: + PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PYTHONDONTWRITEBYTECODE=1 + +RUN apk add --update --no-cache zlib-dev libxml2-dev libxslt-dev libmemcached-dev \ + build-base gcc make alpine-sdk musl-dev postgresql-dev openldap-dev \ + py3-distutils-extra py3-distutils-extra-pyc shadow python3-dev linux-headers pcre-dev + +ENV VIRTUAL_ENV=/venv + +ENV DJANGO_CONFIGURATION=production + +ENV \ + # pip: + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 + + +RUN pip install --upgrade pip wheel +RUN python3 -m venv $VIRTUAL_ENV + +COPY requirements.txt . +RUN . /venv/bin/activate && pip install -r requirements.txt + +RUN mkdir -p /app +COPY ./ /app/ + +WORKDIR /app + +RUN . /venv/bin/activate && \ + DJANGO_SECRET_KEY=dummy \ + DJANGO_STATIC_ROOT=/app/static ./manage.py collectstatic --no-input + +RUN groupadd --gid 1001 app \ + && useradd --home-dir /app --shell /bin/bash --gid app --uid 1001 app + +RUN chown -R app:app ./ + +USER app + +ENV PATH="/venv/bin:$PATH" + diff --git a/ipv6ula/forms.py b/ipv6ula/forms.py index 0800a9f..400f21c 100644 --- a/ipv6ula/forms.py +++ b/ipv6ula/forms.py @@ -1,6 +1,8 @@ from django import forms - +import ipaddress from .models import validate_ula_prefix, ULA +from django.core.exceptions import ValidationError +import logging class ULAForm(forms.ModelForm): class Meta: @@ -17,3 +19,67 @@ class ULAGenerateForm(forms.ModelForm): class Meta: model = ULA fields = [ 'name', 'organization', 'website' ] + +class ULAUpdateForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + self.owner = kwargs.pop('owner', None) + super().__init__(*args, **kwargs) + + class Meta: + model = ULA + fields = [ 'prefix', 'name', 'organization', 'website' ] + + def clean_prefix(self): + prefix = self.cleaned_data.get("prefix") + if not prefix or not self.owner: + return prefix + + net_str = f"{prefix}/48" + net = ipaddress.IPv6Network(net_str, strict=False) + normalized_prefix = str(net[0]) + self.cleaned_data['prefix'] = normalized_prefix + + qs = ULA.objects.filter(prefix=normalized_prefix) + if self.instance.pk: + qs = qs.exclude(pk=self.instance.pk) + + if qs.exists(): + existing = qs.first() + if existing.owner != self.owner: + raise ValidationError("This prefix is already registered by another user.") + else: + self.instance = existing + return normalized_prefix + + +class ULADeleteForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + self.owner = kwargs.pop('owner', None) + super().__init__(*args, **kwargs) + + class Meta: + model = ULA + fields = [ 'prefix' ] + + def clean_prefix(self): + prefix = self.cleaned_data['prefix'] + + if not prefix or not self.owner: + return prefix + + net_str = f"{prefix}/48" + net = ipaddress.IPv6Network(net_str, strict=False) + normalized_prefix = str(net[0]) + self.cleaned_data['prefix'] = normalized_prefix + + try: + self.instance = ULA.objects.get(prefix=normalized_prefix, owner=self.owner) + except ULA.DoesNotExist: + raise ValidationError("No ULA with this prefix was found for the current user.") + + return normalized_prefix + + def delete(self): + if self.instance: + print(f"Deleting: {self.instance}") + self.instance.delete() diff --git a/ipv6ula/models.py b/ipv6ula/models.py index d7232ed..a91b964 100644 --- a/ipv6ula/models.py +++ b/ipv6ula/models.py @@ -31,7 +31,6 @@ def validate_ula_prefix(prefix): params = {'prefix': net } ) - class User(AbstractUser): pass @@ -41,7 +40,7 @@ class ULA(models.Model): on_delete=models.CASCADE ) - prefix = models.GenericIPAddressField(protocol='IPv6', unique=True, validators=[validate_ula_prefix]) + prefix = models.GenericIPAddressField(protocol='IPv6', unique=True) name = models.CharField(max_length=256) organization = models.CharField(max_length=256) website = models.URLField() @@ -54,7 +53,6 @@ class ULA(models.Model): net_str = f"{self.prefix}/48" net = ipaddress.IPv6Network(net_str, strict=False) self.prefix = str(net[0]) - super().save(*args, **kwargs) def __str__(self): diff --git a/ipv6ula/settings.py b/ipv6ula/settings.py index ab27519..12a7d55 100644 --- a/ipv6ula/settings.py +++ b/ipv6ula/settings.py @@ -140,7 +140,7 @@ USE_TZ = True STATIC_URL = '/static/' STATICFILES_DIRS = [ ] -STATIC_ROOT = "/home/app/app/static" +STATIC_ROOT = "/app/static" AUTHENTICATION_BACKENDS = [ diff --git a/ipv6ula/templates/ipv6ula/base.html b/ipv6ula/templates/ipv6ula/base.html index a005058..d9d6fb2 100644 --- a/ipv6ula/templates/ipv6ula/base.html +++ b/ipv6ula/templates/ipv6ula/base.html @@ -33,6 +33,14 @@ Submit existing prefix +
A new random prefix will be generated for you.
+ {% elif method == 'update' %} ++ You update your existing prefix. +
+ {% elif method == 'delete' %} ++ You delete your existing prefix. +
{% endif %}diff --git a/ipv6ula/urls.py b/ipv6ula/urls.py index eebc776..1c0af23 100644 --- a/ipv6ula/urls.py +++ b/ipv6ula/urls.py @@ -28,4 +28,6 @@ urlpatterns = [ path('login/', views.LoginView.as_view(), name='login'), path('logout/', views.logout_view, name='logout'), path('admin/', admin.site.urls), + path('update/', views.UpdateView.as_view(), name='update'), + path('delete/', views.DeleteView.as_view(), name='delete'), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/ipv6ula/views.py b/ipv6ula/views.py index 14cbdee..e284013 100644 --- a/ipv6ula/views.py +++ b/ipv6ula/views.py @@ -10,6 +10,9 @@ from django.contrib.auth import logout from django.shortcuts import redirect import ipaddress +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy +from django.contrib.messages import success from django.conf import settings @@ -73,6 +76,52 @@ class SubmitView(GenerateSubmitView): class LoginView(auth_views.LoginView): template_name = 'ipv6ula/login.html' +class UpdateView(GenerateSubmitView): + form_class = ULAUpdateForm + gen_method = "update" + success_message = "%(the_prefix)s/48 was updated successfully" + + def get_success_message(self, cleaned_data): + return self.success_message % { + "the_prefix": cleaned_data.get("prefix", "(unknown)") + } + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['owner'] = self.request.user + return kwargs + + def form_valid(self, form): + prefix = form.cleaned_data["prefix"] + form.instance.owner = self.request.user + if not ULA.objects.filter(prefix=prefix, owner=self.request.user).exists(): + form.add_error('prefix', 'This prefix is not yours') + return self.form_invalid(form) + return super().form_valid(form) + +class DeleteView(GenerateSubmitView): + form_class = ULADeleteForm + gen_method = "delete" + success_message = "%(the_prefix)s/48 was deleted successfully" + success_url = reverse_lazy("index") + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['owner'] = self.request.user + return kwargs + + def form_valid(self, form): + prefix = form.cleaned_data["prefix"] + try: + instance = ULA.objects.get(prefix=prefix, owner=self.request.user) + instance.delete() + self.object = instance + except ULA.DoesNotExist: + form.add_error("prefix", "The prefix does not exist or does not belong to you.") + return self.form_invalid(form) + success(self.request, self.get_success_message(form.cleaned_data)) + return HttpResponseRedirect(self.get_success_url()) + def logout_view(request): logout(request) return redirect("/") diff --git a/requirements.txt b/requirements.txt index 29cce86..8d4dd79 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ django==3.1.4 django-auth-ldap psycopg2 django-crispy-forms +setuptools +uwsgi diff --git a/uwsgiconfig/app.ini b/uwsgiconfig/app.ini new file mode 100644 index 0000000..7d9a156 --- /dev/null +++ b/uwsgiconfig/app.ini @@ -0,0 +1,12 @@ +[uwsgi] +socket = /app/uwsgi/uwsgi.sock +chdir = /app +venv = /venv +wsgi-file = ipv6ula/wsgi.py +processes = 4 +threads = 2 +chmod-socket = 666 +vacuum = true +plugins = python3 +uid = app +gid = app