]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
auth: api, check qtype location. Some types only live apex and some are not allowed...
authorKees Monshouwer <mind04@monshouwer.org>
Sun, 7 Nov 2021 15:07:10 +0000 (16:07 +0100)
committermind04 <mind04@monshouwer.org>
Mon, 8 Nov 2021 10:55:21 +0000 (11:55 +0100)
pdns/ws-auth.cc
regression-tests.api/test_Zones.py

index 118d34c46284c6572f3563645d6c439acbc02bb7..c63b0cf8b6f5510b6b702bf2e6b591ad3c4dccae 100644 (file)
@@ -58,6 +58,10 @@ static void patchZone(UeberBackend& B, HttpRequest* req, HttpResponse* resp);
 static const std::set<uint16_t> 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<uint16_t> exclusiveEntryTypes = { QType::CNAME };
+// QTypes that MUST be at apex.
+static const std::set<uint16_t> atApexTypes = {QType::SOA, QType::DNSKEY, QType::CDNSKEY, QType::CDS};
+// QTypes that are NOT allowed at apex.
+static const std::set<uint16_t> nonApexTypes = {QType::DS};
 
 AuthWebServer::AuthWebServer() :
   d_start(time(nullptr)),
@@ -1415,7 +1419,8 @@ static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResou
  *   *) no duplicates for QTypes that can only be present once per RRset
  *   *) hostnames are hostnames
  */
-static void checkNewRecords(vector<DNSResourceRecord>& records) {
+static void checkNewRecords(vector<DNSResourceRecord>& 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<DNSResourceRecord>& 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) {
index ab5a7c11972703a1b671e60441e5b773336ccc12..d163185f835bae151b5668aa3ab80a813cb9fabf 100644 (file)
@@ -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 = {