From d29d5db7b0f0e4f690b7b20f5b90f38edd95327d Mon Sep 17 00:00:00 2001 From: Christian Hofstaedtler Date: Tue, 8 Apr 2014 15:15:02 +0200 Subject: [PATCH] Add new SOA-EDIT-API SOA editing setting This triggers whenever a record is changed through the API. --- pdns/dnsbackend.hh | 18 ++++++++++++++ pdns/dnsseckeeper.hh | 1 + pdns/serialtweaker.cc | 18 ++++++++------ pdns/ws-auth.cc | 38 ++++++++++++++++++++++++++++++ regression-tests.api/runtests.py | 2 +- regression-tests.api/test_Zones.py | 37 +++++++++++++++++++++++------ 6 files changed, 99 insertions(+), 15 deletions(-) diff --git a/pdns/dnsbackend.hh b/pdns/dnsbackend.hh index 0bfbb45a1a..620dba9feb 100644 --- a/pdns/dnsbackend.hh +++ b/pdns/dnsbackend.hh @@ -136,7 +136,25 @@ public: // the DNSSEC related (getDomainMetadata has broader uses too) virtual bool getAllDomainMetadata(const string& name, std::map >& meta) { return false; }; virtual bool getDomainMetadata(const string& name, const std::string& kind, std::vector& meta) { return false; } + virtual bool getDomainMetadataOne(const string& name, const std::string& kind, std::string& value) + { + std::vector 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& meta) {return false;} + virtual bool setDomainMetadataOne(const string& name, const std::string& kind, const std::string& value) + { + const std::vector meta(1, value); + return setDomainMetadata(name, kind, meta); + } + virtual void getAllDomains(vector *domains, bool include_disabled=false) { } diff --git a/pdns/dnsseckeeper.hh b/pdns/dnsseckeeper.hh index 06f927a600..da9269a761 100644 --- a/pdns/dnsseckeeper.hh +++ b/pdns/dnsseckeeper.hh @@ -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 diff --git a/pdns/serialtweaker.cc b/pdns/serialtweaker.cc index e7e2afb8d0..e502bf0451 100644 --- a/pdns/serialtweaker.cc +++ b/pdns/serialtweaker.cc @@ -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")) { diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index c489c38e2c..ceecf7a4c6 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -39,6 +39,7 @@ #include "rapidjson/writer.h" #include "ws-api.hh" #include "version.hh" +#include "dnsseckeeper.hh" #include #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(1, rr))) { + throw ApiException("Hosting backend does not support editing records."); + } + } + } catch(...) { di.backend->abortTransaction(); throw; diff --git a/regression-tests.api/runtests.py b/regression-tests.api/runtests.py index ab187f194c..f74ff4a069 100755 --- a/regression-tests.api/runtests.py +++ b/regression-tests.api/runtests.py @@ -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' diff --git a/regression-tests.api/test_Zones.py b/regression-tests.api/test_Zones.py index 3ef90111df..a0caed6b28 100644 --- a/regression-tests.api/test_Zones.py +++ b/regression-tests.api/test_Zones.py @@ -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() -- 2.47.2