]> granicus.if.org Git - pdns/commitdiff
Add new SOA-EDIT-API SOA editing setting
authorChristian Hofstaedtler <christian@hofstaedtler.name>
Tue, 8 Apr 2014 13:15:02 +0000 (15:15 +0200)
committerChristian Hofstaedtler <christian@hofstaedtler.name>
Tue, 8 Apr 2014 13:15:56 +0000 (15:15 +0200)
This triggers whenever a record is changed through the API.

pdns/dnsbackend.hh
pdns/dnsseckeeper.hh
pdns/serialtweaker.cc
pdns/ws-auth.cc
regression-tests.api/runtests.py
regression-tests.api/test_Zones.py

index 0bfbb45a1a7e4e5f436832ffa182efab405ad54a..620dba9feb272b25e9c4168d94ee7ea0a56afde2 100644 (file)
@@ -136,7 +136,25 @@ public:
   // the DNSSEC related (getDomainMetadata has broader uses too)
   virtual bool getAllDomainMetadata(const string& name, std::map<std::string, std::vector<std::string> >& meta) { return false; };
   virtual bool getDomainMetadata(const string& name, const std::string& kind, std::vector<std::string>& meta) { return false; }
+  virtual bool getDomainMetadataOne(const string& name, const std::string& kind, std::string& value)
+  {
+    std::vector<std::string> meta;
+    if (getDomainMetadata(name, kind, meta)) {
+      if(!meta.empty()) {
+        value = *meta.begin();
+        return true;
+      }
+    }
+    return false;
+  }
+
   virtual bool setDomainMetadata(const string& name, const std::string& kind, const std::vector<std::string>& meta) {return false;}
+  virtual bool setDomainMetadataOne(const string& name, const std::string& kind, const std::string& value)
+  {
+    const std::vector<std::string> meta(1, value);
+    return setDomainMetadata(name, kind, meta);
+  }
+
 
   virtual void getAllDomains(vector<DomainInfo> *domains, bool include_disabled=false) { }
 
index 06f927a6002fa9fa21f362a562a447617f6c935f..da9269a7611f18af1dfcc97defda14458da82590 100644 (file)
@@ -170,4 +170,5 @@ class DNSPacket;
 uint32_t calculateEditSOA(SOAData sd, const string& kind);
 uint32_t localtime_format_YYYYMMDDSS(time_t t, uint32_t seq);
 bool editSOA(DNSSECKeeper& dk, const string& qname, DNSPacket* dp);
+bool editSOARecord(DNSResourceRecord& rr, const string& kind);
 #endif
index e7e2afb8d0ddc4221a992c334077ff7a3eb7b058..e502bf045122dabf74892db73dd55ee1b2d33e70 100644 (file)
@@ -43,18 +43,22 @@ bool editSOA(DNSSECKeeper& dk, const string& qname, DNSPacket* dp)
     if(rr.qtype.getCode() == QType::SOA && pdns_iequals(rr.qname,qname)) {
       string kind;
       dk.getFromMeta(qname, "SOA-EDIT", kind);
-      if(kind.empty())
-        return false;
-      SOAData sd;
-      fillSOAData(rr.content, sd);
-      sd.serial = calculateEditSOA(sd, kind);
-      rr.content = serializeSOAData(sd);      
-      return true;
+      return editSOARecord(rr, kind);
     }
   }
   return false;
 }
 
+bool editSOARecord(DNSResourceRecord& rr, const string& kind) {
+  if(kind.empty())
+    return false;
+
+  SOAData sd;
+  fillSOAData(rr.content, sd);
+  sd.serial = calculateEditSOA(sd, kind);
+  rr.content = serializeSOAData(sd);
+  return true;
+}
 
 uint32_t calculateEditSOA(SOAData sd, const string& kind) {
   if(pdns_iequals(kind,"INCEPTION")) {
index c489c38e2cd856705f44633abe3e3a6fb5dbed40..ceecf7a4c601dc65212651f21fe612ff62c141c6 100644 (file)
@@ -39,6 +39,7 @@
 #include "rapidjson/writer.h"
 #include "ws-api.hh"
 #include "version.hh"
+#include "dnsseckeeper.hh"
 #include <iomanip>
 
 #ifdef HAVE_CONFIG_H
@@ -298,6 +299,9 @@ static void fillZone(const string& zonename, HttpResponse* resp) {
   doc.AddMember("name", di.zone.c_str(), doc.GetAllocator());
   doc.AddMember("type", "Zone", doc.GetAllocator());
   doc.AddMember("kind", di.getKindString(), doc.GetAllocator());
+  string soa_edit_api;
+  di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api);
+  doc.AddMember("soa_edit_api", soa_edit_api.c_str(), doc.GetAllocator());
   Value masters;
   masters.SetArray();
   BOOST_FOREACH(const string& master, di.masters) {
@@ -382,6 +386,10 @@ static void updateDomainSettingsFromDocument(const DomainInfo& di, const string&
 
   di.backend->setKind(zonename, DomainInfo::stringToKind(stringFromJson(document, "kind")));
   di.backend->setMaster(zonename, master);
+
+  if (document["soa_edit_api"].IsString()) {
+    di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].GetString());
+  }
 }
 
 static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
@@ -592,6 +600,10 @@ static void patchZone(HttpRequest* req, HttpResponse* resp) {
   di.backend->startTransaction(zonename);
 
   try {
+    string soa_edit_api_kind;
+    di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
+    bool soa_edit_done = false;
+
     for(SizeType rrsetIdx = 0; rrsetIdx < rrsets.Size(); ++rrsetIdx) {
       const Value& rrset = rrsets[rrsetIdx];
       string qname, changetype;
@@ -663,6 +675,10 @@ static void patchZone(HttpRequest* req, HttpResponse* resp) {
               new_ptrs.push_back(ptr);
             }
 
+            if (rr.qtype.getCode() == QType::SOA && pdns_iequals(rr.qname, zonename)) {
+              soa_edit_done = editSOARecord(rr, soa_edit_api_kind);
+            }
+
             new_records.push_back(rr);
           }
         }
@@ -703,6 +719,28 @@ static void patchZone(HttpRequest* req, HttpResponse* resp) {
       else
         throw ApiException("Changetype not understood");
     }
+
+    // edit SOA (if needed)
+    if (!soa_edit_api_kind.empty() && !soa_edit_done) {
+      SOAData sd;
+      if (!B.getSOA(zonename, sd))
+        throw ApiException("No SOA found for domain '"+zonename+"'");
+
+      DNSResourceRecord rr;
+      rr.qname = zonename;
+      rr.content = serializeSOAData(sd);
+      rr.qtype = "SOA";
+      rr.domain_id = di.id;
+      rr.auth = 1;
+      rr.ttl = sd.ttl;
+      rr.priority = 0;
+      editSOARecord(rr, soa_edit_api_kind);
+
+      if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
+        throw ApiException("Hosting backend does not support editing records.");
+      }
+    }
+
   } catch(...) {
     di.backend->abortTransaction();
     throw;
index ab187f194c17b4a2318f5bbc65d41cf795c9952e..f74ff4a06951e9db4a60ad9a1cad217beff1be8b 100755 (executable)
@@ -79,7 +79,7 @@ if daemon == 'authoritative':
         tf.seek(0, os.SEEK_SET)  # rewind
         subprocess.check_call(["sqlite3", SQLITE_DB], stdin=tf)
 
-    pdnscmd = ("../pdns/pdns_server --daemon=no --local-port=5300 --socket-dir=./ --no-shuffle --launch=gsqlite3 --gsqlite3-dnssec --send-root-referral --allow-2136-from=127.0.0.0/8 --experimental-rfc2136=yes --cache-ttl=0 --no-config --gsqlite3-database="+SQLITE_DB+" --experimental-json-interface=yes --webserver=yes --webserver-port="+WEBPORT+" --webserver-address=127.0.0.1 --query-logging  --webserver-password="+WEBPASSWORD).split()
+    pdnscmd = ("../pdns/pdns_server --daemon=no --local-port=5300 --socket-dir=./ --no-shuffle --launch=gsqlite3 --gsqlite3-dnssec --send-root-referral --allow-2136-from=127.0.0.0/8 --experimental-rfc2136=yes --cache-ttl=0 --no-config --gsqlite3-dnssec=on --gsqlite3-database="+SQLITE_DB+" --experimental-json-interface=yes --webserver=yes --webserver-port="+WEBPORT+" --webserver-address=127.0.0.1 --query-logging  --webserver-password="+WEBPASSWORD).split()
 
 else:
     conf_dir = 'rec-conf.d'
index 3ef90111df51f3bffa04e2df3c1139e9e9c31704..a0caed6b2871008b1720625906316237b4b5b0a4 100644 (file)
@@ -1,4 +1,5 @@
 import json
+import time
 import requests
 import unittest
 from test_helper import ApiTestCase, unique_zone_name, isAuth, isRecursor
@@ -45,7 +46,15 @@ class AuthZones(ApiTestCase):
 
     def test_CreateZone(self):
         payload, data = self.create_zone()
-        for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial'):
+        for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api'):
+            self.assertIn(k, data)
+            if k in payload:
+                self.assertEquals(data[k], payload[k])
+        self.assertEquals(data['comments'], [])
+
+    def test_CreateZoneWithSoaEditApi(self):
+        payload, data = self.create_zone(soa_edit_api='EPOCH')
+        for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api'):
             self.assertIn(k, data)
             if k in payload:
                 self.assertEquals(data[k], payload[k])
@@ -91,10 +100,11 @@ class AuthZones(ApiTestCase):
     def test_UpdateZone(self):
         payload, zone = self.create_zone()
         name = payload['name']
-        # update, set as Master
+        # update, set as Master and enable SOA-EDIT-API
         payload = {
             'kind': 'Master',
-            'masters': ['192.0.2.1','192.0.2.2']
+            'masters': ['192.0.2.1','192.0.2.2'],
+            'soa_edit_api': 'EPOCH'
         }
         r = self.session.put(
             self.url("/servers/localhost/zones/" + name),
@@ -105,9 +115,10 @@ class AuthZones(ApiTestCase):
         for k in payload.keys():
             self.assertIn(k, data)
             self.assertEquals(data[k], payload[k])
-        # update, back to Native
+        # update, back to Native and empty(off)
         payload = {
-            'kind': 'Native'
+            'kind': 'Native',
+            'soa_edit_api': ''
         }
         r = self.session.put(
             self.url("/servers/localhost/zones/" + name),
@@ -259,7 +270,8 @@ class AuthZones(ApiTestCase):
         self.assertEquals(recs, [])
 
     def test_ZoneDisableReenable(self):
-        payload, zone = self.create_zone()
+        # This also tests that SOA-EDIT-API works.
+        payload, zone = self.create_zone(soa_edit_api='EPOCH')
         name = payload['name']
         # disable zone by disabling SOA
         rrset = {
@@ -283,10 +295,16 @@ class AuthZones(ApiTestCase):
             data=json.dumps(payload),
             headers={'content-type': 'application/json'})
         self.assertSuccessJson(r)
-        # make sure it's still in zone list
+        # check SOA serial has been edited
+        print r.json()
+        soa_serial1 = [rec for rec in r.json()['records'] if rec['type'] == 'SOA'][0]['content'].split()[2]
+        self.assertNotEquals(soa_serial1, '1')
+        # make sure domain is still in zone list (disabled SOA!)
         r = self.session.get(self.url("/servers/localhost/zones"))
         domains = r.json()
         self.assertEquals(len([domain for domain in domains if domain['name'] == name]), 1)
+        # sleep 1sec to ensure the EPOCH value changes for the next request
+        time.sleep(1)
         # verify that modifying it still works
         rrset['records'][0]['disabled'] = False
         payload = {'rrsets': [rrset]}
@@ -295,6 +313,11 @@ class AuthZones(ApiTestCase):
             data=json.dumps(payload),
             headers={'content-type': 'application/json'})
         self.assertSuccessJson(r)
+        # check SOA serial has been edited again
+        print r.json()
+        soa_serial2 = [rec for rec in r.json()['records'] if rec['type'] == 'SOA'][0]['content'].split()[2]
+        self.assertNotEquals(soa_serial2, '1')
+        self.assertNotEquals(soa_serial2, soa_serial1)
 
     def test_ZoneRRUpdateQTypeMismatch(self):
         payload, zone = self.create_zone()