From: Pieter Lexis Date: Fri, 6 Oct 2017 14:13:22 +0000 (+0200) Subject: API: Implement conditional rectification X-Git-Tag: rec-4.1.0-rc2~36^2~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=986e485879b2a0556e7c7f0a5ff9835df3968830;p=thirdparty%2Fpdns.git API: Implement conditional rectification 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. --- diff --git a/docs/domainmetadata.rst b/docs/domainmetadata.rst index c58917ebc6..879be84335 100644 --- a/docs/domainmetadata.rst +++ b/docs/domainmetadata.rst @@ -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 `. + +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 diff --git a/docs/http-api/endpoint-zones.rst b/docs/http-api/endpoint-zones.rst index a0b6b81929..09a0793e50 100644 --- a/docs/http-api/endpoint-zones.rst +++ b/docs/http-api/endpoint-zones.rst @@ -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 diff --git a/docs/http-api/zone.rst b/docs/http-api/zone.rst index d5f31e79b9..282f7d2f73 100644 --- a/docs/http-api/zone.rst +++ b/docs/http-api/zone.rst @@ -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 ` 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. diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 791c9fb335..51d2dd8415 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -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 records; - vector 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 records; + vector 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 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); diff --git a/regression-tests.api/test_Zones.py b/regression-tests.api/test_Zones.py index 95581072f4..af2c720999 100644 --- a/regression-tests.api/test_Zones.py +++ b/regression-tests.api/test_Zones.py @@ -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):