]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
API: Implement conditional rectification
authorPieter Lexis <pieter.lexis@powerdns.com>
Fri, 6 Oct 2017 14:13:22 +0000 (16:13 +0200)
committerPieter Lexis <pieter.lexis@powerdns.com>
Tue, 17 Oct 2017 14:17:15 +0000 (16:17 +0200)
This commit takes a lot of ideas and code from #3417 and subsequent
development and implements the following things:

 - Generate DNSSEC keys for a zone when "dnssec" is true in an API
   POST/PATCH for zones
 - Rectify DNSSEC zones after POST/PATCH when API-RECTIFY metadata is 1
 - Allow setting this metadata via the "api-rectify" param in a Zone
   object
 - Shows "nsec3param" and "nsec3narrow" in Zone API responses
 - Adds an "rrsets" request parameter for a zone to skip sending RRSets
   in the response (Closes #5712)

Closes #3417

Many thanks to Nils Wisiol (@nils-wisiol) for the initial
implementation.

docs/domainmetadata.rst
docs/http-api/endpoint-zones.rst
docs/http-api/zone.rst
pdns/ws-auth.cc
regression-tests.api/test_Zones.py

index c58917ebc648927d79d780a9d8fcb30b4a832d5f..879be8433586d2e302ce2babc95639cd528454aa 100644 (file)
@@ -46,6 +46,18 @@ Each ACL has its own row in the database:
 To disallow all IP's, except those explicitly allowed by domainmetadata
 records, add ``allow-axfr-ips=`` to ``pdns.conf``.
 
+.. _metadata-api-rectify:
+
+API-RECTIFY
+-----------
+.. since:: 4.1.0
+
+This metadata item controls whether or not a zone is fully rectified on changes
+to the contents of a zone made through the :doc:`API <http-api/index>`.
+
+When the ``API-RECTIFY`` value is "1", the zone will be rectified on changes.
+Any other other value means that it will not be rectified.
+
 .. _metadata-axfr-source:
 
 AXFR-SOURCE
index a0b6b819296fc40e6a429d50c26e9985bfae3868..09a0793e50bad610f70b3698bfa5bbdd1871cf8b 100644 (file)
@@ -8,28 +8,29 @@ Zones endpoint
 
 .. http:post:: /api/v1/servers/:server_id/zones
 
-  Creates a new domain.
+  Creates a new domain, returns the :json:object:`Zone` on creation.
 
   :param server_id: The name of the server
+  :query rrsets: "true" (default) or "false", whether to include the "rrsets" in the response :json:object:`Zone` object.
+  :statuscode 201: The zone was successfully created
 
   A :json:object:`Zone` MUST be sent in the request body.
 
-  -  ``dnssec``, ``nsec3narrow``, ``presigned``, ``nsec3param``, ``active-keys`` are OPTIONAL.
-  -  ``dnssec``, ``nsec3narrow``, ``presigned`` default to ``false``.
+  -  ``dnssec``, ``nsec3narrow``, ``presigned``, ``nsec3param``, ``api_rectify``, ``active-keys`` are OPTIONAL.
+  -  ``dnssec``, ``nsec3narrow``, ``presigned``, ``api_rectify`` default to ``false``.
 
   The server MUST create a SOA record.
   The created SOA record SHOULD have serial set to the value given as ``serial`` (or 0 if missing), use the nameserver name, email, TTL values as specified in the PowerDNS configuration (``default-soa-name``, ``default-soa-mail``, etc).
   These default values can be overridden by supplying a custom SOA record in the records list.
   If ``soa_edit_api`` is set, the SOA record is edited according to the SOA-EDIT-API rules before storing it (also applies to custom SOA records).
 
-  **TODO**: ``dnssec``, ``nsec3narrow``, ``nsec3param``, ``presigned`` are not yet implemented.
-
 .. http:get:: /api/v1/servers/:server_id/zones/:zone_id
 
   Returns zone information.
 
   :param server_id: The name of the server
   :param zone_id: The id number of the :json:object:`Zone`
+  :query rrsets: "true" (default) or "false", whether to include the "rrsets" in the response :json:object:`Zone` object.
 
 .. http:delete:: /api/v1/servers/:server_id/zones/:zone_id
 
index d5f31e79b93155acf8a3fcfde0a75c09b04609ac..282f7d2f7323c3cba9e4a2e884a69376d46446eb 100644 (file)
@@ -24,11 +24,12 @@ Comments are per-RRset.
   :property integer notified_serial: The SOA serial notifications have been sent out for
   :property [str] masters: List of IP addresses configured as a master for this zone ("Slave" type zones only)
   :property bool dnssec: Whether or not this zone is DNSSEC signed (inferred from presigned being true XOR presence of at least one cryptokey with active being true)
-  :property string nsec3param: The NSEC3PARAM record (not implemented)
-  :property bool nsec3narrow: Whether or not the zone uses NSEC3 narrow (not implemented)
-  :property bool presigned: Whether or not the zone is pre-signed (not implemented)
+  :property string nsec3param: The NSEC3PARAM record
+  :property bool nsec3narrow: Whether or not the zone uses NSEC3 narrow
+  :property bool presigned: Whether or not the zone is pre-signed
   :property string soa_edit: The :ref:`metadata-soa-edit` metadata item
   :property string soa_edit_api: The :ref:`metadata-soa-edit-api` metadata item
+  :property bool api_rectify: Whether or not the zone will be rectified on data changes via the API
   :property string zone: MAY contain a BIND-style zone file when creating a zone
   :property str account: MAY be set. Its value is defined by local policy
   :property [str] nameservers: MAY be sent in client bodies during creation, and MUST NOT be sent by the server. Simple list of strings of nameserver names, including the trailing dot. Not required for slave zones.
@@ -37,24 +38,19 @@ Comments are per-RRset.
 
   Switching ``dnssec`` to ``true`` (from ``false``) sets up DNSSEC signing
   based on the other flags, this includes running the equivalent of
-  ``secure-zone`` and ``rectify-zone``. This also applies to newly created
-  zones. If ``presigned`` is ``true``, no DNSSEC changes will be made to
-  the zone or cryptokeys.
-
-  ``dnssec``, ``nsec3narrow``, ``nsec3param``, ``presigned`` are not yet implemented.
+  ``secure-zone`` and ``rectify-zone`` (if ``api_rectify`` is set to "1").
+  This also applies to newly created zones. If ``presigned`` is ``true``,
+  no DNSSEC changes will be made to the zone or cryptokeys.
 
 .. note::
 
   ``notified_serial``, ``serial`` MUST NOT be sent in client bodies.
 
-Changes made through the Zones API will always yield valid zone data,
-and the zone will be properly "rectified". If changes are made through other means
-(e.g. direct database access), this is not guaranteed to be true and clients SHOULD
-trigger rectify.
-
-.. note::
+Changes made through the Zones API will always yield valid zone data, as the API will reject records with wrong data.
 
-  Rectification is not yet implemented.
+DNSSEC-enabled zones should be :ref:`rectified <rules-for-filling-out-dnssec-fields>` after changing the zone data.
+This can be done by the API automatically after a change when the :ref:`metadata-api-rectify` metadata is set.
+When creating or updating a zone, the "api_rectify" field of the :json:object:`ZOne` can be set to `true` to enable this behaviour.
 
 Backends might implement additional features (by coincidence or not).
 These things are not supported through the API.
index 791c9fb3354eab34d2dfd488f61d396e4f5c8910..51d2dd8415eaf28520c30cecf0acd778179fe659 100644 (file)
@@ -318,7 +318,15 @@ static Json::object getZoneInfo(const DomainInfo& di, DNSSECKeeper *dk) {
   };
 }
 
-static void fillZone(const DNSName& zonename, HttpResponse* resp) {
+static bool shouldDoRRSets(HttpRequest* req) {
+  if (req->getvars.count("rrsets") == 0 || req->getvars["rrsets"] == "true")
+    return true;
+  if (req->getvars["rrsets"] == "false")
+    return false;
+  throw ApiException("'rrsets' request parameter value '"+req->getvars["rrsets"]+"' is not supported");
+}
+
+static void fillZone(const DNSName& zonename, HttpResponse* resp, bool doRRSets) {
   UeberBackend B;
   DomainInfo di;
   if(!B.getDomainInfo(zonename, di))
@@ -333,93 +341,108 @@ static void fillZone(const DNSName& zonename, HttpResponse* resp) {
   string soa_edit;
   di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
   doc["soa_edit"] = soa_edit;
-
-  vector<DNSResourceRecord> records;
-  vector<Comment> comments;
-
-  // load all records + sort
-  {
-    DNSResourceRecord rr;
-    di.backend->list(zonename, di.id, true); // incl. disabled
-    while(di.backend->get(rr)) {
-      if (!rr.qtype.getCode())
-        continue; // skip empty non-terminals
-      records.push_back(rr);
+  string nsec3param;
+  di.backend->getDomainMetadataOne(zonename, "NSEC3PARAM", nsec3param);
+  doc["nsec3param"] = nsec3param;
+  string nsec3narrow;
+  bool nsec3narrowbool = false;
+  di.backend->getDomainMetadataOne(zonename, "NSEC3NARROW", nsec3narrow);
+  if (nsec3narrow == "1")
+    nsec3narrowbool = true;
+  doc["nsec3narrow"] = nsec3narrowbool;
+
+  string api_rectify;
+  di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
+  doc["api_rectify"] = (api_rectify == "1");
+
+  if (doRRSets) {
+    vector<DNSResourceRecord> records;
+    vector<Comment> comments;
+
+    // load all records + sort
+    {
+      DNSResourceRecord rr;
+      di.backend->list(zonename, di.id, true); // incl. disabled
+      while(di.backend->get(rr)) {
+        if (!rr.qtype.getCode())
+          continue; // skip empty non-terminals
+        records.push_back(rr);
+      }
+      sort(records.begin(), records.end(), [](const DNSResourceRecord& a, const DNSResourceRecord& b) {
+              if (a.qname == b.qname) {
+                  return b.qtype < a.qtype;
+              }
+              return b.qname < a.qname;
+          });
     }
-    sort(records.begin(), records.end(), [](const DNSResourceRecord& a, const DNSResourceRecord& b) {
-            if (a.qname == b.qname) {
-                return b.qtype < a.qtype;
-            }
-            return b.qname < a.qname;
-        });
-  }
 
-  // load all comments + sort
-  {
-    Comment comment;
-    di.backend->listComments(di.id);
-    while(di.backend->getComment(comment)) {
-      comments.push_back(comment);
+    // load all comments + sort
+    {
+      Comment comment;
+      di.backend->listComments(di.id);
+      while(di.backend->getComment(comment)) {
+        comments.push_back(comment);
+      }
+      sort(comments.begin(), comments.end(), [](const Comment& a, const Comment& b) {
+              if (a.qname == b.qname) {
+                  return b.qtype < a.qtype;
+              }
+              return b.qname < a.qname;
+          });
     }
-    sort(comments.begin(), comments.end(), [](const Comment& a, const Comment& b) {
-            if (a.qname == b.qname) {
-                return b.qtype < a.qtype;
-            }
-            return b.qname < a.qname;
-        });
-  }
 
-  Json::array rrsets;
-  Json::object rrset;
-  Json::array rrset_records;
-  Json::array rrset_comments;
-  DNSName current_qname;
-  QType current_qtype;
-  uint32_t ttl;
-  auto rit = records.begin();
-  auto cit = comments.begin();
-
-  while (rit != records.end() || cit != comments.end()) {
-    if (cit == comments.end() || (rit != records.end() && (cit->qname.toString() <= rit->qname.toString() || cit->qtype < rit->qtype || cit->qtype == rit->qtype))) {
-      current_qname = rit->qname;
-      current_qtype = rit->qtype;
-      ttl = rit->ttl;
-    } else {
-      current_qname = cit->qname;
-      current_qtype = cit->qtype;
-      ttl = 0;
-    }
+    Json::array rrsets;
+    Json::object rrset;
+    Json::array rrset_records;
+    Json::array rrset_comments;
+    DNSName current_qname;
+    QType current_qtype;
+    uint32_t ttl;
+    auto rit = records.begin();
+    auto cit = comments.begin();
+
+    while (rit != records.end() || cit != comments.end()) {
+      if (cit == comments.end() || (rit != records.end() && (cit->qname.toString() <= rit->qname.toString() || cit->qtype < rit->qtype || cit->qtype == rit->qtype))) {
+        current_qname = rit->qname;
+        current_qtype = rit->qtype;
+        ttl = rit->ttl;
+      } else {
+        current_qname = cit->qname;
+        current_qtype = cit->qtype;
+        ttl = 0;
+      }
 
-    while(rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
-      ttl = min(ttl, rit->ttl);
-      rrset_records.push_back(Json::object {
-        { "disabled", rit->disabled },
-        { "content", makeApiRecordContent(rit->qtype, rit->content) }
-      });
-      rit++;
-    }
-    while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
-      rrset_comments.push_back(Json::object {
-        { "modified_at", (double)cit->modified_at },
-        { "account", cit->account },
-        { "content", cit->content }
-      });
-      cit++;
+      while(rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
+        ttl = min(ttl, rit->ttl);
+        rrset_records.push_back(Json::object {
+          { "disabled", rit->disabled },
+          { "content", makeApiRecordContent(rit->qtype, rit->content) }
+        });
+        rit++;
+      }
+      while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
+        rrset_comments.push_back(Json::object {
+          { "modified_at", (double)cit->modified_at },
+          { "account", cit->account },
+          { "content", cit->content }
+        });
+        cit++;
+      }
+
+      rrset["name"] = current_qname.toString();
+      rrset["type"] = current_qtype.getName();
+      rrset["records"] = rrset_records;
+      rrset["comments"] = rrset_comments;
+      rrset["ttl"] = (double)ttl;
+      rrsets.push_back(rrset);
+      rrset.clear();
+      rrset_records.clear();
+      rrset_comments.clear();
     }
 
-    rrset["name"] = current_qname.toString();
-    rrset["type"] = current_qtype.getName();
-    rrset["records"] = rrset_records;
-    rrset["comments"] = rrset_comments;
-    rrset["ttl"] = (double)ttl;
-    rrsets.push_back(rrset);
-    rrset.clear();
-    rrset_records.clear();
-    rrset_comments.clear();
+    doc["rrsets"] = rrsets;
   }
 
-  doc["rrsets"] = rrsets;
-
   resp->setBody(doc);
 }
 
@@ -497,8 +520,31 @@ static void gatherComments(const Json container, const DNSName& qname, const QTy
   }
 }
 
-static void updateDomainSettingsFromDocument(const DomainInfo& di, const DNSName& zonename, const Json document) {
+static void checkDefaultDNSSECAlgos() {
+  int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
+  int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
+  int k_size = arg().asNum("default-ksk-size");
+  int z_size = arg().asNum("default-zsk-size");
+
+  // Sanity check DNSSEC parameters
+  if (::arg()["default-zsk-algorithm"] != "") {
+    if (k_algo == -1)
+      throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
+    else if (k_algo <= 10 && k_size == 0)
+      throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
+  }
+
+  if (::arg()["default-zsk-algorithm"] != "") {
+    if (z_algo == -1)
+      throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
+    else if (z_algo <= 10 && z_size == 0)
+      throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
+  }
+}
+
+static void updateDomainSettingsFromDocument(UeberBackend& B, const DomainInfo& di, const DNSName& zonename, const Json document) {
   string zonemaster;
+  bool shouldRectify = false;
   for(auto value : document["masters"].array_items()) {
     string master = value.string_value();
     if (master.empty())
@@ -506,18 +552,110 @@ static void updateDomainSettingsFromDocument(const DomainInfo& di, const DNSName
     zonemaster += master + " ";
   }
 
-  di.backend->setKind(zonename, DomainInfo::stringToKind(stringFromJson(document, "kind")));
-  di.backend->setMaster(zonename, zonemaster);
-
+  if (zonemaster != "") {
+    di.backend->setMaster(zonename, zonemaster);
+  }
+  if (document["kind"].is_string()) {
+    di.backend->setKind(zonename, DomainInfo::stringToKind(stringFromJson(document, "kind")));
+  }
   if (document["soa_edit_api"].is_string()) {
     di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
   }
   if (document["soa_edit"].is_string()) {
     di.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
   }
+  if (document["api_rectify"].is_string()) {
+    di.backend->setDomainMetadataOne(zonename, "API-RECTIFY", document["api_rectify"].string_value());
+  }
   if (document["account"].is_string()) {
     di.backend->setAccount(zonename, document["account"].string_value());
   }
+
+  DNSSECKeeper dk(&B);
+  bool dnssecInJSON = false;
+  bool dnssecDocVal = false;
+
+  try {
+    dnssecDocVal = boolFromJson(document, "dnssec");
+    dnssecInJSON = true;
+  }
+  catch (JsonException) {}
+
+  bool isDNSSECZone = dk.isSecuredZone(zonename);
+
+  if (dnssecInJSON) {
+    if (dnssecDocVal) {
+      if (!isDNSSECZone) {
+        checkDefaultDNSSECAlgos();
+
+        int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
+        int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
+        int k_size = arg().asNum("default-ksk-size");
+        int z_size = arg().asNum("default-zsk-size");
+
+        if (k_algo != -1) {
+          int64_t id;
+          if (!dk.addKey(zonename, true, k_algo, id, k_size)) {
+            throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC"
+                + "capable backends are loaded, or because the backends have DNSSEC disabled."
+                + "For the Generic SQL backends, set the 'gsqlite3-dnssec', 'gmysql-dnssec' or"
+                + "'gpgsql-dnssec' flag. Also make sure the schema has been updated for DNSSEC!");
+          }
+        }
+
+        if (z_algo != -1) {
+          int64_t id;
+          if (!dk.addKey(zonename, false, z_algo, id, z_size)) {
+            throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC"
+                + "capable backends are loaded, or because the backends have DNSSEC disabled."
+                + "For the Generic SQL backends, set the 'gsqlite3-dnssec', 'gmysql-dnssec' or"
+                + "'gpgsql-dnssec' flag. Also make sure the schema has been updated for DNSSEC!");
+          }
+        }
+
+        // Used later for NSEC3PARAM
+        isDNSSECZone = dk.isSecuredZone(zonename);
+
+        if (!isDNSSECZone) {
+          throw ApiException("Failed to secure '" + zonename.toString() + "'. Is your backend dnssec enabled? (set "
+              + "gsqlite3-dnssec, or gmysql-dnssec etc). Check this first."
+              + "If you run with the BIND backend, make sure you have configured"
+              + "it to use DNSSEC with 'bind-dnssec-db=/path/fname' and"
+              + "'pdnsutil create-bind-db /path/fname'!");
+        }
+        shouldRectify = true;
+      }
+    } else {
+      // "dnssec": false in json
+      if (isDNSSECZone) {
+        throw ApiException("Refusing to un-secure zone " + zonename.toString());
+      }
+    }
+  }
+
+  if(document["nsec3param"].string_value().length() > 0) {
+    shouldRectify = true;
+    NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
+    string error_msg = "";
+    if (!isDNSSECZone) {
+      throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"', but zone is not DNSSEC secured.");
+    }
+    if (!dk.checkNSEC3PARAM(ns3pr, error_msg)) {
+      throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"' are invalid. " + error_msg);
+    }
+    if (!dk.setNSEC3PARAM(zonename, ns3pr, boolFromJson(document, "nsec3narrow", false))) {
+      throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() +
+          "' passed our basic sanity checks, but cannot be used with the current backend.");
+    }
+  }
+
+  string api_rectify;
+  di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
+  if (shouldRectify && dk.isSecuredZone(zonename) && !dk.isPresigned(zonename) && api_rectify == "1") {
+    string error_msg = "";
+    if (!dk.rectifyZone(zonename, error_msg))
+      throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
+  }
 }
 
 static bool isValidMetadataKind(const string& kind, bool readonly) {
@@ -547,6 +685,7 @@ static bool isValidMetadataKind(const string& kind, bool readonly) {
 
   // the following options do not allow modifications via API
   static vector<string> protectedOptions {
+    "API-RECTIFY",
     "NSEC3NARROW",
     "NSEC3PARAM",
     "PRESIGNED",
@@ -1136,6 +1275,18 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
 
     checkDuplicateRecords(new_records);
 
+    if (boolFromJson(document, "dnssec", false)) {
+      checkDefaultDNSSECAlgos();
+
+      if(document["nsec3param"].string_value().length() > 0) {
+        NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
+        string error_msg = "";
+        if (!dk.checkNSEC3PARAM(ns3pr, error_msg)) {
+          throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"' are invalid. " + error_msg);
+        }
+      }
+    }
+
     // no going back after this
     if(!B.createDomain(zonename))
       throw ApiException("Creating domain '"+zonename.toString()+"' failed");
@@ -1159,13 +1310,13 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
       di.backend->feedComment(c);
     }
 
-    updateDomainSettingsFromDocument(di, zonename, document);
+    updateDomainSettingsFromDocument(B, di, zonename, document);
 
     di.backend->commitTransaction();
 
     storeChangedPTRs(B, new_ptrs);
 
-    fillZone(zonename, resp);
+    fillZone(zonename, resp, shouldDoRRSets(req));
     resp->status = 201;
     return;
   }
@@ -1193,7 +1344,7 @@ static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
     if(!B.getDomainInfo(zonename, di))
       throw ApiException("Could not find domain '"+zonename.toString()+"'");
 
-    updateDomainSettingsFromDocument(di, zonename, req->json());
+    updateDomainSettingsFromDocument(B, di, zonename, req->json());
 
     resp->body = "";
     resp->status = 204; // No Content, but indicate success
@@ -1217,7 +1368,7 @@ static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
     patchZone(req, resp);
     return;
   } else if (req->method == "GET") {
-    fillZone(zonename, resp);
+    fillZone(zonename, resp, shouldDoRRSets(req));
     return;
   }
 
@@ -1510,6 +1661,16 @@ static void patchZone(HttpRequest* req, HttpResponse* resp) {
     di.backend->abortTransaction();
     throw;
   }
+
+  DNSSECKeeper dk;
+  string api_rectify;
+  di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
+  if (dk.isSecuredZone(zonename) && !dk.isPresigned(zonename) && api_rectify == "1") {
+    string error_msg = "";
+    if (!dk.rectifyZone(zonename, error_msg))
+      throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
+  }
+
   di.backend->commitTransaction();
 
   purgeAuthCachesExact(zonename);
index 95581072f414f0dc6bf1e22b1b5fadf6866e417a..af2c7209996677f455a9f5ea341fd94774181438 100644 (file)
@@ -337,6 +337,85 @@ class AuthZones(ApiTestCase, AuthZonesHelperMixin):
             headers={'content-type': 'application/json'})
         self.assertEquals(r.status_code, 422)
 
+    def test_create_zone_with_dnssec(self):
+        """
+        Create a zone with "dnssec" set and see if a key was made.
+        """
+        name = unique_zone_name()
+        name, payload, data = self.create_zone(dnssec=True)
+
+        r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
+
+        for k in ('dnssec', ):
+            self.assertIn(k, data)
+            if k in payload:
+                self.assertEquals(data[k], payload[k])
+
+        r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name + '/cryptokeys'))
+
+        keys = r.json()
+
+        print keys
+
+        self.assertEquals(r.status_code, 200)
+        self.assertEquals(len(keys), 1)
+        self.assertEquals(keys[0]['type'], 'Cryptokey')
+        self.assertEquals(keys[0]['active'], True)
+        self.assertEquals(keys[0]['keytype'], 'csk')
+
+    def test_create_zone_with_nsec3param(self):
+        """
+        Create a zone with "nsec3param" set and see if the metadata was added.
+        """
+        name = unique_zone_name()
+        nsec3param = '1 0 500 aabbccddeeff'
+        name, payload, data = self.create_zone(dnssec=True, nsec3param=nsec3param)
+
+        r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
+
+        for k in ('dnssec', 'nsec3param'):
+            self.assertIn(k, data)
+            if k in payload:
+                self.assertEquals(data[k], payload[k])
+
+        r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name + '/metadata/NSEC3PARAM'))
+
+        data = r.json()
+
+        print data
+
+        self.assertEquals(r.status_code, 200)
+        self.assertEquals(len(data['metadata']), 1)
+        self.assertEquals(data['kind'], 'NSEC3PARAM')
+        self.assertEquals(data['metadata'][0], nsec3param)
+
+    def test_create_zone_with_nsec3narrow(self):
+        """
+        Create a zone with "nsec3narrow" set and see if the metadata was added.
+        """
+        name = unique_zone_name()
+        nsec3param = '1 0 500 aabbccddeeff'
+        name, payload, data = self.create_zone(dnssec=True, nsec3param=nsec3param,
+                                               nsec3narrow=True)
+
+        r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
+
+        for k in ('dnssec', 'nsec3param', 'nsec3narrow'):
+            self.assertIn(k, data)
+            if k in payload:
+                self.assertEquals(data[k], payload[k])
+
+        r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name + '/metadata/NSEC3NARROW'))
+
+        data = r.json()
+
+        print data
+
+        self.assertEquals(r.status_code, 200)
+        self.assertEquals(len(data['metadata']), 1)
+        self.assertEquals(data['kind'], 'NSEC3NARROW')
+        self.assertEquals(data['metadata'][0], '1')
+
     def test_zone_absolute_url(self):
         name, payload, data = self.create_zone()
         r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
@@ -1314,6 +1393,45 @@ fred   IN  A      192.168.0.4
         # should return zone, SOA, ns1, ns2, sub.sub A (but not the ENT)
         self.assertEquals(len(r.json()), 5)
 
+    def test_rrset_parameter_post_false(self):
+        name = unique_zone_name()
+        payload = {
+            'name': name,
+            'kind': 'Native',
+            'nameservers': ['ns1.example.com.', 'ns2.example.com.']
+        }
+        r = self.session.post(
+            self.url("/api/v1/servers/localhost/zones?rrsets=false"),
+            data=json.dumps(payload),
+            headers={'content-type': 'application/json'})
+        print r.json()
+        self.assert_success_json(r)
+        self.assertEquals(r.status_code, 201)
+        self.assertEquals(r.json().get('rrsets'), None)
+
+    def test_rrset_false_parameter(self):
+        name = unique_zone_name()
+        self.create_zone(name=name, kind='Native')
+        r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=false"))
+        self.assert_success_json(r)
+        print r.json()
+        self.assertEquals(r.json().get('rrsets'), None)
+
+    def test_rrset_true_parameter(self):
+        name = unique_zone_name()
+        self.create_zone(name=name, kind='Native')
+        r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=true"))
+        self.assert_success_json(r)
+        print r.json()
+        self.assertEquals(len(r.json().get('rrsets')), 2)
+
+    def test_wrong_rrset_parameter(self):
+        name = unique_zone_name()
+        self.create_zone(name=name, kind='Native')
+        r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=foobar"))
+        self.assertEquals(r.status_code, 422)
+        self.assertIn("'rrsets' request parameter value 'foobar' is not supported", r.json()['error'])
+
 
 @unittest.skipIf(not is_auth(), "Not applicable")
 class AuthRootZone(ApiTestCase, AuthZonesHelperMixin):
@@ -1490,7 +1608,6 @@ class RecursorZones(ApiTestCase):
         # should return zone, SOA
         self.assertEquals(len(r.json()), 2)
 
-
 @unittest.skipIf(not is_auth(), "Not applicable")
 class AuthZoneKeys(ApiTestCase, AuthZonesHelperMixin):