From 04ae79e8f5d996e328ff45027dfbbe20980e077b Mon Sep 17 00:00:00 2001 From: Kees Monshouwer Date: Sun, 7 Nov 2021 16:07:10 +0100 Subject: [PATCH] auth: api, check qtype location. Some types only live apex and some are not allowed (or useful) there. --- pdns/ws-auth.cc | 15 ++++++-- regression-tests.api/test_Zones.py | 55 +++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index b1a39a86de..600acb120c 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -58,6 +58,8 @@ static void patchZone(UeberBackend& B, HttpRequest* req, HttpResponse* resp); static const std::set onlyOneEntryTypes = { QType::CNAME, QType::DNAME, QType::SOA }; // QTypes that MUST NOT be used with any other QType on the same name. static const std::set exclusiveEntryTypes = { QType::CNAME }; +// QTypes that MUST be at apex. +static const std::set atApexTypes = {QType::SOA}; AuthWebServer::AuthWebServer() : d_start(time(nullptr)), @@ -1414,7 +1416,8 @@ static void gatherRecordsFromZone(const std::string& zonestring, vector& records) { +static void checkNewRecords(vector& records, const DNSName& zone) +{ sort(records.begin(), records.end(), [](const DNSResourceRecord& rec_a, const DNSResourceRecord& rec_b) -> bool { /* we need _strict_ weak ordering */ @@ -1437,6 +1440,12 @@ static void checkNewRecords(vector& records) { } } + if (rec.qname != zone) { + if (atApexTypes.count(rec.qtype.getCode()) != 0) { + throw ApiException("Record " + rec.qname.toString() + " IN " + rec.qtype.toString() + " is only allowed at apex"); + } + } + // Check if the DNSNames that should be hostnames, are hostnames try { checkHostnameCorrectness(rec); @@ -1704,7 +1713,7 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp) { } } - checkNewRecords(new_records); + checkNewRecords(new_records, zonename); if (boolFromJson(document, "dnssec", false)) { checkDefaultDNSSECAlgos(); @@ -2032,7 +2041,7 @@ static void patchZone(UeberBackend& B, HttpRequest* req, HttpResponse* resp) { soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind); } } - checkNewRecords(new_records); + checkNewRecords(new_records, zonename); } if (replace_comments) { diff --git a/regression-tests.api/test_Zones.py b/regression-tests.api/test_Zones.py index ab5a7c1197..ca9e581cf2 100644 --- a/regression-tests.api/test_Zones.py +++ b/regression-tests.api/test_Zones.py @@ -1409,14 +1409,14 @@ $ORIGIN %NAME% self.assertIn('Conflicts with pre-existing RRset', r.json()['error']) @parameterized.expand([ - ('SOA', ['ns1.example.org. test@example.org. 10 10800 3600 604800 3600', 'ns2.example.org. test@example.org. 10 10800 3600 604800 3600']), - ('CNAME', ['01.example.org.', '02.example.org.']), + ('', 'SOA', ['ns1.example.org. test@example.org. 10 10800 3600 604800 3600', 'ns2.example.org. test@example.org. 10 10800 3600 604800 3600']), + ('sub.', 'CNAME', ['01.example.org.', '02.example.org.']), ]) - def test_rrset_single_qtypes(self, qtype, contents): + def test_rrset_single_qtypes(self, label, qtype, contents): name, payload, zone = self.create_zone() rrset = { 'changetype': 'replace', - 'name': 'sub.'+name, + 'name': label + name, 'type': qtype, 'ttl': 3600, 'records': [ @@ -1468,6 +1468,53 @@ $ORIGIN %NAME% headers={'content-type': 'application/json'}) self.assert_success(r) # user should be able to create DNAME at APEX as per RFC 6672 section 2.3 + @parameterized.expand([ + ('SOA', 'ns1.example.org. test@example.org. 10 10800 3600 604800 1800'), + ]) + def test_only_at_apex(self, qtype, content): + name, payload, zone = self.create_zone(soa_edit_api='') + rrset = { + 'changetype': 'replace', + 'name': name, + 'type': qtype, + 'ttl': 3600, + 'records': [ + { + "content": content, + "disabled": False + }, + ] + } + payload = {'rrsets': [rrset]} + r = self.session.patch( + self.url("/api/v1/servers/localhost/zones/" + name), + data=json.dumps(payload), + headers={'content-type': 'application/json'}) + self.assert_success(r) + # verify that the new record is there + data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json() + self.assertEqual(get_rrset(data, name, qtype)['records'], rrset['records']) + + rrset = { + 'changetype': 'replace', + 'name': 'sub.' + name, + 'type': qtype, + 'ttl': 3600, + 'records': [ + { + "content": content, + "disabled": False + }, + ] + } + payload = {'rrsets': [rrset]} + r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload), + headers={'content-type': 'application/json'}) + self.assertEqual(r.status_code, 422) + self.assertIn('only allowed at apex', r.json()['error']) + data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json() + self.assertIsNone(get_rrset(data, 'sub.' + name, qtype)) + def test_rr_svcb(self): name, payload, zone = self.create_zone() rrset = { -- 2.47.2