From: Miod Vallat Date: Thu, 27 Mar 2025 16:04:45 +0000 (+0100) Subject: Naive plumbing of views and networks in the REST API. X-Git-Tag: auth-5.0.0-alpha1~1^2~19 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d3cc2bbb5a664e1eccd7d1acb5e2208ed198920d;p=thirdparty%2Fpdns.git Naive plumbing of views and networks in the REST API. --- diff --git a/pdns/misc.hh b/pdns/misc.hh index 2e8bd486dc..ee03a5112c 100644 --- a/pdns/misc.hh +++ b/pdns/misc.hh @@ -474,7 +474,7 @@ inline size_t pdns_ci_find(const string& haystack, const string& needle) pair splitField(const string& inp, char sepa); -inline bool isCanonical(const string& qname) +inline bool isCanonical(std::string_view qname) { if(qname.empty()) return false; diff --git a/pdns/ws-api.cc b/pdns/ws-api.cc index 9040853d86..8edab43635 100644 --- a/pdns/ws-api.cc +++ b/pdns/ws-api.cc @@ -269,6 +269,19 @@ DNSName apiNameToDNSName(const string& name) #if defined(PDNS_AUTH) ZoneName apiNameToZoneName(const string& name) { + // Split the variant name, if any, in order to be able to invoke + // isCanonical on the right subset. + if (auto sep = ZoneName::findVariantSeparator(name); sep != std::string_view::npos) { + if (!isCanonical(std::string_view(name).substr(0, sep))) { + throw ApiException("Zone Name '" + name + "' is not canonical"); + } + try { + return ZoneName(name, sep); + } + catch (...) { + throw ApiException("Unable to parse Zone Name '" + name + "'"); + } + } return ZoneName(apiNameToDNSName(name)); } #endif diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 295cecbb7f..5f110f6e23 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -2649,6 +2649,146 @@ static void prometheusMetrics(HttpRequest* /* req */, HttpResponse* resp) resp->status = 200; } +// Views + +// Serialize a list of ZoneName as a JSON array of strings +static void jsonFillZoneNameArray(Json::array& array, std::vector& zones) +{ + for (const auto& zone : zones) { + // Remember ZoneName::toString() intentionally omits the variant + std::string name(zone.toString()); + if (zone.hasVariant()) { + name.push_back('.'); + name += zone.getVariant(); + } + array.emplace_back(name); + } +} + +// GET /views returns the list of all views (tags) +static void apiServerViewsAllGET(HttpRequest* /* req */, HttpResponse* resp) +{ + std::vector views; + UeberBackend backend; + + backend.viewList(views); + + Json::object jsonresult{ + {"views", std::move(views)}}; + resp->setJsonBody(jsonresult); +} + +// GET /views/ returns the list of all ZoneName in the given "view" view +static void apiServerViewsGET(HttpRequest* req, HttpResponse* resp) +{ + std::string view{req->parameters["view"]}; + std::vector zones; + UeberBackend backend; + + backend.viewListZones(view, zones); + + if (zones.empty()) { + throw HttpNotFoundException(); // view does not exist + } + + Json::array jsonarray; + jsonFillZoneNameArray(jsonarray, zones); + Json::object jsonresult{ + {"zones", jsonarray}}; // FIXME: this should probably be a list of zone objects that at least have name and variant (perhaps separated?) and a path for .../zones/[encoded domain name with variant] + resp->setJsonBody(jsonresult); +} + +// POST /views/ + name in json adds ZoneName "name" to view "view" +static void apiServerViewsPOST(HttpRequest* req, HttpResponse* resp) +{ + UeberBackend backend; + DomainInfo domainInfo; + const auto& document = req->json(); + ZoneName zonename = apiNameToZoneName(stringFromJson(document, "name")); + + if (!backend.getDomainInfo(zonename, domainInfo)) { + throw ApiException("Zone " + zonename.toString() + "does not exist"); + } + std::string view{req->parameters["view"]}; + + if (!domainInfo.backend->viewAddZone(view, zonename)) { + throw ApiException("Failed to add " + zonename.toString() + " to view " + view); + } + + resp->body = ""; + resp->status = 204; +} + +// DELETE /views// removes ZoneName "id" from view "view" +static void apiServerViewsDELETE(HttpRequest* req, HttpResponse* resp) +{ + ZoneData zoneData{req}; + std::string view{req->parameters["view"]}; + + if (!zoneData.domainInfo.backend->viewDelZone(view, zoneData.zoneName)) { + throw ApiException("Failed to remove " + zoneData.zoneName.toString() + " from view " + view); + } + + resp->body = ""; + resp->status = 204; +} + +// Networks + +// GET /networks return the list of all registered networks and views (only one view per network) +// GET /networks// return the name of the view for the given network +static void apiServerNetworksGET(HttpRequest* req, HttpResponse* resp) +{ + Netmask network; + if (req->parameters.count("ip") != 0 && req->parameters.count("prefixlen") != 0) { + std::string subnet{req->parameters["ip"]}; + std::string prefixlen{req->parameters["prefixlen"]}; + network = subnet + "/" + prefixlen; + } + + UeberBackend backend; + std::vector> networks; + backend.networkList(networks); + Json::array jsonarray; + Json::object item; + for (const auto& pair : networks) { + if (!network.empty() && !(pair.first == network)) { // FIXME: should this case handled by a separate call networkGet, to be implemented in lmdbbackend? + continue; + } + item["network"] = pair.first.toString(); + item["view"] = pair.second; + jsonarray.emplace_back(item); + item.clear(); + } + + if (!network.empty() && jsonarray.empty()) { + throw HttpNotFoundException(); // no views configured for that network + } + + Json::object jsonresult{ + {"networks", std::move(jsonarray)}}; + resp->setJsonBody(jsonresult); +} + +// PUT /networks// sets the name of the view for the given network +static void apiServerNetworksPUT(HttpRequest* req, HttpResponse* resp) +{ + std::string subnet{req->parameters["ip"]}; + std::string prefixlen{req->parameters["prefixlen"]}; + Netmask network(subnet + "/" + prefixlen); + + const auto& document = req->json(); + std::string view = stringFromJson(document, "view"); + + UeberBackend backend; + if (!backend.networkSet(network, view)) { + throw ApiException("Failed to setup view " + view + " for network " + network.toString()); + } + + resp->body = ""; + resp->status = 204; +} + static void cssfunction(HttpRequest* /* req */, HttpResponse* resp) { resp->headers["Cache-Control"] = "max-age=86400"; @@ -2697,11 +2837,18 @@ void AuthWebServer::webThread() d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries//", &apiServerAutoprimaryDetailDELETE, "DELETE"); d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries", &apiServerAutoprimariesGET, "GET"); d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries", &apiServerAutoprimariesPOST, "POST"); + d_ws->registerApiHandler("/api/v1/servers/localhost/networks", apiServerNetworksGET, "GET"); + d_ws->registerApiHandler("/api/v1/servers/localhost/networks//", apiServerNetworksGET, "GET"); + d_ws->registerApiHandler("/api/v1/servers/localhost/networks//", apiServerNetworksPUT, "PUT"); d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/", apiServerTSIGKeyDetailGET, "GET"); d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/", apiServerTSIGKeyDetailPUT, "PUT"); d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/", apiServerTSIGKeyDetailDELETE, "DELETE"); d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys", apiServerTSIGKeysGET, "GET"); d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys", apiServerTSIGKeysPOST, "POST"); + d_ws->registerApiHandler("/api/v1/servers/localhost/views", apiServerViewsAllGET, "GET"); + d_ws->registerApiHandler("/api/v1/servers/localhost/views/", apiServerViewsGET, "GET"); + d_ws->registerApiHandler("/api/v1/servers/localhost/views/", apiServerViewsPOST, "POST"); + d_ws->registerApiHandler("/api/v1/servers/localhost/views//", apiServerViewsDELETE, "DELETE"); d_ws->registerApiHandler("/api/v1/servers/localhost/zones//axfr-retrieve", apiServerZoneAxfrRetrieve, "PUT"); d_ws->registerApiHandler("/api/v1/servers/localhost/zones//cryptokeys/", apiZoneCryptokeysGET, "GET"); d_ws->registerApiHandler("/api/v1/servers/localhost/zones//cryptokeys/", apiZoneCryptokeysPOST, "POST");