Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 5.2.11 on 2026-02-25 16:40

import django.contrib.postgres.operations
from django.db import migrations, models


class Migration(migrations.Migration):
replaces = [
("desecapi", "0045_rr_unique_record_in_rrset"),
("desecapi", "0046_remove_rr_unique_record_in_rrset_and_more"),
]

dependencies = [
("desecapi", "0044_alter_captcha_created_alter_domain_renewal_state_and_more"),
]

operations = [
django.contrib.postgres.operations.CreateExtension(
name="pgcrypto",
),
migrations.AddConstraint(
model_name="rr",
constraint=models.UniqueConstraint(
models.F("rrset"),
models.Func(
models.F("content"), models.Value("sha256"), function="digest"
),
name="unique_record_in_rrset",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.2.11 on 2026-02-25 16:35

from django.contrib.postgres.operations import CreateExtension
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("desecapi", "0045_rr_unique_record_in_rrset"),
]

operations = [
migrations.RemoveConstraint(
model_name="rr",
name="unique_record_in_rrset",
),
CreateExtension("pgcrypto"),
migrations.AddConstraint(
model_name="rr",
constraint=models.UniqueConstraint(
models.F("rrset"),
models.Func(
models.F("content"), models.Value("sha256"), function="digest"
),
name="unique_record_in_rrset",
),
),
]
8 changes: 5 additions & 3 deletions api/desecapi/models/domains.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,11 @@ def keys(self):
if not self._keys:
self._keys = [{**key, "managed": True} for key in pdns.get_keys(self)]
try:
unmanaged_keys = self.rrset_set.get(
subname="", type="DNSKEY"
).records.all()
unmanaged_keys = (
self.rrset_set.get(subname="", type="DNSKEY")
.records.order_by("content")
.all()
)
except RRset.DoesNotExist:
pass
else:
Expand Down
13 changes: 4 additions & 9 deletions api/desecapi/models/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.core import validators
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models
from django.db.models import Manager
from django.db.models import F, Func, Manager, Value
from django.db.models.expressions import RawSQL
from django_prometheus.models import ExportModelOperationsMixin
from dns import rdataclass, rdatatype
Expand Down Expand Up @@ -267,15 +267,10 @@ class RR(ExportModelOperationsMixin("RR"), models.Model):

class Meta:
constraints = [
# not using UniqueConstraint as its btree's entry size is limited to 1/3 page size;
# however, some records (OPENPGPKEY, PQC keys/signatures?) may be larger
# Alternatively, could use a unique constraint on hash(content), but Django lacks API
ExclusionConstraint(
models.UniqueConstraint(
F("rrset"),
Func(F("content"), Value("sha256"), function="digest"),
name="unique_record_in_rrset",
expressions=[
("rrset", RangeOperators.EQUAL),
("content", RangeOperators.EQUAL),
],
),
]

Expand Down
11 changes: 7 additions & 4 deletions api/desecapi/tests/test_rrsets.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from base64 import b64encode
from contextlib import nullcontext
from ipaddress import IPv4Network
from itertools import product
Expand All @@ -8,7 +9,7 @@
from django.core.exceptions import ValidationError
from django.core.management import call_command
from django.db import IntegrityError
from psycopg.errors import ExclusionViolation
from psycopg.errors import UniqueViolation
from rest_framework import status

from desecapi.models import BlockedSubnet, Domain, RR, RRset
Expand All @@ -19,15 +20,17 @@
class UnauthenticatedRRSetTestCase(DesecTestCase):
def test_unique_record_in_rrset(self):
domain = self.create_domain()
# This results in 39708-octet rdata, the longest one currently in production
openpgpkey_rdata = b64encode(b"a" * 29781)
with self.assertRaises(IntegrityError) as cm:
RRset.objects.create(
domain=domain,
subname="foo",
type="A",
type="OPENPGPKEY",
ttl=3600,
contents=["1.2.3.4"] * 2,
contents=[openpgpkey_rdata.decode()] * 2,
)
self.assertIsInstance(cm.exception.__cause__, ExclusionViolation)
self.assertIsInstance(cm.exception.__cause__, UniqueViolation)

def test_unauthorized_access(self):
url = self.reverse("v1:rrsets", name="example.com")
Expand Down
8 changes: 4 additions & 4 deletions api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
captcha~=0.7.1
celery~=5.5.3
coverage~=7.13.1
cryptography~=46.0.3
celery~=5.6.2
coverage~=7.13.2
cryptography~=46.0.4
Django~=5.2.7
django-cors-headers~=4.9.0
djangorestframework~=3.16.1
django-celery-email~=3.0.0
django-netfields~=1.3.2
django-pgtrigger~=4.15.4
django-pgtrigger~=4.17.0
django-prometheus~=2.4.1
dnspython~=2.8.0
pyotp~=2.9.0
Expand Down
1 change: 1 addition & 0 deletions www/webapp/src/views/HomePage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ export default {
host: 'dfw-1.a.desec.io',
left: '13%',
top: '34%',
adopted_by: 'Hanobox e.K. (50%)',
},
{
name: 'Hong Kong (ns1.desec.io)',
Expand Down
Loading