From: Kees Monshouwer Date: Sun, 7 Nov 2021 15:07:10 +0000 (+0100) Subject: auth: api, check qtype location. Some types only live apex and some are not allowed... X-Git-Tag: rec-4.6.0-beta1~3^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2eb206ecda89f267c3adfe1af6d46f1582d05ce6;p=thirdparty%2Fpdns.git auth: api, check qtype location. Some types only live apex and some are not allowed (or useful) there. --- diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 118d34c462..c63b0cf8b6 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -58,6 +58,10 @@ 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, QType::DNSKEY, QType::CDNSKEY, QType::CDS}; +// QTypes that are NOT allowed at apex. +static const std::set nonApexTypes = {QType::DS}; AuthWebServer::AuthWebServer() : d_start(time(nullptr)), @@ -1415,7 +1419,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 */ @@ -1438,6 +1443,15 @@ static void checkNewRecords(vector& records) { } } + if (rec.qname == zone) { + if (nonApexTypes.count(rec.qtype.getCode()) != 0) { + throw ApiException("Record " + rec.qname.toString() + " IN " + rec.qtype.toString() + " is not allowed at apex"); + } + } + else 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); @@ -1705,7 +1719,7 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp) { } } - checkNewRecords(new_records); + checkNewRecords(new_records, zonename); if (boolFromJson(document, "dnssec", false)) { checkDefaultDNSSECAlgos(); @@ -2033,7 +2047,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..d163185f83 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,103 @@ $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'), + ('DNSKEY', '257 3 8 AwEAAb/+pXOZWYQ8mv9WM5dFva8WU9jcIUdDuEjldbyfnkQ/xlrJC5zAEfhYhrea3SmIPmMTDimLqbh3/4SMTNPTUF+9+U1vpNfIRTFadqsmuU9Fddz3JqCcYwEpWbReg6DJOeyu+9oBoIQkPxFyLtIXEPGlQzrynKubn04Cx83I6NfzDTraJT3jLHKeW5PVc1ifqKzHz5TXdHHTA7NkJAa0sPcZCoNE1LpnJI/wcUpRUiuQhoLFeT1E432GuPuZ7y+agElGj0NnBxEgnHrhrnZWUbULpRa/il+Cr5Taj988HqX9Xdm6FjcP4Lbuds/44U7U8du224Q8jTrZ57Yvj4VDQKc='), + ('CDNSKEY', '0 3 0 AA=='), + ('CDS', '0 0 0 00'), + ]) + 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)) + + @parameterized.expand([ + ('DS', '44030 8 2 d4c3d5552b8679faeebc317e5f048b614b2e5f607dc57f1553182d49ab2179f7'), + ]) + def test_not_allowed_at_apex(self, qtype, content): + name, payload, zone = self.create_zone() + 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.assertEqual(r.status_code, 422) + self.assertIn('not 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)) + + 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.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, 'sub.' + name, qtype)['records'], rrset['records']) + def test_rr_svcb(self): name, payload, zone = self.create_zone() rrset = {