From 24e110436dd8812386934a013b6929a0a69f39c5 Mon Sep 17 00:00:00 2001 From: Christian Jurk Date: Sun, 3 Jul 2016 18:23:24 +0200 Subject: [PATCH] Added metadata handlers for HTTP API --- pdns/ws-auth.cc | 177 +++++++++++++++++++++++++++++ regression-tests.api/test_Zones.py | 41 +++++++ 2 files changed, 218 insertions(+) diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index a568c2e5a7..5f96bbd800 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -526,6 +526,181 @@ static void updateDomainSettingsFromDocument(const DomainInfo& di, const DNSName } } +static bool isValidMetadataKind(const string& kind, bool readonly) { + static vector builtinOptions { + "ALLOW-AXFR-FROM", + "AXFR-SOURCE", + "ALLOW-DNSUPDATE-FROM", + "TSIG-ALLOW-DNSUPDATE", + "FORWARD-DNSUPDATE", + "SOA-EDIT-DNSUPDATE", + "ALSO-NOTIFY", + "AXFR-MASTER-TSIG", + "GSS-ALLOW-AXFR-PRINCIPAL", + "GSS-ACCEPTOR-PRINCIPAL", + "IXFR", + "LUA-AXFR-SCRIPT", + "NSEC3NARROW", + "NSEC3PARAM", + "PRESIGNED", + "PUBLISH-CDNSKEY", + "PUBLISH-CDS", + "SOA-EDIT", + "TSIG-ALLOW-AXFR", + "TSIG-ALLOW-DNSUPDATE" + }; + + // the following options do not allow modifications via API + static vector protectedOptions { + "NSEC3NARROW", + "NSEC3PARAM", + "PRESIGNED", + "LUA-AXFR-SCRIPT" + }; + + bool found = false; + + for (string& s : builtinOptions) { + if (kind == s) { + for (string& s2 : protectedOptions) { + if (!readonly && s == s2) + return false; + } + found = true; + break; + } + } + + return found; +} + +static void apiZoneMetadata(HttpRequest* req, HttpResponse *resp) { + DNSName zonename = apiZoneIdToName(req->parameters["id"]); + UeberBackend B; + + if (req->method == "GET") { + map > md; + Json::array document; + + if (!B.getAllDomainMetadata(zonename, md)) + throw HttpNotFoundException(); + + for (const auto& i : md) { + Json::array entries; + for (string j : i.second) + entries.push_back(j); + + Json::object key { + { "type", "Metadata" }, + { "kind", i.first }, + { "metadata", entries } + }; + + document.push_back(key); + } + + resp->setBody(document); + } else if (req->method == "POST" && !::arg().mustDo("api-readonly")) { + auto document = req->json(); + string kind; + vector entries; + + try { + kind = stringFromJson(document, "kind"); + } catch (JsonException) { + throw ApiException("kind is not specified or not a string"); + } + + if (!isValidMetadataKind(kind, false)) + throw ApiException("Unsupported metadata kind '" + kind + "'"); + + vector vecMetadata; + auto& metadata = document["metadata"]; + if (!metadata.is_array()) + throw ApiException("metadata is not specified or not an array"); + + for (const auto& i : metadata.array_items()) { + if (!i.is_string()) + throw ApiException("metadata must be strings"); + vecMetadata.push_back(i.string_value()); + } + + if (!B.setDomainMetadata(zonename, kind, vecMetadata)) + throw ApiException("Could not update metadata entries for domain '" + zonename.toString() + "'"); + + Json::object key { + { "type", "Metadata" }, + { "kind", document["kind"] }, + { "metadata", metadata } + }; + + resp->status = 201; + resp->setBody(key); + } else + throw HttpMethodNotAllowedException(); +} + +static void apiZoneMetadataKind(HttpRequest* req, HttpResponse* resp) { + DNSName zonename = apiZoneIdToName(req->parameters["id"]); + string kind = req->parameters["kind"]; + UeberBackend B; + + if (req->method == "GET") { + vector metadata; + Json::object document; + Json::array entries; + + if (!B.getDomainMetadata(zonename, kind, metadata)) + throw HttpNotFoundException(); + else if (!isValidMetadataKind(kind, true)) + throw ApiException("Unsupported metadata kind '" + kind + "'"); + + document["type"] = "Metadata"; + document["kind"] = kind; + + for (const string& i : metadata) + entries.push_back(i); + + document["metadata"] = entries; + resp->setBody(document); + } else if (req->method == "PUT" && !::arg().mustDo("api-readonly")) { + auto document = req->json(); + + if (!isValidMetadataKind(kind, false)) + throw ApiException("Unsupported metadata kind '" + kind + "'"); + + vector vecMetadata; + auto& metadata = document["metadata"]; + if (!metadata.is_array()) + throw ApiException("metadata is not specified or not an array"); + + for (const auto& i : metadata.array_items()) { + if (!i.is_string()) + throw ApiException("metadata must be strings"); + vecMetadata.push_back(i.string_value()); + } + + if (!B.setDomainMetadata(zonename, kind, vecMetadata)) + throw ApiException("Could not update metadata entries for domain '" + zonename.toString() + "'"); + + Json::object key { + { "type", "Metadata" }, + { "kind", kind }, + { "metadata", metadata } + }; + + resp->setBody(key); + } else if (req->method == "DELETE" && !::arg().mustDo("api-readonly")) { + if (!isValidMetadataKind(kind, false)) + throw ApiException("Unsupported metadata kind '" + kind + "'"); + + vector md; // an empty vector will do it + if (!B.setDomainMetadata(zonename, kind, md)) + throw ApiException("Could not delete metadata for domain '" + zonename.toString() + "' (" + kind + ")"); + } else + throw HttpMethodNotAllowedException(); +} + static void apiZoneCryptokeys(HttpRequest* req, HttpResponse* resp) { if(req->method != "GET") throw ApiException("Only GET is implemented"); @@ -1249,6 +1424,8 @@ void AuthWebServer::webThread() d_ws->registerApiHandler("/api/v1/servers/localhost/zones//cryptokeys/", &apiZoneCryptokeys); d_ws->registerApiHandler("/api/v1/servers/localhost/zones//cryptokeys", &apiZoneCryptokeys); d_ws->registerApiHandler("/api/v1/servers/localhost/zones//export", &apiServerZoneExport); + d_ws->registerApiHandler("/api/v1/servers/localhost/zones//metadata/", &apiZoneMetadataKind); + d_ws->registerApiHandler("/api/v1/servers/localhost/zones//metadata", &apiZoneMetadata); d_ws->registerApiHandler("/api/v1/servers/localhost/zones//notify", &apiServerZoneNotify); d_ws->registerApiHandler("/api/v1/servers/localhost/zones/", &apiServerZoneDetail); d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones); diff --git a/regression-tests.api/test_Zones.py b/regression-tests.api/test_Zones.py index 93e345b8c0..45efc0464a 100644 --- a/regression-tests.api/test_Zones.py +++ b/regression-tests.api/test_Zones.py @@ -286,6 +286,47 @@ class AuthZones(ApiTestCase, AuthZonesHelperMixin): headers={'content-type': 'application/json'}) self.assertEquals(r.status_code, 422) + def test_create_zone_metadata(self): + payload_metadata = {"type": "Metadata", "kind": "AXFR-SOURCE", "metadata": ["127.0.0.2"]} + r = self.session.post(self.url("/api/v1/servers/localhost/zones/example.com/metadata"), + data=json.dumps(payload_metadata)) + rdata = r.json() + self.assertEquals(r.status_code, 201) + self.assertEquals(rdata["metadata"], payload_metadata["metadata"]) + + def test_create_zone_metadata_kind(self): + payload_metadata = {"metadata": ["127.0.0.2"]} + r = self.session.put(self.url("/api/v1/servers/localhost/zones/example.com/metadata/AXFR-SOURCE"), + data=json.dumps(payload_metadata)) + rdata = r.json() + self.assertEquals(r.status_code, 200) + self.assertEquals(rdata["metadata"], payload_metadata["metadata"]) + + def test_create_protected_zone_metadata(self): + # test whether it prevents modification of certain kinds + for k in ("NSEC3NARROW", "NSEC3PARAM", "PRESIGNED", "LUA-AXFR-SCRIPT"): + payload = {"metadata": ["FOO", "BAR"]} + r = self.session.put(self.url("/api/v1/servers/localhost/zones/example.com/metadata/%s" % k), + data=json.dumps(payload)) + self.assertEquals(r.status_code, 422) + + def test_retrieve_zone_metadata(self): + payload_metadata = {"type": "Metadata", "kind": "AXFR-SOURCE", "metadata": ["127.0.0.2"]} + self.session.post(self.url("/api/v1/servers/localhost/zones/example.com/metadata"), + data=json.dumps(payload_metadata)) + r = self.session.get(self.url("/api/v1/servers/localhost/zones/example.com/metadata")) + rdata = r.json() + self.assertEquals(r.status_code, 200) + self.assertIn(payload_metadata, rdata) + + def test_delete_zone_metadata(self): + r = self.session.delete(self.url("/api/v1/servers/localhost/zones/example.com/metadata/AXFR-SOURCE")) + self.assertEquals(r.status_code, 200) + r = self.session.get(self.url("/api/v1/servers/localhost/zones/example.com/metadata/AXFR-SOURCE")) + rdata = r.json() + self.assertEquals(r.status_code, 200) + self.assertEquals(rdata["metadata"], []) + def test_create_slave_zone(self): # Test that nameservers can be absent for slave zones. name, payload, data = self.create_zone(kind='Slave', nameservers=None, masters=['127.0.0.2']) -- 2.47.2