From 55afa5183c045c9c6bada38bbb2e56099d093ff1 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Thu, 13 Jul 2017 15:49:08 +0200 Subject: [PATCH] dnsdist: Make the API available with an API key only --- pdns/dnsdist-web.cc | 85 +++++++++++++++++++++------- regression-tests.dnsdist/test_API.py | 19 +++++-- 2 files changed, 79 insertions(+), 25 deletions(-) diff --git a/pdns/dnsdist-web.cc b/pdns/dnsdist-web.cc index f82fe5f66..feb0ff6a6 100644 --- a/pdns/dnsdist-web.cc +++ b/pdns/dnsdist-web.cc @@ -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 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"; + } } } diff --git a/regression-tests.dnsdist/test_API.py b/regression-tests.dnsdist/test_API.py index b5312d36c..2fd53d8d4 100644 --- a/regression-tests.dnsdist/test_API.py +++ b/regression-tests.dnsdist/test_API.py @@ -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 -- 2.40.0