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

index b1a39a86ded64a1a34f15302d61ab4a1bb7db424..600acb120c639ce219110e3c235488bcff04ff08 100644 (file)
@@ -58,6 +58,8 @@ 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};
 
 AuthWebServer::AuthWebServer() :
   d_start(time(nullptr)),
@@ -1414,7 +1416,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 */
@@ -1437,6 +1440,12 @@ static void checkNewRecords(vector<DNSResourceRecord>& 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) {
index ab5a7c11972703a1b671e60441e5b773336ccc12..ca9e581cf2b1330a4c5c796c7c2b8edbbbb74494 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,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 = {