Skip to content

Commit

Permalink
API Auth: replace zone contents
Browse files Browse the repository at this point in the history
In PUT on a specific zone it is now possible to set "rrsets", like in
POST.
  • Loading branch information
zeha committed Aug 11, 2023
1 parent c43b2d2 commit 70f1db7
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 3 deletions.
72 changes: 69 additions & 3 deletions pdns/ws-auth.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1899,10 +1899,76 @@ static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
}

if(req->method == "PUT") {
// update domain settings
// update domain contents and/or settings
auto document = req->json();

di.backend->startTransaction(zonename, -1);
updateDomainSettingsFromDocument(B, di, zonename, req->json(), false);
auto rrsets = document["rrsets"];
bool zoneWasModified = false;
// if records/comments are given, load, check and insert them
if (rrsets.is_array()) {
zoneWasModified = true;
bool haveSoa = false;
string soaEditApiKind;
string soaEditKind;
di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soaEditApiKind);
di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soaEditKind);

vector<DNSResourceRecord> new_records;
vector<Comment> new_comments;

for (const auto& rrset : rrsets.array_items()) {
DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
apiCheckQNameAllowedCharacters(qname.toString());
QType qtype;
qtype = stringFromJson(rrset, "type");
if (qtype.getCode() == 0) {
throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
}
if (rrset["records"].is_array()) {
int ttl = intFromJson(rrset, "ttl");
gatherRecords(rrset, qname, qtype, ttl, new_records);
}
if (rrset["comments"].is_array()) {
gatherComments(rrset, qname, qtype, new_comments);
}
}

for(auto& rr : new_records) {
rr.qname.makeUsLowerCase();
if (!rr.qname.isPartOf(zonename) && rr.qname != zonename)
throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.toString()+": Name is out of zone");
apiCheckQNameAllowedCharacters(rr.qname.toString());

if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
haveSoa = true;
increaseSOARecord(rr, soaEditApiKind, soaEditKind);
}
}

if (!haveSoa) {
// Require SOA regardless if this is a secondary zone or not.
// If clients want to "zero out" a secondary zone, they should still send a SOA with a,
// for their use case, "low enough" serial.
throw ApiException("Must give SOA record for zone when replacing all RR sets");
}

checkNewRecords(new_records, zonename);

di.backend->startTransaction(zonename, di.id);
for(auto& rr : new_records) {
rr.domain_id = di.id;
di.backend->feedRecord(rr, DNSName());
}
for(Comment& c : new_comments) {
c.domain_id = di.id;
di.backend->feedComment(c);
}
} else {
// avoid deleting current zone contents
di.backend->startTransaction(zonename, -1);
}

updateDomainSettingsFromDocument(B, di, zonename, document, zoneWasModified);
di.backend->commitTransaction();

resp->body = "";
Expand Down
67 changes: 67 additions & 0 deletions regression-tests.api/test_Zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ def eq_zone_rrsets(rrsets, expected):
assert data_got == data_expected, "%r != %r" % (data_got, data_expected)


def assert_eq_rrsets(rrsets, expected):
"""Assert rrsets sets are equal, ignoring sort order."""
key = lambda rrset: (rrset['name'], rrset['type'])
assert sorted(rrsets, key=key) == sorted(expected, key=key)


class Zones(ApiTestCase):

def _test_list_zones(self, dnssec=True):
Expand Down Expand Up @@ -2305,6 +2311,67 @@ def test_put_slave_tsig_key_ids_non_existent(self):
}
self.put_zone(name, payload, expect_error='A TSIG key with the name')

def test_zone_replace_rrsets_basic(self):
"""Basic test: all automatic modification is off, on replace the new rrsets are ingested as is."""
name, _, _ = self.create_zone(dnssec=False, soa_edit='', soa_edit_api='')
rrsets = [
{'name': name, 'type': 'SOA', 'ttl': 3600, 'records': [{'content': 'invalid. hostmaster.invalid. 1 10800 3600 604800 3600'}]},
{'name': name, 'type': 'NS', 'ttl': 3600, 'records': [{'content': 'ns1.example.org.'}, {'content': 'ns2.example.org.'}]},
{'name': 'www.' + name, 'type': 'A', 'ttl': 3600, 'records': [{'content': '192.0.2.1'}]},
{'name': 'sub.' + name, 'type': 'NS', 'ttl': 3600, 'records': [{'content': 'ns1.example.org.'}]},
]
self.put_zone(name, {'rrsets': rrsets})

data = self.get_zone(name)
for rrset in rrsets:
rrset.setdefault('comments', [])
for record in rrset['records']:
record.setdefault('disabled', False)
assert_eq_rrsets(data['rrsets'], rrsets)

def test_zone_replace_rrsets_dnssec(self):
"""With dnssec: check automatic rectify is done"""
name, _, _ = self.create_zone(dnssec=True)
rrsets = [
{'name': name, 'type': 'SOA', 'ttl': 3600, 'records': [{'content': 'invalid. hostmaster.invalid. 1 10800 3600 604800 3600'}]},
{'name': name, 'type': 'NS', 'ttl': 3600, 'records': [{'content': 'ns1.example.org.'}, {'content': 'ns2.example.org.'}]},
{'name': 'www.' + name, 'type': 'A', 'ttl': 3600, 'records': [{'content': '192.0.2.1'}]},
]
self.put_zone(name, {'rrsets': rrsets})

if not is_auth_lmdb():
# lmdb: skip, no get_db_records implementations
dbrecs = get_db_records(name, 'A')
assert dbrecs[0]['ordername'] is not None # default = rectify enabled

def test_zone_replace_rrsets_with_soa_edit(self):
"""SOA-EDIT was enabled before rrsets will be replaced"""
name, _, _ = self.create_zone(soa_edit='INCEPTION-INCREMENT', soa_edit_api='SOA-EDIT-INCREASE')
rrsets = [
{'name': name, 'type': 'SOA', 'ttl': 3600, 'records': [{'content': 'invalid. hostmaster.invalid. 1 10800 3600 604800 3600'}]},
{'name': name, 'type': 'NS', 'ttl': 3600, 'records': [{'content': 'ns1.example.org.'}, {'content': 'ns2.example.org.'}]},
{'name': 'www.' + name, 'type': 'A', 'ttl': 3600, 'records': [{'content': '192.0.2.1'}]},
{'name': 'sub.' + name, 'type': 'NS', 'ttl': 3600, 'records': [{'content': 'ns1.example.org.'}]},
]
self.put_zone(name, {'rrsets': rrsets})

data = self.get_zone(name)
soa = [rrset['records'][0]['content'] for rrset in data['rrsets'] if rrset['type'] == 'SOA'][0]
assert int(soa.split()[2]) > 1 # serial is larger than what we sent

def test_zone_replace_rrsets_no_soa_primary(self):
"""Replace all RRsets but supply no SOA. Should fail."""
name, _, _ = self.create_zone()
rrsets = [
{'name': name, 'type': 'NS', 'ttl': 3600, 'records': [{'content': 'ns1.example.org.'}, {'content': 'ns2.example.org.'}]}
]
self.put_zone(name, {'rrsets': rrsets}, expect_error='Must give SOA record for zone when replacing all RR sets')

def test_zone_replace_rrsets_no_soa_secondary(self):
"""Replace all RRsets in a SECONDARY zone, but supply no SOA. Should still fail."""
name, _, _ = self.create_zone(kind='Secondary', nameservers=None, masters=['127.0.0.2'])
self.put_zone(name, {'rrsets': []}, expect_error='Must give SOA record for zone when replacing all RR sets')


@unittest.skipIf(not is_auth(), "Not applicable")
class AuthRootZone(ApiTestCase, AuthZonesHelperMixin):
Expand Down

0 comments on commit 70f1db7

Please sign in to comment.