]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add an option for unauthenticated access to the API 11514/head
authorRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 20 Jan 2022 14:27:37 +0000 (15:27 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 7 Apr 2022 14:38:02 +0000 (16:38 +0200)
pdns/dnsdist-lua.cc
pdns/dnsdist-web.cc
pdns/dnsdistdist/dnsdist-web.hh
pdns/dnsdistdist/docs/reference/config.rst
regression-tests.dnsdist/test_API.py

index 2f8d858c9bdb89c0538b168c1c2b75fb1fbea226..ee393b5d0e3aecfff69e234fb47fa659a71845fd 100644 (file)
@@ -1091,6 +1091,10 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
       setWebserverStatsRequireAuthentication(boost::get<bool>(vars->at("statsRequireAuthentication")));
     }
 
+    if (vars->count("apiRequiresAuthentication")) {
+      setWebserverAPIRequiresAuthentication(boost::get<bool>(vars->at("apiRequiresAuthentication")));
+    }
+
     if (vars->count("maxConcurrentConnections")) {
       setWebserverMaxConcurrentConnections(std::stoi(boost::get<std::string>(vars->at("maxConcurrentConnections"))));
     }
index 7398cf019dd078d8731797e1c6548a6bfa623852..0c03c8e2f4ff177d2a9f7d845bd2b479824850a4 100644 (file)
@@ -52,6 +52,7 @@ struct WebserverConfig
   std::unique_ptr<CredentialsHolder> password;
   std::unique_ptr<CredentialsHolder> apiKey;
   boost::optional<std::unordered_map<std::string, std::string> > customHeaders;
+  bool apiRequiresAuthentication{true};
   bool statsRequireAuthentication{true};
 };
 
@@ -79,6 +80,7 @@ std::string getWebserverConfig()
     else {
       out << "None" << endl;
     }
+    out << "API requires authentication: " << (config->apiRequiresAuthentication ? "yes" : "no") << endl;
     out << "Statistics require authentication: " << (config->statsRequireAuthentication ? "yes" : "no") << endl;
     out << "Password: " << (config->password ? "set" : "unset") << endl;
     out << "API key: " << (config->apiKey ? "set" : "unset") << endl;
@@ -304,7 +306,7 @@ static bool handleAuthorization(const YaHTTP::Request& req)
 
   if (isAnAPIRequest(req)) {
     /* Access to the API requires a valid API key */
-    if (checkAPIKey(req, config->apiKey)) {
+    if (!config->apiRequiresAuthentication  || checkAPIKey(req, config->apiKey)) {
       return true;
     }
 
@@ -1558,6 +1560,11 @@ void setWebserverStatsRequireAuthentication(bool require)
   g_webserverConfig.lock()->statsRequireAuthentication = require;
 }
 
+void setWebserverAPIRequiresAuthentication(bool require)
+{
+  g_webserverConfig.lock()->apiRequiresAuthentication = require;
+}
+
 void setWebserverMaxConcurrentConnections(size_t max)
 {
   s_connManager.setMaxConcurrentConnections(max);
index e021e5866c7d00af4eaed8d616ebbf7e254ce949..e33469a9cd6358f5a8bf8ca60794395e6d9dd2e0 100644 (file)
@@ -6,6 +6,7 @@ void setWebserverAPIKey(std::unique_ptr<CredentialsHolder>&& apiKey);
 void setWebserverPassword(std::unique_ptr<CredentialsHolder>&& password);
 void setWebserverACL(const std::string& acl);
 void setWebserverCustomHeaders(const boost::optional<std::unordered_map<std::string, std::string> > customHeaders);
+void setWebserverAPIRequiresAuthentication(bool);
 void setWebserverStatsRequireAuthentication(bool);
 void setWebserverMaxConcurrentConnections(size_t);
 
index 33987be6b3c9263b28347432130e9057d831519e..0f3a96868ecef33dc3fe72ccd5d02c44f29d2c4f 100644 (file)
@@ -358,6 +358,9 @@ Webserver configuration
     The optional ``password`` and ``apiKey`` parameters now accept hashed passwords.
     The optional ``hashPlaintextCredentials`` parameter has been added.
 
+  .. versionchanged:: 1.6.0
+    ``apiRequiresAuthentication`` optional parameters added.
+
   Setup webserver configuration. See :func:`webserver`.
 
   :param table options: A table with key: value pairs with webserver options.
@@ -368,6 +371,7 @@ Webserver configuration
   * ``apiKey=newKey``: string - Changes the API Key (set to an empty string do disable it). Since 1.7.0 the key should be hashed and salted via the :func:`hashPassword` command.
   * ``customHeaders={[str]=str,...}``: map of string - Allows setting custom headers and removing the defaults.
   * ``acl=newACL``: string - List of IP addresses, as a string, that are allowed to open a connection to the web server. Defaults to "127.0.0.1, ::1".
+  * ``apiRequiresAuthentication``: bool - Whether access to the API (/api endpoints) require a valid API key. Defaults to true.
   * ``statsRequireAuthentication``: bool - Whether access to the statistics (/metrics and /jsonstat endpoints) require a valid password or API key. Defaults to true.
   * ``maxConcurrentConnections``: int - The maximum number of concurrent web connections, or 0 which means an unlimited number. Defaults to 100.
   * ``hashPlaintextCredentials``: bool - Whether passwords and API keys provided in plaintext should be hashed during startup, to prevent the plaintext versions from staying in memory. Doing so increases significantly the cost of verifying credentials. Defaults to false.
index 98141fdc0de68fa396e3bc66086025ffc7e06d7d..d58dd7d80f684d61b97e1ccaa1d2c3b22be15a73 100644 (file)
@@ -643,6 +643,42 @@ class TestAPIACL(APITestsBase):
         self.assertTrue(r)
         self.assertEqual(r.status_code, 200)
 
+class TestAPIWithoutAuthentication(APITestsBase):
+    __test__ = True
+    _apiPath = '/api/v1/servers/localhost/config'
+    # paths accessible using basic auth only (list not exhaustive)
+    _basicOnlyPath = '/'
+    _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
+    _config_template = """
+    setACL({"127.0.0.1/32", "::1/128"})
+    newServer({address="127.0.0.1:%s"})
+    webserver("127.0.0.1:%s")
+    setWebserverConfig({password="%s", apiRequiresAuthentication=false })
+    """
+
+    def testAuth(self):
+        """
+        API: API do not require authentication
+        """
+
+        for path in [self._apiPath]:
+            url = 'http://127.0.0.1:' + str(self._webServerPort) + path
+
+            r = requests.get(url, timeout=self._webTimeout)
+            self.assertTrue(r)
+            self.assertEqual(r.status_code, 200)
+
+        # these should still require basic authentication
+        for path in [self._basicOnlyPath]:
+            url = 'http://127.0.0.1:' + str(self._webServerPort) + path
+
+            r = requests.get(url, timeout=self._webTimeout)
+            self.assertEqual(r.status_code, 401)
+
+            r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
+            self.assertTrue(r)
+            self.assertEqual(r.status_code, 200)
+
 class TestCustomLuaEndpoint(APITestsBase):
     __test__ = True
     _config_template = """