From: Francis Dupont Date: Mon, 13 Jul 2020 12:57:17 +0000 (+0200) Subject: [#1304] Checkpoint: HA/CA tests to add X-Git-Tag: Kea-1.9.0~143 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=de85e6e564667453d21df038682f2e8f81dba0dd;p=thirdparty%2Fkea.git [#1304] Checkpoint: HA/CA tests to add --- diff --git a/doc/sphinx/arm/hooks-ha.rst b/doc/sphinx/arm/hooks-ha.rst index 0da4ea04ed..a679cabaeb 100644 --- a/doc/sphinx/arm/hooks-ha.rst +++ b/doc/sphinx/arm/hooks-ha.rst @@ -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 diff --git a/src/hooks/dhcp/high_availability/ha_config.cc b/src/hooks/dhcp/high_availability/ha_config.cc index 6721040abe..d9aa89d0b2 100644 --- a/src/hooks/dhcp/high_availability/ha_config.cc +++ b/src/hooks/dhcp/high_availability/ha_config.cc @@ -10,13 +10,15 @@ #include #include +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) { } diff --git a/src/hooks/dhcp/high_availability/ha_config.h b/src/hooks/dhcp/high_availability/ha_config.h index 83b9891d2c..95c81b47c9 100644 --- a/src/hooks/dhcp/high_availability/ha_config.h +++ b/src/hooks/dhcp/high_availability/ha_config.h @@ -8,6 +8,8 @@ #define HA_CONFIG_H #include +#include +#include #include #include #include @@ -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. diff --git a/src/hooks/dhcp/high_availability/ha_config_parser.cc b/src/hooks/dhcp/high_availability/ha_config_parser.cc index 5e71910dce..7cba9c10cd 100644 --- a/src/hooks/dhcp/high_availability/ha_config_parser.cc +++ b/src/hooks/dhcp/high_availability/ha_config_parser.cc @@ -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. diff --git a/src/hooks/dhcp/high_availability/ha_service.cc b/src/hooks/dhcp/high_availability/ha_service.cc index 159a2cb377..345b2abf34 100644 --- a/src/hooks/dhcp/high_availability/ha_service.cc +++ b/src/hooks/dhcp/high_availability/ha_service.cc @@ -986,6 +986,7 @@ HAService::asyncSendLeaseUpdate(const QueryPtrType& query, PostHttpRequestJsonPtr request = boost::make_shared (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 (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 (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 (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(last_lease), config_->getSyncPageLimit())); @@ -1949,6 +1954,7 @@ HAService::processMaintenanceStart() { PostHttpRequestJsonPtr request = boost::make_shared (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 (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(); diff --git a/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc b/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc index eae27780a7..3056dd39c5 100644 --- a/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc +++ b/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc @@ -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) {