Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resurrect dedyn.io registrations #1014

Merged
merged 10 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions api/desecapi/serializers/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ def __init__(self, default):

def __call__(self, serializer_field):
is_many = getattr(serializer_field.root, "many", False)
if is_many:
partial = getattr(serializer_field.root, "partial", False)
if is_many or (serializer_field.root.instance and not partial):
serializer_field.fail("required")
if callable(self.default):
if getattr(self.default, "requires_context", False):
Expand Down Expand Up @@ -392,7 +393,6 @@ def __init__(self, *args, **kwargs):

def get_fields(self):
fields = super().get_fields()
fields["subname"].validators.append(validators.ReadOnlyOnUpdateValidator())
fields["type"].validators.append(validators.ReadOnlyOnUpdateValidator())
fields["ttl"].validators.append(MinValueValidator(limit_value=self.minimum_ttl))
return fields
Expand Down Expand Up @@ -447,6 +447,13 @@ def validate_records(self, value):
return value

def validate_subname(self, value):
# Needs to live here (instead of .subname.validators) because `allow_blank`
# prevents validators from running on subname="" (but this method here runs!)
if self.instance and value != self.instance.subname:
raise serializers.ValidationError(
validators.ReadOnlyOnUpdateValidator.message, code="read-only-on-update"
)

try:
dns.name.from_text(value, dns.name.from_text(self.domain.name))
except dns.name.NameTooLong:
Expand Down Expand Up @@ -508,14 +515,28 @@ def _validate_blocked_content(self, attrs, type_):
return attrs

def validate(self, attrs):
if "records" in attrs:
# on the RRsetDetail endpoint, the type is not in attrs
type_ = attrs.get("type") or self.instance.type
# on the RRsetDetail endpoint, the type is not in attrs
type_ = attrs.get("type") or self.instance.type

if "records" in attrs:
attrs = self._validate_canonical_presentation(attrs, type_)
attrs = self._validate_length(attrs)
attrs = self._validate_blocked_content(attrs, type_)

# Disallow modification of NS RRsets for locally registrable domains
# Deletion using records=[] is allowed, except at the apex
if (
type_ == "NS"
and self.domain.is_locally_registrable
and (
attrs.get("records", True)
or not attrs.get("subname", self.instance.subname)
)
):
raise serializers.ValidationError(
{"type": ["Cannot modify NS records for this domain."]}
peterthomassen marked this conversation as resolved.
Show resolved Hide resolved
)

return attrs

def exists(self, arg):
Expand Down
12 changes: 9 additions & 3 deletions api/desecapi/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,12 @@ def requests_desec_rr_sets_update(cls, name=None):
cls.request_pdns_zone_axfr(name=name),
]

def assertBadRequest(self, response, message, path=()):
self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
reduce(lambda obj, key: obj.__getitem__(key), path, response.data), message
)

def assertRRSet(
self, response_rr, domain=None, subname=None, records=None, type_=None, **kwargs
):
Expand Down Expand Up @@ -1337,9 +1343,9 @@ def _test_rr_sets(cls, subname=None, type_=None, records=None, ttl=None):

def setUp(self):
super().setUp()
# TODO this test does not cover "dyn" / auto delegation domains
self.my_empty_domain = self.create_domain(suffix="", owner=self.owner)
self.my_rr_set_domain = self.create_domain(suffix="", owner=self.owner)
suffix = self.AUTO_DELEGATION_DOMAINS if self.DYN else None
self.my_empty_domain = self.create_domain(owner=self.owner, suffix=suffix)
self.my_rr_set_domain = self.create_domain(owner=self.owner, suffix=suffix)
self.other_rr_set_domain = self.create_domain(suffix="")
for domain in [self.my_rr_set_domain, self.other_rr_set_domain]:
for subname, type_, records, ttl in self._test_rr_sets():
Expand Down
163 changes: 162 additions & 1 deletion api/desecapi/tests/test_rrsets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
from contextlib import nullcontext
from ipaddress import IPv4Network
import re
from itertools import product
from math import ceil, floor

Expand Down Expand Up @@ -1236,6 +1236,37 @@ def test_update_my_rr_sets(self):
)
self.assertStatus(response, status.HTTP_400_BAD_REQUEST)

def test_update_my_rr_sets_missing_subname(self):
for subname in ["", "test"]:
with self.assertNoRequestsBut():
data = {
"records": ["127.0.0.1"],
"ttl": 3630,
"type": "A",
}
self.assertBadRequest(
self.client.put_rr_set(
self.my_rr_set_domain.name, subname, "A", data
),
"This field is required.",
("subname", 0),
)

def test_update_my_rr_sets_wrong_subname(self):
for s1, s2 in [("", "test"), ("test", "")]:
with self.assertNoRequestsBut():
data = {
"records": ["127.0.0.1"],
"ttl": 3630,
"type": "A",
"subname": s1,
}
self.assertBadRequest(
self.client.put_rr_set(self.my_rr_set_domain.name, s2, "A", data),
"Can only be written on create.",
("subname", 0),
)

def test_update_my_rr_set_with_invalid_payload_type(self):
for subname in self.SUBNAMES:
data = [
Expand Down Expand Up @@ -1625,6 +1656,8 @@ def assertRequests(*, allowed):
class AuthenticatedRRSetLPSTestCase(AuthenticatedRRSetBaseTestCase):
DYN = True
peterthomassen marked this conversation as resolved.
Show resolved Hide resolved

ns_data = {"type": "NS", "records": ["ns.example."], "ttl": 3600}

def test_create_my_rr_sets_ip_block(self):
BlockedSubnet.from_ip("3.2.2.3").save()
response = self.client.post_rr_set(
Expand All @@ -1636,3 +1669,131 @@ def test_create_my_rr_sets_ip_block(self):
)
self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
self.assertIn("IP address 3.2.2.5 not allowed.", str(response.data))

def test_create_ns_rrset(self):
nils-wisiol marked this conversation as resolved.
Show resolved Hide resolved
for subname in ["", "sub"]:
data = dict(self.ns_data, subname=subname)
with self.assertNoRequestsBut():
self.assertBadRequest(
self.client.post_rr_set(
domain_name=self.my_empty_domain.name, **data
),
"Cannot modify NS records for this domain.",
("type", 0),
)

def test_update_ns_rrset(self):
for subname in ["", "sub"]:
data = dict(self.ns_data, subname=subname)
self.create_rr_set(
self.my_domain,
settings.DEFAULT_NS,
subname=subname,
type="NS",
ttl=3600,
)
for method in (self.client.patch_rr_set, self.client.put_rr_set):
with self.assertNoRequestsBut():
self.assertBadRequest(
method(self.my_domain.name, subname, "NS", data),
"Cannot modify NS records for this domain.",
("type", 0),
)

def test_delete_ns_rrset_apex(self):
data = dict(self.ns_data, records=[], subname="")
self.create_rr_set(
self.my_domain, settings.DEFAULT_NS, subname="", type="NS", ttl=3600
)
for method in (self.client.patch_rr_set, self.client.put_rr_set):
with self.assertNoRequestsBut():
self.assertBadRequest(
method(self.my_domain.name, "", "NS", data),
"Cannot modify NS records for this domain.",
("type", 0),
)
with self.assertNoRequestsBut():
response = self.client.delete_rr_set(self.my_domain.name, "", "NS")
self.assertStatus(response, status.HTTP_400_BAD_REQUEST)

def test_delete_ns_rrset_nonapex(self):
data = dict(self.ns_data, subname="sub", records=[])
for method in (self.client.patch_rr_set, self.client.put_rr_set):
self.create_rr_set(
self.my_domain, settings.DEFAULT_NS, subname="sub", type="NS", ttl=3600
)
with self.assertRequests(
self.requests_desec_rr_sets_update(name=self.my_domain.name)
):
response = method(self.my_domain.name, "sub", "NS", data)
peterthomassen marked this conversation as resolved.
Show resolved Hide resolved
self.assertStatus(response, status.HTTP_204_NO_CONTENT)
self.create_rr_set(
self.my_domain, settings.DEFAULT_NS, subname="sub", type="NS", ttl=3600
)
with self.assertRequests(
self.requests_desec_rr_sets_update(name=self.my_domain.name)
):
response = self.client.delete_rr_set(self.my_domain.name, "sub", "NS")
self.assertStatus(response, status.HTTP_204_NO_CONTENT)

def test_bulk_create_ns_rrset(self):
for subname in ["", "sub"]:
data = dict(self.ns_data, subname=subname)
for method in (
self.client.bulk_post_rr_sets,
self.client.bulk_patch_rr_sets,
self.client.bulk_put_rr_sets,
):
with self.assertNoRequestsBut():
self.assertBadRequest(
method(self.my_empty_domain.name, [data]),
"Cannot modify NS records for this domain.",
(0, "type", 0),
)

def test_bulk_update_ns_rrset(self):
for subname in ["", "sub"]:
data = dict(self.ns_data, subname=subname)
self.create_rr_set(
self.my_domain,
settings.DEFAULT_NS,
subname=subname,
type="NS",
ttl=3600,
)
for method in (
self.client.bulk_patch_rr_sets,
self.client.bulk_put_rr_sets,
):
with self.assertNoRequestsBut():
self.assertBadRequest(
method(self.my_domain.name, [data]),
"Cannot modify NS records for this domain.",
(0, "type", 0),
)

def test_bulk_delete_ns_rrset_apex(self):
data = dict(self.ns_data, subname="", records=[])
self.create_rr_set(
self.my_domain, settings.DEFAULT_NS, subname="", type="NS", ttl=3600
)
for method in (self.client.bulk_patch_rr_sets, self.client.bulk_put_rr_sets):
with self.assertNoRequestsBut():
self.assertBadRequest(
method(self.my_domain.name, [data]),
"Cannot modify NS records for this domain.",
(0, "type", 0),
)

def test_bulk_delete_ns_rrset_nonapex(self):
data = dict(self.ns_data, subname="sub", records=[])
for method in (self.client.bulk_patch_rr_sets, self.client.bulk_put_rr_sets):
self.create_rr_set(
self.my_domain, settings.DEFAULT_NS, subname="sub", type="NS", ttl=3600
)
with self.assertRequests(
self.requests_desec_rr_sets_update(name=self.my_domain.name)
):
response = method(self.my_domain.name, [data])
self.assertStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data, [])
6 changes: 5 additions & 1 deletion api/desecapi/views/records.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.http import Http404
from rest_framework import generics
from rest_framework.exceptions import PermissionDenied
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.permissions import IsAuthenticated, SAFE_METHODS

from desecapi import models, permissions
Expand Down Expand Up @@ -99,6 +99,10 @@ def update(self, request, *args, **kwargs):
return response

def perform_destroy(self, instance):
# Disallow modification of apex NS RRset for locally registrable domains
if instance.type == "NS" and self.domain.is_locally_registrable:
if instance.subname == "":
raise ValidationError("Cannot modify NS records for this domain.")
peterthomassen marked this conversation as resolved.
Show resolved Hide resolved
with PDNSChangeTracker():
super().perform_destroy(instance)

Expand Down
4 changes: 2 additions & 2 deletions api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ captcha~=0.6.0
celery~=5.4.0
coverage~=7.6.10
cryptography~=44.0.0
Django~=5.1.4
Django~=5.1.5
django-cors-headers~=4.6.0
djangorestframework~=3.14.0
django-celery-email~=3.0.0
Expand All @@ -11,7 +11,7 @@ django-pgtrigger~=4.13.3
django-prometheus~=2.3.1
dnspython~=2.7.0
pyotp~=2.9.0
psycopg[binary]~=3.2.3
psycopg[binary]~=3.2.4
psl-dns~=1.1.1
pylibmc~=1.6.3
pyyaml~=6.0.2
Expand Down
8 changes: 8 additions & 0 deletions docs/auth/account.rst
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ A JSON object representing your user account will be returned::

{
"created": "2019-10-16T18:09:17.715702Z",
"domains_under_management": 3,
"email": "youremailaddress@example.com",
"id": "9ab16e5c-805d-4ab1-9030-af3f5a541d47",
"limit_domains": 15,
Expand All @@ -219,6 +220,13 @@ Field details:

Registration timestamp.

``domains_under_management``
:Access mode: read-only

Number of domains this user can manage, including domains listed in
:ref:`token scoping policies` of :ref:`user-override` tokens owned by this
account.

``email``
:Access mode: read-only

Expand Down
2 changes: 2 additions & 0 deletions docs/auth/tokens.rst
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@ During deletion of tokens, users, or domains, policies are cleaned up
automatically.


.. _`user-override`:

User Override
`````````````
One user can authorize another such that the latter can use their token to
Expand Down
5 changes: 0 additions & 5 deletions www/conf/conf.d/ssl.conf.var → www/conf/conf.d/ssl.conf
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,3 @@ ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;

resolver 127.0.0.11; # OCSP request needs DNS resolution. Use Docker's embedded DNS, forwarded to host.
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate ${CERT_PATH}desec.${DESECSTACK_DOMAIN}.cer;
2 changes: 1 addition & 1 deletion www/conf/envreplace.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ then
export PROD_ONLY='#'
fi

for file in /etc/nginx/sites-available/*.var /etc/nginx/conf.d/*.var; do
for file in /etc/nginx/sites-available/*.var; do
# we only replace occurrences of the variables specified below as first argument
(envsubst '$DESECSTACK_IPV4_REAR_PREFIX16' |
envsubst '$DESECSTACK_DOMAIN' |
Expand Down
1 change: 1 addition & 0 deletions www/conf/sites-available/90-desec.api.location.var
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ location /api/ {
etag off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
include uwsgi_params;
uwsgi_param HTTP_HOST $host;
uwsgi_pass desecapi;

location /api/v1/serials/ {
Expand Down
Loading