]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Added metadata handlers for HTTP API
authorChristian Jurk <cj@tfmt.de>
Sun, 3 Jul 2016 16:23:24 +0000 (18:23 +0200)
committerChristian Jurk <cj@tfmt.de>
Sun, 3 Jul 2016 16:23:24 +0000 (18:23 +0200)
pdns/ws-auth.cc
regression-tests.api/test_Zones.py

index a568c2e5a7d5e10d44d9e5182c963c4855b63ad2..5f96bbd8004736a8fbfa400fc90ff1980a2e8ed7 100644 (file)
@@ -526,6 +526,181 @@ static void updateDomainSettingsFromDocument(const DomainInfo& di, const DNSName
   }
 }
 
+static bool isValidMetadataKind(const string& kind, bool readonly) {
+  static vector<string> 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<string> 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<string, vector<string> > 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<string> 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<string> 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<string> 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<string> 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<string> 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/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
       d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
       d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport);
+      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind);
+      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata);
       d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
       d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
       d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
index 93e345b8c0e70cd61d4579f8c7c4cb169d902180..45efc0464a3747bfec559a157175818b34bd7e3c 100644 (file)
@@ -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'])