]> granicus.if.org Git - pdns/commitdiff
dnsdist: Make the API available with an API key only
authorRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 13 Jul 2017 13:49:08 +0000 (15:49 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 10 Aug 2017 14:44:57 +0000 (16:44 +0200)
pdns/dnsdist-web.cc
regression-tests.dnsdist/test_API.py

index f82fe5f66de3cc440a3e31250b2ca2b97120a681..feb0ff6a6867e648aa9bfcaccfb4da346667eaf0 100644 (file)
@@ -79,13 +79,28 @@ static void apiSaveACL(const NetmaskGroup& nmg)
   apiWriteConfigFile("acl", content);
 }
 
-static bool compareAuthorization(YaHTTP::Request& req, const string &expected_password, const string& expectedApiKey)
+static bool checkAPIKey(const YaHTTP::Request& req, const string& expectedApiKey)
 {
-  // validate password
-  YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
-  bool auth_ok = false;
-  if (header != req.headers.end() && toLower(header->second).find("basic ") == 0) {
-    string cookie = header->second.substr(6);
+  if (expectedApiKey.empty()) {
+    return false;
+  }
+
+  const auto header = req.headers.find("x-api-key");
+  if (header != req.headers.end()) {
+    return (header->second == expectedApiKey);
+  }
+
+  return false;
+}
+
+static bool checkWebPassword(const YaHTTP::Request& req, const string &expected_password)
+{
+  static const char basicStr[] = "basic ";
+
+  const auto header = req.headers.find("authorization");
+
+  if (header != req.headers.end() && toLower(header->second).find(basicStr) == 0) {
+    string cookie = header->second.substr(sizeof(basicStr) - 1);
 
     string plain;
     B64Decode(cookie, plain);
@@ -93,21 +108,46 @@ static bool compareAuthorization(YaHTTP::Request& req, const string &expected_pa
     vector<string> cparts;
     stringtok(cparts, plain, ":");
 
-    // this gets rid of terminating zeros
-    auth_ok = (cparts.size()==2 && (0==strcmp(cparts[1].c_str(), expected_password.c_str())));
+    if (cparts.size() == 2) {
+      return cparts[1] == expected_password;
+    }
   }
-  if (!auth_ok && !expectedApiKey.empty()) {
-    /* if this is a request for the API,
-       check if the API key is correct */
-    if (req.url.path=="/jsonstat" ||
-        req.url.path.find("/api/") == 0) {
-      header = req.headers.find("x-api-key");
-      if (header != req.headers.end()) {
-        auth_ok = (0==strcmp(header->second.c_str(), expectedApiKey.c_str()));
-      }
+
+  return false;
+}
+
+static bool isAnAPIRequest(const YaHTTP::Request& req)
+{
+  return req.url.path.find("/api/") == 0;
+}
+
+static bool isAnAPIRequestAllowedWithWebAuth(const YaHTTP::Request& req)
+{
+  return req.url.path == "/api/v1/servers/localhost";
+}
+
+static bool isAStatsRequest(const YaHTTP::Request& req)
+{
+  return req.url.path == "/jsonstat";
+}
+
+static bool compareAuthorization(const YaHTTP::Request& req, const string &expected_password, const string& expectedApiKey)
+{
+  if (isAnAPIRequest(req)) {
+    /* Access to the API requires a valid API key */
+    if (checkAPIKey(req, expectedApiKey)) {
+      return true;
     }
+
+    return isAnAPIRequestAllowedWithWebAuth(req) && checkWebPassword(req, expected_password);
+  }
+
+  if (isAStatsRequest(req)) {
+    /* Access to the stats is allowed for both API and Web users */
+    return checkAPIKey(req, expectedApiKey) || checkWebPassword(req, expected_password);
   }
-  return auth_ok;
+
+  return checkWebPassword(req, expected_password);
 }
 
 static bool isMethodAllowed(const YaHTTP::Request& req)
@@ -123,9 +163,9 @@ static bool isMethodAllowed(const YaHTTP::Request& req)
   return false;
 }
 
-static void handleCORS(YaHTTP::Request& req, YaHTTP::Response& resp)
+static void handleCORS(const YaHTTP::Request& req, YaHTTP::Response& resp)
 {
-  YaHTTP::strstr_map_t::iterator origin = req.headers.find("Origin");
+  const auto origin = req.headers.find("Origin");
   if (origin != req.headers.end()) {
     if (req.method == "OPTIONS") {
       /* Pre-flight request */
@@ -139,7 +179,10 @@ static void handleCORS(YaHTTP::Request& req, YaHTTP::Response& resp)
     }
 
     resp.headers["Access-Control-Allow-Origin"] = origin->second;
-    resp.headers["Access-Control-Allow-Credentials"] = "true";
+
+    if (isAStatsRequest(req) || isAnAPIRequestAllowedWithWebAuth(req)) {
+      resp.headers["Access-Control-Allow-Credentials"] = "true";
+    }
   }
 }
 
index b5312d36c992bab46c5515815e48a1b3405ec568..2fd53d8d4f2e19e83806f2b5bb15b88315fd8df6 100644 (file)
@@ -11,8 +11,10 @@ class TestAPIBasics(DNSDistTest):
     _webServerPort = 8083
     _webServerBasicAuthPassword = 'secret'
     _webServerAPIKey = 'apisecret'
-    # paths accessible using the API key
-    _apiPaths = ['/api/v1/servers/localhost', '/api/v1/servers/localhost/config', '/api/v1/servers/localhost/config/allow-from', '/api/v1/servers/localhost/statistics', '/jsonstat?command=stats', '/jsonstat?command=dynblocklist']
+    # paths accessible using the API key only
+    _apiOnlyPaths = ['/api/v1/servers/localhost/config', '/api/v1/servers/localhost/config/allow-from', '/api/v1/servers/localhost/statistics']
+    # paths accessible using an API key or basic auth
+    _statsPaths = [ '/jsonstat?command=stats', '/jsonstat?command=dynblocklist', '/api/v1/servers/localhost']
     # paths accessible using basic auth only (list not exhaustive)
     _basicOnlyPaths = ['/', '/index.html']
     _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
@@ -26,7 +28,7 @@ class TestAPIBasics(DNSDistTest):
         """
         API: Basic Authentication
         """
-        for path in self._basicOnlyPaths + self._apiPaths:
+        for path in self._basicOnlyPaths + self._statsPaths:
             url = 'http://127.0.0.1:' + str(self._webServerPort) + path
             r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
             self.assertTrue(r)
@@ -37,7 +39,7 @@ class TestAPIBasics(DNSDistTest):
         API: X-Api-Key
         """
         headers = {'x-api-key': self._webServerAPIKey}
-        for path in self._apiPaths:
+        for path in self._apiOnlyPaths + self._statsPaths:
             url = 'http://127.0.0.1:' + str(self._webServerPort) + path
             r = requests.get(url, headers=headers, timeout=self._webTimeout)
             self.assertTrue(r)
@@ -53,6 +55,15 @@ class TestAPIBasics(DNSDistTest):
             r = requests.get(url, headers=headers, timeout=self._webTimeout)
             self.assertEquals(r.status_code, 401)
 
+    def testAPIKeyOnly(self):
+        """
+        API: API Key Only
+        """
+        for path in self._apiOnlyPaths:
+            url = 'http://127.0.0.1:' + str(self._webServerPort) + path
+            r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
+            self.assertEquals(r.status_code, 401)
+
     def testServersLocalhost(self):
         """
         API: /api/v1/servers/localhost