// Check that two changetype values are compatible with each other.
// Throws if they aren't.
-static void checkChangetypeCompatibility(changeType lastOperationType, changeType thisOperationType, const std::string thisChangetype)
+static void checkChangetypeCompatibility(changeType lastOperationType, changeType thisOperationType)
{
// DELETE and REPLACE operations are not compatible with PRUNE and
// EXTEND operations.
if (lastOperationOperatedOnRRsets ^ thisOperationOperatesOnRRsets) {
throw ApiException("Mixing RRset operations with single-record operations is not allowed");
}
- // Moreover, we currently only allow a single rrset for PRUNE and
- // EXTEND.
- if (thisOperationType == PRUNE || thisOperationType == EXTEND) {
- throw ApiException("Only one rrset may be provided for " + thisChangetype + " changetype");
- }
}
// Parse the record name and type from a Json patch record.
}
}
+// The return value of the apply* functions below
+enum applyResult
+{
+ SUCCESS, // successful and changes performed
+ NOP, // successful but no changes needed
+ ABORT // failed horribly, don't process anything further
+};
+
// Apply a DELETE changetype.
-static void applyDelete(const DomainInfo& domainInfo, DNSName& qname, QType& qtype)
+static applyResult applyDelete(const DomainInfo& domainInfo, DNSName& qname, QType& qtype)
{
// Delete all matching qname/qtype RRs (and implicitly, comments).
if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, {})) {
throw ApiException("Hosting backend does not support editing records.");
}
+ return SUCCESS;
}
// Struct gathering the SOA edition details, so as not to pass billions of
};
// Apply a REPLACE changetype.
-static bool applyReplace(const DomainInfo& domainInfo, const ZoneName& zonename, const Json& rrset, DNSName& qname, QType& qtype, bool allowUnderscores, soaEditSettings& soa, HttpResponse* resp)
+static applyResult applyReplace(const DomainInfo& domainInfo, const ZoneName& zonename, const Json& rrset, DNSName& qname, QType& qtype, bool allowUnderscores, soaEditSettings& soa, HttpResponse* resp)
{
bool replace_records = rrset["records"].is_array();
bool replace_comments = rrset["comments"].is_array();
}
if (!checkNewRecords(resp, new_records, zonename, allowUnderscores)) {
// Proper error response has been setup, no need to do anything further.
- return false;
+ return ABORT;
}
}
throw ApiException("Hosting backend does not support editing comments.");
}
}
- return true;
+ return SUCCESS;
}
-static bool applyPruneOrExtend(const DomainInfo& domainInfo, const ZoneName& zonename, const Json& container, DNSName& qname, QType& qtype, bool allowUnderscores, soaEditSettings& soa, HttpResponse* resp, changeType operationType)
+static applyResult applyPruneOrExtend(const DomainInfo& domainInfo, const ZoneName& zonename, const Json& container, DNSName& qname, QType& qtype, bool allowUnderscores, soaEditSettings& soa, HttpResponse* resp, changeType operationType)
{
if (!container["records"].is_array()) {
throw ApiException("No record provided for PRUNE or EXTEND operation");
if (!checkNewRecords(resp, new_records, zonename, allowUnderscores)) {
// Proper error response has been setup, no need to do anything further.
- return false;
+ return ABORT;
}
// Fetch the existing RRSet
rrset.emplace_back(new_record);
}
bool submitChanges = (operationType == EXTEND && !seenRecord) || (operationType == PRUNE && seenRecord);
- if (submitChanges) {
- if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, rrset)) {
- throw ApiException("Hosting backend does not support editing records.");
- }
+ if (!submitChanges) {
+ return NOP;
}
- else {
- resp->body = "";
- resp->status = 204; // No Content, but indicate success
- // This will force our caller to abort the transaction and return quickly,
- // without increasing the zone serial number and flushing caches.
- // This is safe to do as we do not allow more than one PRUNE or EXTEND
- // operation, so there is no further zone changes to process.
- return false;
+ if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, rrset)) {
+ throw ApiException("Hosting backend does not support editing records.");
}
}
catch (const JsonException& e) {
throw ApiException("Submitted record is invalid: " + string(e.what()));
}
- return true;
+ return SUCCESS;
}
static void patchZone(UeberBackend& backend, const ZoneName& zonename, DomainInfo& domainInfo, const vector<Json>& rrsets, HttpResponse* resp)
{
+ bool madeAnyChanges{false};
domainInfo.backend->startTransaction(zonename);
try {
soaEditSettings soa;
firstPass = false;
}
else {
- checkChangetypeCompatibility(lastOperationType, operationType, changetype);
+ checkChangetypeCompatibility(lastOperationType, operationType);
}
lastOperationType = operationType; // for next pass
DNSName qname;
QType qtype;
parseRecordNameAndType(rrset, qname, qtype);
- if (seen.count({qname, qtype, operationType}) != 0) {
- throw ApiException("Duplicate RRset " + qname.toString() + " IN " + qtype.toString() + " with changetype: " + changetype);
+ // Since we need to fetch RRset from the backend for prune and extend
+ // operations, we risk operating on stale data if we allow more than
+ // one of these for a given RRset (even processing two prune requests
+ // in a row could result in the action of the first one being lost).
+ // So for the sake of the sentinel, use the same operation type for
+ // prune and extend.
+ {
+ auto operationType2 = operationType == EXTEND ? PRUNE : operationType;
+ if (seen.count({qname, qtype, operationType2}) != 0) {
+ if (operationType == EXTEND) {
+ changetype = "EXTEND/PRUNE"; // for the sake of the error message
+ }
+ throw ApiException("Duplicate RRset " + qname.toString() + " IN " + qtype.toString() + " with changetype: " + changetype);
+ }
+ seen.insert({qname, qtype, operationType2});
}
- seen.insert({qname, qtype, operationType});
if (operationType != DELETE) {
if (domainInfo.kind == DomainInfo::Consumer) {
}
}
- bool abortPatch{false};
+ applyResult result;
switch (operationType) {
case DELETE:
- applyDelete(domainInfo, qname, qtype);
+ result = applyDelete(domainInfo, qname, qtype);
break;
case REPLACE:
- abortPatch = !applyReplace(domainInfo, zonename, rrset, qname, qtype, allowUnderscores, soa, resp);
+ result = applyReplace(domainInfo, zonename, rrset, qname, qtype, allowUnderscores, soa, resp);
break;
case PRUNE:
case EXTEND:
- abortPatch = !applyPruneOrExtend(domainInfo, zonename, rrset, qname, qtype, allowUnderscores, soa, resp, operationType);
+ result = applyPruneOrExtend(domainInfo, zonename, rrset, qname, qtype, allowUnderscores, soa, resp, operationType);
break;
}
- if (abortPatch) {
+ if (result == ABORT) {
// Proper error response has been setup, no need to do anything further.
domainInfo.backend->abortTransaction();
return;
}
+ if (result == SUCCESS) {
+ madeAnyChanges = true;
+ }
}
- SOAData soaData;
- bool zone_disabled = (!backend.getSOAUncached(zonename, soaData));
+ if (madeAnyChanges) {
+ SOAData soaData;
+ bool zone_disabled = (!backend.getSOAUncached(zonename, soaData));
- // edit SOA (if needed)
- if (!zone_disabled && !soa.edit_api_kind.empty() && !soa.edit_done) {
- // return old serial in headers, before changing it
- resp->headers["X-PDNS-Old-Serial"] = std::to_string(soaData.serial);
+ // edit SOA (if needed)
+ if (!zone_disabled && !soa.edit_api_kind.empty() && !soa.edit_done) {
+ // return old serial in headers, before changing it
+ resp->headers["X-PDNS-Old-Serial"] = std::to_string(soaData.serial);
- updateZoneSerial(domainInfo, soaData, soa.edit_api_kind, soa.edit_kind);
+ updateZoneSerial(domainInfo, soaData, soa.edit_api_kind, soa.edit_kind);
- // return new serial in headers
- resp->headers["X-PDNS-New-Serial"] = std::to_string(soaData.serial);
- }
+ // return new serial in headers
+ resp->headers["X-PDNS-New-Serial"] = std::to_string(soaData.serial);
+ }
- // Rectify
- DNSSECKeeper dnssecKeeper(&backend);
- if (!zone_disabled && !dnssecKeeper.isPresigned(zonename) && isZoneApiRectifyEnabled(domainInfo)) {
- string info;
- string error_msg;
- if (!dnssecKeeper.rectifyZone(zonename, error_msg, info, false)) {
- throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
+ // Rectify
+ DNSSECKeeper dnssecKeeper(&backend);
+ if (!zone_disabled && !dnssecKeeper.isPresigned(zonename) && isZoneApiRectifyEnabled(domainInfo)) {
+ string info;
+ string error_msg;
+ if (!dnssecKeeper.rectifyZone(zonename, error_msg, info, false)) {
+ throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
+ }
}
}
}
throw;
}
- domainInfo.backend->commitTransaction();
+ if (madeAnyChanges) {
+ domainInfo.backend->commitTransaction();
- DNSSECKeeper::clearCaches(zonename);
- purgeAuthCaches(zonename.operator const DNSName&().toString() + "$");
+ DNSSECKeeper::clearCaches(zonename);
+ purgeAuthCaches(zonename.operator const DNSName&().toString() + "$");
+ }
+ else {
+ domainInfo.backend->abortTransaction();
+ }
resp->body = "";
resp->status = 204; // No Content, but indicate success