]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#1304] Checkpoint: HA/CA tests to add
authorFrancis Dupont <fdupont@isc.org>
Mon, 13 Jul 2020 12:57:17 +0000 (14:57 +0200)
committerFrancis Dupont <fdupont@isc.org>
Sat, 12 Sep 2020 08:50:34 +0000 (10:50 +0200)
doc/sphinx/arm/hooks-ha.rst
src/hooks/dhcp/high_availability/ha_config.cc
src/hooks/dhcp/high_availability/ha_config.h
src/hooks/dhcp/high_availability/ha_config_parser.cc
src/hooks/dhcp/high_availability/ha_service.cc
src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc

index 0da4ea04ed2b5664966075b14a17f9c5750f21bc..a679cabaeb8e7313d920e3b8f61f52655bb6651b 100644 (file)
@@ -470,6 +470,8 @@ with the only difference that ``this-server-name`` should be set to
                        "name": "server3",
                        "url": "http://192.168.56.99:8000/",
                        "role": "backup",
+                       "basic-auth-user": "foo",
+                       "basic-auth-password": "bar",
                        "auto-failover": false
                    }]
                }]
@@ -579,6 +581,9 @@ server. It may also contain an unlimited number of backup servers. In
 this example, there is one backup server which receives lease updates
 from the active servers.
 
+Since Kea version 1.7.10 the basic HTTP authentication is available
+to protect the Kea control agent against local attackers.
+
 These are the parameters specified for each of the peers within this
 list:
 
@@ -588,6 +593,15 @@ list:
    the control channel. Other servers use this URL to send control
    commands to that server.
 
+-  ``basic-auth-user`` - specifies the user id for basic HTTP
+   authentication. If not specified or specified to the empty string
+   no authentication header will be added to HTTP transactions.
+   Must not contain the colon (:) character.
+
+-  ``basic-auth-password`` - specifies the password for basic HTTP
+   authentication. Ignored when the user id is not specified or empty.
+   The password is optional: if not specified an empty password is used.
+
 -  ``role`` - denotes the role of the server in the HA setup. The
    following roles are supported in the load-balancing configuration:
    ``primary``, ``secondary``, and ``backup``. There must be exactly one
@@ -608,7 +622,9 @@ state, it can serve leases from both pools and it selects the pool which
 is appropriate for the received query. In other words, if the query
 would normally be processed by ``server2`` but this server is not
 available, ``server1`` will allocate the lease from the pool of
-"192.0.3.200 - 192.0.3.250".
+"192.0.3.200 - 192.0.3.250". The Kea control agent in front of the
+``server3`` requires basic HTTP authentication and authorizes the
+user id "foo" with the password "bar".
 
 .. _ha-load-balancing-advanced-config:
 
@@ -754,6 +770,8 @@ hot-standby configuration:
                    }, {
                        "name": "server3",
                        "url": "http://192.168.56.99:8000/",
+                       "basic-auth-user": "foo",
+                       "basic-auth-password": "bar",
                        "role": "backup",
                        "auto-failover": false
                    }]
@@ -830,6 +848,8 @@ passive-backup configuration:
                    }, {
                        "name": "server3",
                        "url": "http://192.168.56.99:8000/",
+                       "basic-auth-user": "foo",
+                       "basic-auth-password": "bar",
                        "role": "backup"
                    }]
                }]
@@ -1271,6 +1291,8 @@ load-balancing and the hot-standby cases presented in previous sections.
    }
    }
 
+Since Kea version 1.7.10 the basic HTTP authentication is supported.
+
 .. _ha-maintenance:
 
 Controlled Shutdown and Maintenance of DHCP servers
index 6721040abecffd59f51a03f31de61cb8a223525a..d9aa89d0b23d0a6d5c268fc0a09b2f6326908351 100644 (file)
 #include <ha_service_states.h>
 #include <sstream>
 
+using namespace isc::http;
 using namespace isc::util;
 
 namespace isc {
 namespace ha {
 
 HAConfig::PeerConfig::PeerConfig()
-    : name_(), url_(""), role_(STANDBY), auto_failover_(false) {
+    : name_(), url_(""), role_(STANDBY), auto_failover_(false),
+      basic_auth_() {
 }
 
 void
@@ -79,6 +81,15 @@ HAConfig::PeerConfig::roleToString(const HAConfig::PeerConfig::Role& role) {
     return ("");
 }
 
+void
+HAConfig::PeerConfig::addBasicAuthHttpHeader(PostHttpRequestJsonPtr request) const {
+    const BasicHttpAuthPtr& auth = getBasicAuth();
+    if (!request || !auth) {
+        return;
+    }
+    request->context()->headers_.push_back(BasicAuthHttpHeaderContext(*auth));
+}
+
 HAConfig::StateConfig::StateConfig(const int state)
     : state_(state), pausing_(STATE_PAUSE_NEVER) {
 }
index 83b9891d2cbf05d15f6459a2b4bbeb2618e1b972..95c81b47c931e07892ce3e6b3f90ec750e83c1b3 100644 (file)
@@ -8,6 +8,8 @@
 #define HA_CONFIG_H
 
 #include <exceptions/exceptions.h>
+#include <http/basic_auth.h>
+#include <http/post_request_json.h>
 #include <http/url.h>
 #include <util/state_model.h>
 #include <boost/shared_ptr.hpp>
@@ -148,13 +150,26 @@ public:
             auto_failover_ = auto_failover;
         }
 
-    private:
+        /// @brief Returns non-const basic HTTP authentication.
+        http::BasicHttpAuthPtr& getBasicAuth() {
+            return (basic_auth_);
+        }
+
+        /// @brief Returns const basic HTTP authentication.
+        const http::BasicHttpAuthPtr& getBasicAuth() const {
+            return (basic_auth_);
+        }
+
+        /// @brief Adds a basic HTTP authentication header to a request.
+        void addBasicAuthHttpHeader(http::PostHttpRequestJsonPtr request) const;
 
-        std::string name_;   ///< Server name.
-        http::Url url_;      ///< Server URL.
-        Role role_;          ///< Server role.
-        bool auto_failover_; ///< Auto failover state.
+    private:
 
+        std::string name_;                  ///< Server name.
+        http::Url url_;                     ///< Server URL.
+        Role role_;                         ///< Server role.
+        bool auto_failover_;                ///< Auto failover state.
+        http::BasicHttpAuthPtr basic_auth_; ///< Basic HTTP authentication.
     };
 
     /// @brief Pointer to the server's configuration.
index 5e71910dce9655567a9b8f4ec0c51abcdb5c0126..7cba9c10cd9013e3f2fc1f3b8b506bff460a9f00 100644 (file)
@@ -185,6 +185,27 @@ HAConfigParser::parseInternal(const HAConfigPtr& config_storage,
 
         // Auto failover configuration.
         cfg->setAutoFailover(getBoolean(*p, "auto-failover"));
+
+        // Basic HTTP authentication password.
+        std::string password;
+        if ((*p)->contains("basic-auth-password")) {
+            password = getString((*p), "basic-auth-password");
+        }
+
+        // Basic HTTP authentication user.
+        if ((*p)->contains("basic-auth-user")) {
+            std::string user = getString((*p), "basic-auth-user");
+            BasicHttpAuthPtr& auth = cfg->getBasicAuth();
+            try {
+                if (!user.empty()) {
+                    // Validate the user id value.
+                    auth.reset(new BasicHttpAuth(user, password));
+                }
+            } catch (const std::exception& ex) {
+                isc_throw(dhcp::DhcpConfigError, ex.what() << " in peer '"
+                          << cfg->getName() << "'");
+            }
+        }
     }
 
     // Per state configuration is optional.
index 159a2cb377dca01023a869b68d2ca98c92787f03..345b2abf34c10ceb01e28f4335f2fd9a9ab3deea 100644 (file)
@@ -986,6 +986,7 @@ HAService::asyncSendLeaseUpdate(const QueryPtrType& query,
     PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
         (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11(),
          HostHttpHeader(config->getUrl().getHostname()));
+    config->addBasicAuthHttpHeader(request);
     request->setBodyAsJson(command);
     request->finalize();
 
@@ -1268,6 +1269,7 @@ HAService::asyncSendHeartbeat() {
     PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
         (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11(),
          HostHttpHeader(partner_config->getUrl().getHostname()));
+    partner_config->addBasicAuthHttpHeader(request);
     request->setBodyAsJson(CommandCreator::createHeartbeat(server_type_));
     request->finalize();
 
@@ -1406,6 +1408,7 @@ HAService::asyncDisableDHCPService(HttpClient& http_client,
         (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11(),
          HostHttpHeader(remote_config->getUrl().getHostname()));
 
+    remote_config->addBasicAuthHttpHeader(request);
     request->setBodyAsJson(CommandCreator::createDHCPDisable(max_period,
                                                              server_type_));
     request->finalize();
@@ -1479,6 +1482,7 @@ HAService::asyncEnableDHCPService(HttpClient& http_client,
     PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
         (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11(),
          HostHttpHeader(remote_config->getUrl().getHostname()));
+    remote_config->addBasicAuthHttpHeader(request);
     request->setBodyAsJson(CommandCreator::createDHCPEnable(server_type_));
     request->finalize();
 
@@ -1612,6 +1616,7 @@ HAService::asyncSyncLeasesInternal(http::HttpClient& http_client,
     PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
         (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11(),
          HostHttpHeader(partner_config->getUrl().getHostname()));
+    partner_config->addBasicAuthHttpHeader(request);
     if (server_type_ == HAServerType::DHCPv4) {
         request->setBodyAsJson(CommandCreator::createLease4GetPage(
             boost::dynamic_pointer_cast<Lease4>(last_lease), config_->getSyncPageLimit()));
@@ -1949,6 +1954,7 @@ HAService::processMaintenanceStart() {
     PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
         (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11(),
          HostHttpHeader(remote_config->getUrl().getHostname()));
+    remote_config->addBasicAuthHttpHeader(request);
     request->setBodyAsJson(CommandCreator::createMaintenanceNotify(false, server_type_));
     request->finalize();
 
@@ -2070,6 +2076,7 @@ HAService::processMaintenanceCancel() {
     PostHttpRequestJsonPtr request = boost::make_shared<PostHttpRequestJson>
         (HttpRequest::Method::HTTP_POST, "/", HttpVersion::HTTP_11(),
          HostHttpHeader(remote_config->getUrl().getHostname()));
+    remote_config->addBasicAuthHttpHeader(request);
     request->setBodyAsJson(CommandCreator::createMaintenanceNotify(true, server_type_));
     request->finalize();
 
index eae27780a74e537b6e67e093142d4a3f2ff6fcf2..3056dd39c59e73e8703fbc10f2ce40e623df3a5a 100644 (file)
@@ -79,16 +79,20 @@ TEST_F(HAConfigTest, configureLoadBalancing) {
         "                \"name\": \"server1\","
         "                \"url\": \"http://127.0.0.1:8080/\","
         "                \"role\": \"primary\","
+        "                \"basic-auth-password\": \"1234\","
         "                \"auto-failover\": false"
         "            },"
         "            {"
         "                \"name\": \"server2\","
         "                \"url\": \"http://127.0.0.1:8081/\","
+        "                \"basic-auth-user\": \"\","
         "                \"role\": \"secondary\""
         "            },"
         "            {"
         "                \"name\": \"server3\","
         "                \"url\": \"http://127.0.0.1:8082/\","
+        "                \"basic-auth-user\": \"foo\","
+        "                \"basic-auth-password\": \"bar\","
         "                \"role\": \"backup\","
         "                \"auto-failover\": false"
         "            }"
@@ -133,6 +137,7 @@ TEST_F(HAConfigTest, configureLoadBalancing) {
     EXPECT_EQ(cfg->getLogLabel(), "server1 (http://127.0.0.1:8080/)");
     EXPECT_EQ(HAConfig::PeerConfig::PRIMARY, cfg->getRole());
     EXPECT_FALSE(cfg->isAutoFailover());
+    EXPECT_FALSE(cfg->getBasicAuth());
 
     cfg = impl->getConfig()->getPeerConfig("server2");
     ASSERT_TRUE(cfg);
@@ -141,6 +146,7 @@ TEST_F(HAConfigTest, configureLoadBalancing) {
     EXPECT_EQ(cfg->getLogLabel(), "server2 (http://127.0.0.1:8081/)");
     EXPECT_EQ(HAConfig::PeerConfig::SECONDARY, cfg->getRole());
     EXPECT_TRUE(cfg->isAutoFailover());
+    EXPECT_FALSE(cfg->getBasicAuth());
 
     cfg = impl->getConfig()->getPeerConfig("server3");
     ASSERT_TRUE(cfg);
@@ -149,6 +155,8 @@ TEST_F(HAConfigTest, configureLoadBalancing) {
     EXPECT_EQ(cfg->getLogLabel(), "server3 (http://127.0.0.1:8082/)");
     EXPECT_EQ(HAConfig::PeerConfig::BACKUP, cfg->getRole());
     EXPECT_FALSE(cfg->isAutoFailover());
+    ASSERT_TRUE(cfg->getBasicAuth());
+    EXPECT_EQ("foo:bar", cfg->getBasicAuth()->getSecret());
 
     // Verify that per-state configuration is correct.x
 
@@ -200,6 +208,7 @@ TEST_F(HAConfigTest, configureHotStandby) {
         "            {"
         "                \"name\": \"server1\","
         "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"basic-auth-user\": \"admin\","
         "                \"role\": \"primary\","
         "                \"auto-failover\": false"
         "            },"
@@ -238,6 +247,8 @@ TEST_F(HAConfigTest, configureHotStandby) {
     EXPECT_EQ("http://127.0.0.1:8080/", cfg->getUrl().toText());
     EXPECT_EQ(HAConfig::PeerConfig::PRIMARY, cfg->getRole());
     EXPECT_FALSE(cfg->isAutoFailover());
+    ASSERT_TRUE(cfg->getBasicAuth());
+    EXPECT_EQ("admin:", cfg->getBasicAuth()->getSecret());
 
     cfg = impl->getConfig()->getPeerConfig("server2");
     ASSERT_TRUE(cfg);
@@ -245,6 +256,7 @@ TEST_F(HAConfigTest, configureHotStandby) {
     EXPECT_EQ("http://127.0.0.1:8081/", cfg->getUrl().toText());
     EXPECT_EQ(HAConfig::PeerConfig::STANDBY, cfg->getRole());
     EXPECT_TRUE(cfg->isAutoFailover());
+    EXPECT_FALSE(cfg->getBasicAuth());
 
     cfg = impl->getConfig()->getPeerConfig("server3");
     ASSERT_TRUE(cfg);
@@ -252,6 +264,7 @@ TEST_F(HAConfigTest, configureHotStandby) {
     EXPECT_EQ("http://127.0.0.1:8082/", cfg->getUrl().toText());
     EXPECT_EQ(HAConfig::PeerConfig::BACKUP, cfg->getRole());
     EXPECT_FALSE(cfg->isAutoFailover());
+    EXPECT_FALSE(cfg->getBasicAuth());
 
     HAConfig::StateConfigPtr state_cfg;
     ASSERT_NO_THROW(state_cfg = impl->getConfig()->getStateMachineConfig()->
@@ -312,6 +325,8 @@ TEST_F(HAConfigTest, configurePassiveBackup) {
         "            {"
         "                \"name\": \"server3\","
         "                \"url\": \"http://127.0.0.1:8082/\","
+        "                \"basic-auth-user\": \"test\","
+        "                \"basic-auth-password\": \"123\\u00a3\","
         "                \"role\": \"backup\""
         "            }"
         "        ]"
@@ -330,18 +345,22 @@ TEST_F(HAConfigTest, configurePassiveBackup) {
     EXPECT_EQ("server1", cfg->getName());
     EXPECT_EQ("http://127.0.0.1:8080/", cfg->getUrl().toText());
     EXPECT_EQ(HAConfig::PeerConfig::PRIMARY, cfg->getRole());
+    EXPECT_FALSE(cfg->getBasicAuth());
 
     cfg = impl->getConfig()->getPeerConfig("server2");
     ASSERT_TRUE(cfg);
     EXPECT_EQ("server2", cfg->getName());
     EXPECT_EQ("http://127.0.0.1:8081/", cfg->getUrl().toText());
     EXPECT_EQ(HAConfig::PeerConfig::BACKUP, cfg->getRole());
+    EXPECT_FALSE(cfg->getBasicAuth());
 
     cfg = impl->getConfig()->getPeerConfig("server3");
     ASSERT_TRUE(cfg);
     EXPECT_EQ("server3", cfg->getName());
     EXPECT_EQ("http://127.0.0.1:8082/", cfg->getUrl().toText());
     EXPECT_EQ(HAConfig::PeerConfig::BACKUP, cfg->getRole());
+    ASSERT_TRUE(cfg->getBasicAuth());
+    EXPECT_EQ("dGVzdDoxMjPCow==", cfg->getBasicAuth()->getCredential());
 }
 
 // This server name must not be empty.
@@ -1135,6 +1154,33 @@ TEST_F(HAConfigTest, passiveBackupNoPrimary) {
         "primary server required in the passive backup configuration");
 }
 
+// Test that empty name id is forbidden for basic HTTP authentication.
+TEST_F(HAConfigTest, invalidUser) {
+    testInvalidConfig(
+        "["
+        "    {"
+        "        \"this-server-name\": \"server1\","
+        "        \"mode\": \"load-balancing\","
+        "        \"peers\": ["
+        "            {"
+        "                \"name\": \"server1\","
+        "                \"url\": \"http://127.0.0.1:8080/\","
+        "                \"role\": \"primary\","
+        "                \"auto-failover\": false"
+        "            },"
+        "            {"
+        "                \"name\": \"server2\","
+        "                \"url\": \":http//127.0.0.1:8080/\","
+        "                \"basic-auth-user\": \"foo:bar\","
+        "                \"role\": \"secondary\","
+        "                \"auto-failover\": true"
+        "            }"
+        "        ]"
+        "    }"
+        "]",
+        "user 'foo:bar' must not contain a ':' in peer 'server2'");
+}
+
 
 // Test that conversion of the role names works correctly.
 TEST_F(HAConfigTest, stringToRole) {