1 /*
2 Copyright (c) 2020, 2020, Oracle and/or its affiliates.
3 
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License, version 2.0,
6 as published by the Free Software Foundation.
7 
8 This program is also distributed with certain software (including
9 but not limited to OpenSSL) that is licensed under separate terms,
10 as designated in a particular file or component or in included license
11 documentation.  The authors of MySQL hereby grant you an additional
12 permission to link the program and your derivative works with the
13 separately licensed software that they have included with MySQL.
14 
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 GNU General Public License for more details.
19 
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
23 */
24 
25 #include <chrono>
26 #include <thread>
27 
28 #include <gmock/gmock.h>
29 
30 #include "config_builder.h"
31 #include "keyring/keyring_manager.h"
32 #include "mock_server_rest_client.h"
33 #include "mock_server_testutils.h"
34 #include "mysql_session.h"
35 #include "mysqlrouter/cluster_metadata.h"
36 #include "rest_api_testutils.h"
37 #include "router_component_test.h"
38 #include "tcp_port_pool.h"
39 
40 using namespace std::chrono_literals;
41 
42 Path g_origin_path;
43 
44 struct Credentials {
45   std::string username;
46   std::string password_hash;
47 };
48 
49 struct Auth_data {
50   Credentials credentials;
51   std::string privileges;
52   std::string auth_method{"modular_crypt_format"};
53 };
54 
55 struct Http_response_details {
56   HttpStatusCode::key_type code;
57   std::string type;
58 };
59 
60 class MetadataHttpAuthTest : public RouterComponentTest {
61  protected:
get_metadata_cache_section(const std::chrono::milliseconds ttl=kTTL,const std::chrono::milliseconds auth_cache_ttl=kAuthCacheTTL,const std::chrono::milliseconds auth_cache_refresh_interval=kAuthCacheRefreshRate) const62   std::string get_metadata_cache_section(
63       const std::chrono::milliseconds ttl = kTTL,
64       const std::chrono::milliseconds auth_cache_ttl = kAuthCacheTTL,
65       const std::chrono::milliseconds auth_cache_refresh_interval =
66           kAuthCacheRefreshRate) const {
67     const auto ttl_str =
68         std::to_string(std::chrono::duration<double>(ttl).count());
69     const auto auth_cache_ttl_str =
70         std::to_string(std::chrono::duration<double>(auth_cache_ttl).count());
71     const auto auth_cache_refresh_interval_str = std::to_string(
72         std::chrono::duration<double>(auth_cache_refresh_interval).count());
73 
74     return kMetadataCacheSectionBase + "ttl=" + ttl_str +
75            "\n"
76            "auth_cache_ttl=" +
77            auth_cache_ttl_str +
78            "\n"
79            "auth_cache_refresh_interval=" +
80            auth_cache_refresh_interval_str +
81            "\n"
82            "\n";
83   }
84 
get_metadata_cache_routing_section(const uint16_t router_port) const85   std::string get_metadata_cache_routing_section(
86       const uint16_t router_port) const {
87     const std::string result = "[routing:test_default" +
88                                std::to_string(router_port) +
89                                "]\n"
90                                "bind_port=" +
91                                std::to_string(router_port) + "\n" +
92                                "destinations=metadata-cache://test/"
93                                "default?role=PRIMARY\nprotocol=classic\n"
94                                "routing_strategy=first-available\n";
95 
96     return result;
97   }
98 
auth_backend_settings() const99   virtual std::string auth_backend_settings() const {
100     return "[http_auth_backend:somebackend]\n"
101            "backend=metadata_cache\n";
102   }
103 
get_rest_section() const104   std::string get_rest_section() const {
105     const std::string result =
106         "[http_server]\n"
107         "port=" +
108         std::to_string(http_server_port) +
109         "\n"
110         "[rest_router]\n"
111         "require_realm = somerealm\n"
112         "[rest_api]\n"
113         "[http_auth_realm:somerealm]\n"
114         "backend = somebackend\n"
115         "method = basic\n"
116         "name = test\n" +
117         auth_backend_settings() +
118         "[rest_routing]\n"
119         "require_realm = somerealm\n";
120 
121     return result;
122   }
123 
create_state_file_content(const std::string & cluster_id,const unsigned view_id,const uint16_t & metadata_server_port)124   std::string create_state_file_content(const std::string &cluster_id,
125                                         const unsigned view_id,
126                                         const uint16_t &metadata_server_port) {
127     const std::string metadata_servers =
128         "\"mysql://127.0.0.1:" + std::to_string(metadata_server_port) + "\"";
129     // clang-format off
130     const std::string result =
131       "{"
132          R"("version": "1.0.0",)"
133          R"("metadata-cache": {)"
134            R"("group-replication-id": ")" + cluster_id + R"(",)"
135            R"("cluster-metadata-servers": [)" + metadata_servers + "],"
136            R"("view-id":)" + std::to_string(view_id) +
137           "}"
138         "}";
139     // clang-format on
140 
141     return result;
142   }
143 
launch_router(const std::string & metadata_cache_section,const int expected_errorcode=EXIT_SUCCESS)144   auto &launch_router(const std::string &metadata_cache_section,
145                       const int expected_errorcode = EXIT_SUCCESS) {
146     const std::string &temp_test_dir_str = temp_test_dir.name();
147 
148     const auto &routing_section =
149         get_metadata_cache_routing_section(router_port);
150 
151     const auto &rest_section = get_rest_section();
152 
153     SCOPED_TRACE("// Create a router state file");
154     const std::string state_file = create_state_file(
155         temp_test_dir_str,
156         create_state_file_content(cluster_id, view_id, cluster_node_port));
157 
158     const std::string masterkey_file =
159         Path(temp_test_dir_str).join("master.key").str();
160     const std::string keyring_file =
161         Path(temp_test_dir_str).join("keyring").str();
162     mysql_harness::init_keyring(keyring_file, masterkey_file, true);
163     mysql_harness::Keyring *keyring = mysql_harness::get_keyring();
164     keyring->store("mysql_router1_user", "password", "root");
165     mysql_harness::flush_keyring();
166     mysql_harness::reset_keyring();
167 
168     // launch the router with metadata-cache configuration
169     auto default_section = get_DEFAULT_defaults();
170     default_section["keyring_path"] = keyring_file;
171     default_section["master_key_path"] = masterkey_file;
172     default_section["dynamic_state"] = state_file;
173     const std::string conf_file = create_config_file(
174         temp_test_dir_str,
175         metadata_cache_section + routing_section + rest_section,
176         &default_section);
177 
178     return ProcessManager::launch_router({"-c", conf_file}, expected_errorcode);
179   }
180 
set_mock_metadata(const std::vector<Auth_data> & auth_data_collection,const uint16_t http_port,const std::string & gr_id,const uint16_t cluster_node_port,const bool error_on_md_query=false,const unsigned primary_id=0,const unsigned view_id=0,const mysqlrouter::MetadataSchemaVersion md_version={2, 0, 3}) const181   void set_mock_metadata(
182       const std::vector<Auth_data> &auth_data_collection,
183       const uint16_t http_port, const std::string &gr_id,
184       const uint16_t cluster_node_port, const bool error_on_md_query = false,
185       const unsigned primary_id = 0, const unsigned view_id = 0,
186       const mysqlrouter::MetadataSchemaVersion md_version = {2, 0, 3}) const {
187     auto json_doc = mock_GR_metadata_as_json(
188         gr_id, {cluster_node_port}, primary_id, view_id, error_on_md_query);
189 
190     JsonAllocator allocator;
191     JsonValue nodes(rapidjson::kArrayType);
192     for (const auto &auth_data : auth_data_collection) {
193       JsonValue node(rapidjson::kArrayType);
194       node.PushBack(JsonValue(auth_data.credentials.username.c_str(),
195                               auth_data.credentials.username.size(), allocator),
196                     allocator);
197 
198       node.PushBack(
199           JsonValue(auth_data.credentials.password_hash.c_str(),
200                     auth_data.credentials.password_hash.size(), allocator),
201           allocator);
202 
203       node.PushBack(JsonValue(auth_data.privileges.c_str(),
204                               auth_data.privileges.size(), allocator),
205                     allocator);
206 
207       node.PushBack(JsonValue(auth_data.auth_method.c_str(),
208                               auth_data.auth_method.size(), allocator),
209                     allocator);
210 
211       nodes.PushBack(node, allocator);
212     }
213 
214     json_doc.AddMember("rest_user_credentials", nodes, allocator);
215 
216     JsonValue metadata_version_node(rapidjson::kArrayType);
217     metadata_version_node.PushBack(md_version.major, allocator);
218     metadata_version_node.PushBack(md_version.minor, allocator);
219     metadata_version_node.PushBack(md_version.patch, allocator);
220     json_doc.AddMember("metadata_version", metadata_version_node, allocator);
221 
222     const auto json_str = json_to_string(json_doc);
223 
224     EXPECT_NO_THROW(MockServerRestClient(http_port).set_globals(json_str));
225   }
226 
get_rest_auth_queries_count(const std::string & json_string) const227   int get_rest_auth_queries_count(const std::string &json_string) const {
228     rapidjson::Document json_doc;
229     json_doc.Parse(json_string.c_str());
230     if (json_doc.HasMember("rest_auth_query_count")) {
231       EXPECT_TRUE(json_doc["rest_auth_query_count"].IsInt());
232       return json_doc["rest_auth_query_count"].GetInt();
233     }
234     return 0;
235   }
236 
wait_for_rest_auth_query(int expected_rest_auth_query_count,uint16_t http_port) const237   int wait_for_rest_auth_query(int expected_rest_auth_query_count,
238                                uint16_t http_port) const {
239     int rest_auth_queries_count{0}, retries{0};
240     do {
241       std::this_thread::sleep_for(50ms);
242       const std::string server_globals =
243           MockServerRestClient(http_port).get_globals_as_json_string();
244       rest_auth_queries_count = get_rest_auth_queries_count(server_globals);
245     } while (rest_auth_queries_count < expected_rest_auth_query_count &&
246              retries++ < 100);
247 
248     return rest_auth_queries_count;
249   }
250 
SetUp()251   void SetUp() override {
252     RouterComponentTest::SetUp();
253     ProcessManager::set_origin(g_origin_path);
254 
255     cluster_node_port = port_pool_.get_next_available();
256     cluster_http_port = port_pool_.get_next_available();
257     http_server_port = port_pool_.get_next_available();
258 
259     SCOPED_TRACE("// Launch a server mock that will act as our cluster member");
260     const auto trace_file =
261         get_data_dir().join("metadata_http_auth_backend.js").str();
262 
263     cluster_node = &ProcessManager::launch_mysql_server_mock(
264         trace_file, cluster_node_port, EXIT_SUCCESS, false, cluster_http_port);
265     ASSERT_NO_FATAL_FAILURE(check_port_ready(*cluster_node, cluster_node_port));
266     ASSERT_TRUE(
267         MockServerRestClient(cluster_http_port).wait_for_rest_endpoint_ready())
268         << cluster_node->get_full_output();
269 
270     router_port = port_pool_.get_next_available();
271 
272     uri = std::string(rest_api_basepath) + "/routes/test_default" +
273           std::to_string(router_port) + "/status";
274   }
275 
276   const std::string kMetadataCacheSectionBase =
277       "[metadata_cache:test]\n"
278       "cluster_type=gr\n"
279       "router_id=1\n"
280       "user=mysql_router1_user\n"
281       "metadata_cluster=test\n"
282       "connect_timeout=1\n";
283 
284   static const std::chrono::milliseconds kTTL;
285   static const std::chrono::milliseconds kAuthCacheTTL;
286   static const std::chrono::milliseconds kAuthCacheRefreshRate;
287   static const std::string cluster_id;
288   TempDirectory temp_test_dir;
289   unsigned view_id = 1;
290 
291   ProcessWrapper *cluster_node;
292   uint16_t cluster_node_port;
293   uint16_t cluster_http_port;
294   uint16_t http_server_port;
295   uint16_t router_port;
296 
297   std::string uri;
298 
299   TcpPortPool port_pool_;
300 };
301 
302 const std::chrono::milliseconds MetadataHttpAuthTest::kTTL = 200ms;
303 const std::chrono::milliseconds MetadataHttpAuthTest::kAuthCacheTTL = -1s;
304 const std::chrono::milliseconds MetadataHttpAuthTest::kAuthCacheRefreshRate =
305     500ms;
306 const std::string MetadataHttpAuthTest::cluster_id =
307     "3a0be5af-0022-11e8-9655-0800279e6a88";
308 
309 const Credentials kTestUser1{
310     "foobar",
311     // hash for password="password"
312     {0x24, 0x41, 0x24, 0x30, 0x30, 0x35, 0x24, 0x58, 0x54, 0x72, 0x6F, 0x7D,
313      0x7D, 0x78, 0x6A, 0x62, 0x26, 0x7C, 0x65, 0x5C, 0x11, 0x3E, 0x0C, 0x09,
314      0x04, 0x33, 0x25, 0x33, 0x79, 0x53, 0x35, 0x4F, 0x55, 0x33, 0x79, 0x45,
315      0x6D, 0x53, 0x6D, 0x74, 0x46, 0x30, 0x64, 0x62, 0x6E, 0x6C, 0x69, 0x46,
316      0x75, 0x6F, 0x33, 0x39, 0x7A, 0x49, 0x48, 0x77, 0x58, 0x35, 0x78, 0x59,
317      0x62, 0x51, 0x53, 0x55, 0x41, 0x5A, 0x37, 0x49, 0x31, 0x43}};
318 const Credentials kTestUser2{
319     "testuser",
320     // hash for password="secret"
321     {0x24, 0x41, 0x24, 0x30, 0x30, 0x35, 0x24, 0x3F, 0x44, 0x62, 0x49, 0x71,
322      0x15, 0x52, 0x18, 0x71, 0x27, 0x42, 0x06, 0x04, 0x3E, 0x1E, 0x61, 0x08,
323      0x40, 0x42, 0x29, 0x2E, 0x68, 0x4D, 0x33, 0x4B, 0x76, 0x4C, 0x41, 0x74,
324      0x4C, 0x6C, 0x6F, 0x54, 0x43, 0x4F, 0x4B, 0x64, 0x2E, 0x4A, 0x69, 0x34,
325      0x74, 0x53, 0x63, 0x4E, 0x6E, 0x79, 0x6A, 0x65, 0x38, 0x55, 0x4B, 0x68,
326      0x4F, 0x2F, 0x63, 0x70, 0x71, 0x79, 0x68, 0x36, 0x54, 0x2E}};
327 
328 const Http_response_details ResponseUnauthorized{HttpStatusCode::Unauthorized,
329                                                  kContentTypeHtmlCharset};
330 const Http_response_details ResponseForbidden{HttpStatusCode::Forbidden,
331                                               kContentTypeHtmlCharset};
332 const Http_response_details ResponseOk{HttpStatusCode::Ok, kContentTypeJson};
333 
334 struct BasicMetadataHttpAuthTestParams {
335   std::string username;
336   std::string password;
337   Auth_data cached_info;
338   Http_response_details http_response;
339 };
340 
341 class BasicMetadataHttpAuthTest
342     : public MetadataHttpAuthTest,
343       public ::testing::WithParamInterface<BasicMetadataHttpAuthTestParams> {};
344 
TEST_F(BasicMetadataHttpAuthTest,MetadataHttpAuthDefaultConfig)345 TEST_F(BasicMetadataHttpAuthTest, MetadataHttpAuthDefaultConfig) {
346   set_mock_metadata({{kTestUser1, ""}}, cluster_http_port, cluster_id,
347                     cluster_node_port, false, 0, view_id);
348 
349   SCOPED_TRACE("// Launch the router with the initial state file");
350   ASSERT_NO_FATAL_FAILURE(launch_router(kMetadataCacheSectionBase));
351 
352   wait_for_port_ready(router_port);
353   ASSERT_TRUE(wait_for_rest_endpoint_ready(uri, http_server_port));
354   EXPECT_GT(wait_for_rest_auth_query(2, cluster_http_port), 0);
355 
356   IOContext io_ctx;
357   RestClient rest_client(io_ctx, "127.0.0.1", http_server_port, "foobar",
358                          "password");
359 
360   JsonDocument json_doc;
361   ASSERT_NO_FATAL_FAILURE(request_json(rest_client, uri, HttpMethod::Get,
362                                        HttpStatusCode::Ok, json_doc,
363                                        kContentTypeJson));
364 }
365 
TEST_F(BasicMetadataHttpAuthTest,UnsupportedMetadataSchemaVersion)366 TEST_F(BasicMetadataHttpAuthTest, UnsupportedMetadataSchemaVersion) {
367   set_mock_metadata({{kTestUser1, ""}}, cluster_http_port, cluster_id,
368                     cluster_node_port, false, 0, view_id, {1, 0, 0});
369 
370   SCOPED_TRACE("// Launch the router with the initial state file");
371   launch_router(kMetadataCacheSectionBase);
372 
373   wait_for_port_ready(router_port);
374   ASSERT_TRUE(wait_for_rest_endpoint_ready(uri, http_server_port));
375 
376   IOContext io_ctx;
377   RestClient rest_client(io_ctx, "127.0.0.1", http_server_port, "foobar",
378                          "password");
379 
380   JsonDocument json_doc;
381   ASSERT_NO_FATAL_FAILURE(request_json(rest_client, uri, HttpMethod::Get,
382                                        HttpStatusCode::Unauthorized, json_doc,
383                                        kContentTypeHtmlCharset));
384 }
385 
TEST_P(BasicMetadataHttpAuthTest,BasicMetadataHttpAuth)386 TEST_P(BasicMetadataHttpAuthTest, BasicMetadataHttpAuth) {
387   set_mock_metadata(
388       {{GetParam().cached_info.credentials, GetParam().cached_info.privileges,
389         GetParam().cached_info.auth_method}},
390       cluster_http_port, cluster_id, cluster_node_port, false, 0, view_id);
391 
392   SCOPED_TRACE("// Launch the router with the initial state file");
393   const std::string metadata_cache_section =
394       get_metadata_cache_section(kTTL, kAuthCacheTTL, kAuthCacheRefreshRate);
395   launch_router(metadata_cache_section);
396 
397   wait_for_port_ready(router_port);
398   ASSERT_TRUE(wait_for_rest_endpoint_ready(uri, http_server_port));
399   EXPECT_GT(wait_for_rest_auth_query(2, cluster_http_port), 0);
400 
401   IOContext io_ctx;
402   RestClient rest_client(io_ctx, "127.0.0.1", http_server_port,
403                          GetParam().username, GetParam().password);
404 
405   JsonDocument json_doc;
406   ASSERT_NO_FATAL_FAILURE(request_json(rest_client, uri, HttpMethod::Get,
407                                        GetParam().http_response.code, json_doc,
408                                        GetParam().http_response.type));
409 }
410 
411 INSTANTIATE_TEST_SUITE_P(
412     BasicMetadataHttpAuth, BasicMetadataHttpAuthTest,
413     ::testing::Values(
414         // matching user and password
415         BasicMetadataHttpAuthTestParams{
416             "foobar", "password", {kTestUser1, ""}, ResponseOk},
417         // not matching username
418         BasicMetadataHttpAuthTestParams{
419             "foobar", "password", {kTestUser2, ""}, ResponseUnauthorized},
420         // matching username, wrong password
421         BasicMetadataHttpAuthTestParams{
422             "foobar", "ooops", {kTestUser1, ""}, ResponseUnauthorized},
423         // empty username
424         BasicMetadataHttpAuthTestParams{
425             "", "secret", {kTestUser2, ""}, ResponseUnauthorized},
426         // empty password
427         BasicMetadataHttpAuthTestParams{
428             "nopwd", "", {{"nopwd", ""}, ""}, ResponseOk},
429         // username too long
430         BasicMetadataHttpAuthTestParams{std::string(260, 'x'),
431                                         "secret",
432                                         {kTestUser2, ""},
433                                         ResponseUnauthorized},
434         // matching user and password, but with privileges
435         BasicMetadataHttpAuthTestParams{
436             "foobar", "password", {kTestUser1, "{}"}, ResponseForbidden},
437         // invalid JSON string, user not added to auth cache
438         BasicMetadataHttpAuthTestParams{
439             "foobar", "password", {kTestUser1, "xy{}z"}, ResponseUnauthorized},
440         // unsupported authentication_method
441         BasicMetadataHttpAuthTestParams{
442             "foobar",
443             "password",
444             {kTestUser1, "", "mysql_native_password"},
445             ResponseUnauthorized},
446         // MCF missing rounds
447         BasicMetadataHttpAuthTestParams{
448             "x",
449             "secret",
450             {{"x",
451               "$A$$1=>5szy1\\':\\`\\'yv!@v0ZZkRT04EOc."
452               "sCRxFmoV30RhdtDdvt1N8rtZwmNO4re8"},
453              ""},
454             ResponseUnauthorized},
455         // MCF missing digest
456         BasicMetadataHttpAuthTestParams{
457             "x",
458             "secret",
459             {{"x", "$A$005$1=>5szy1\\':\\`\\'yv!@v"}, ""},
460             ResponseUnauthorized},
461         // MCF missing salt and digest
462         BasicMetadataHttpAuthTestParams{
463             "x", "secret", {{"x", "$A$005$"}, ""}, ResponseUnauthorized},
464         // MCF with unsupported identifier
465         BasicMetadataHttpAuthTestParams{
466             "x",
467             "secret",
468             {{"x",
469               "$_$005$1=>5szy1\\':\\`\\'yv!@v0ZZkRT04EOc."
470               "sCRxFmoV30RhdtDdvt1N8rtZwmNO4re8"},
471              ""},
472             ResponseUnauthorized}));
473 
474 class FileAuthBackendWithMetadataAuthSettings : public MetadataHttpAuthTest {
475  public:
476   const mysql_harness::Path passwd_file =
477       mysql_harness::Path(temp_test_dir.name()).join("passwd");
478 
auth_backend_settings() const479   std::string auth_backend_settings() const override {
480     return "[http_auth_backend:somebackend]\n"
481            "backend=file\n"
482            "filename=" +
483            passwd_file.str() + "\n";
484   }
485 };
486 
TEST_F(FileAuthBackendWithMetadataAuthSettings,MixedBackendSettings)487 TEST_F(FileAuthBackendWithMetadataAuthSettings, MixedBackendSettings) {
488   auto &cmd = launch_command(
489       ProcessManager::get_origin().join("mysqlrouter_passwd").str(),
490       {"set", passwd_file.str(), kRestApiUsername}, EXIT_SUCCESS, true);
491   cmd.register_response("Please enter password", "password\n");
492   EXPECT_EQ(cmd.wait_for_exit(), 0) << cmd.get_full_output();
493 
494   set_mock_metadata({}, cluster_http_port, cluster_id, cluster_node_port, false,
495                     0, view_id);
496 
497   const std::string metadata_cache_section =
498       get_metadata_cache_section(kTTL, kAuthCacheTTL, kAuthCacheRefreshRate);
499   // It should be possible to launch router with backend=file and with
500   // additional metadata_cache auth settings
501   ASSERT_NO_FATAL_FAILURE(launch_router(metadata_cache_section));
502   ASSERT_NO_FATAL_FAILURE(wait_for_port_ready(router_port));
503 }
504 
505 class InvalidMetadataHttpAuthTimersTest
506     : public MetadataHttpAuthTest,
507       public ::testing::WithParamInterface<std::string> {};
508 
TEST_P(InvalidMetadataHttpAuthTimersTest,InvalidMetadataHttpAuthTimers)509 TEST_P(InvalidMetadataHttpAuthTimersTest, InvalidMetadataHttpAuthTimers) {
510   set_mock_metadata({{kTestUser1, ""}}, cluster_http_port, cluster_id,
511                     cluster_node_port, false, 0, view_id);
512 
513   SCOPED_TRACE("// Launch the router with the initial state file");
514   auto &router =
515       launch_router(kMetadataCacheSectionBase + GetParam(), EXIT_FAILURE);
516   check_exit_code(router, EXIT_FAILURE);
517   EXPECT_THAT(router.exit_code(), testing::Ne(0));
518 }
519 
520 INSTANTIATE_TEST_SUITE_P(
521     InvalidMetadataHttpAuthTimers, InvalidMetadataHttpAuthTimersTest,
522     ::testing::Values(
523         std::string{"auth_cache_ttl=2.5\nauth_cache_refresh_interval=2.51\n"},
524         std::string{"auth_cache_ttl=2\nttl=3\n"},
525         std::string{"auth_cache_refresh_interval=1\nttl=2\n"},
526         std::string{"auth_cache_ttl=3600.01\n"},
527         std::string{"auth_cache_ttl=0.0001\n"},
528         std::string{"auth_cache_ttl=-0.1\n"},
529         std::string{"auth_cache_ttl=-1.1\n"},
530         std::string{"auth_cache_ttl=xxx\n"},
531         std::string{"auth_cache_refresh_interval=3600.01\n"},
532         std::string{"auth_cache_refresh_interval=0.0001\n"},
533         std::string{"auth_cache_refresh_interval=yyy\n"}));
534 
535 class ValidMetadataHttpAuthTimersTest
536     : public MetadataHttpAuthTest,
537       public ::testing::WithParamInterface<std::string> {};
538 
TEST_P(ValidMetadataHttpAuthTimersTest,ValidMetadataHttpAuthTimers)539 TEST_P(ValidMetadataHttpAuthTimersTest, ValidMetadataHttpAuthTimers) {
540   set_mock_metadata({{kTestUser1, ""}}, cluster_http_port, cluster_id,
541                     cluster_node_port, false, 0, view_id);
542 
543   SCOPED_TRACE("// Launch the router with the initial state file");
544   ASSERT_NO_FATAL_FAILURE(
545       launch_router(kMetadataCacheSectionBase + "ttl=0.001\n" + GetParam()));
546   ASSERT_NO_FATAL_FAILURE(wait_for_port_ready(router_port));
547 }
548 
549 INSTANTIATE_TEST_SUITE_P(
550     ValidMetadataHttpAuthTimers, ValidMetadataHttpAuthTimersTest,
551     ::testing::Values(
552         std::string{
553             "auth_cache_ttl=0.001\nauth_cache_refresh_interval=0.001\n"},
554         std::string{"auth_cache_ttl=3600\n"},
555         std::string{"auth_cache_ttl=3600.00\n"},
556         std::string{"auth_cache_refresh_interval=0.001\n"},
557         std::string{"auth_cache_refresh_interval=3600\n"},
558         std::string{"auth_cache_refresh_interval=3600.00\n"}));
559 
560 class MetadataHttpAuthTestCustomTimers
561     : public MetadataHttpAuthTest,
562       public ::testing::WithParamInterface<std::string> {};
563 
TEST_P(MetadataHttpAuthTestCustomTimers,MetadataHttpAuthCustomTimers)564 TEST_P(MetadataHttpAuthTestCustomTimers, MetadataHttpAuthCustomTimers) {
565   set_mock_metadata({{kTestUser1, ""}}, cluster_http_port, cluster_id,
566                     cluster_node_port, false, 0, view_id);
567 
568   SCOPED_TRACE("// Launch the router with the initial state file");
569   launch_router(kMetadataCacheSectionBase + GetParam());
570 
571   wait_for_port_ready(router_port);
572   ASSERT_TRUE(wait_for_rest_endpoint_ready(uri, http_server_port));
573   EXPECT_GT(wait_for_rest_auth_query(2, cluster_http_port), 0);
574 
575   IOContext io_ctx;
576   RestClient rest_client(io_ctx, "127.0.0.1", http_server_port, "foobar",
577                          "password");
578 
579   JsonDocument json_doc;
580   ASSERT_NO_FATAL_FAILURE(request_json(rest_client, uri, HttpMethod::Get,
581                                        HttpStatusCode::Ok, json_doc,
582                                        kContentTypeJson));
583 }
584 
585 INSTANTIATE_TEST_SUITE_P(
586     MetadataHttpAuthCustomTimers, MetadataHttpAuthTestCustomTimers,
587     ::testing::Values(
588         std::string{"auth_cache_ttl=3600\nauth_cache_refresh_interval=2\n"},
589         std::string{"auth_cache_ttl=3\n"}, std::string{"auth_cache_ttl=-1\n"},
590         std::string{"auth_cache_refresh_interval=1\n"},
591         std::string{"auth_cache_refresh_interval=1.567\n"},
592         std::string{"auth_cache_ttl=2.567\n"}));
593 
TEST_F(MetadataHttpAuthTest,ExpiredAuthCacheTTL)594 TEST_F(MetadataHttpAuthTest, ExpiredAuthCacheTTL) {
595   set_mock_metadata({{kTestUser1, ""}}, cluster_http_port, cluster_id,
596                     cluster_node_port, false, 0, view_id);
597 
598   std::chrono::milliseconds cache_ttl = kAuthCacheRefreshRate * 4;
599   SCOPED_TRACE("// Launch the router with the initial state file");
600   const std::string metadata_cache_section =
601       get_metadata_cache_section(kTTL, cache_ttl, kAuthCacheRefreshRate);
602   launch_router(metadata_cache_section);
603 
604   ASSERT_TRUE(wait_for_rest_endpoint_ready(uri, http_server_port));
605   EXPECT_GT(wait_for_rest_auth_query(2, cluster_http_port), 0);
606 
607   IOContext io_ctx;
608   RestClient rest_client(io_ctx, "127.0.0.1", http_server_port, "foobar",
609                          "password");
610 
611   JsonDocument json_doc;
612   ASSERT_NO_FATAL_FAILURE(request_json(rest_client, uri, HttpMethod::Get,
613                                        HttpStatusCode::Ok, json_doc,
614                                        kContentTypeJson));
615 
616   const bool fail_on_md_query = true;
617   // Start to fail metadata cache updates
618   set_mock_metadata({{kTestUser1, ""}}, cluster_http_port, cluster_id,
619                     cluster_node_port, fail_on_md_query, 0, view_id);
620 
621   // wait long enough for the auth cache to expire
622   std::this_thread::sleep_for(cache_ttl);
623 
624   ASSERT_NO_FATAL_FAILURE(request_json(rest_client, uri, HttpMethod::Get,
625                                        HttpStatusCode::Unauthorized, json_doc,
626                                        kContentTypeHtmlCharset));
627 }
628 
629 struct MetadataAuthCacheUpdateParams {
630   std::vector<Auth_data> first_auth_cache_data_set;
631   Http_response_details first_http_response;
632   std::vector<Auth_data> second_auth_cache_data_set;
633   Http_response_details second_http_response;
634 };
635 
636 class MetadataAuthCacheUpdate
637     : public MetadataHttpAuthTest,
638       public ::testing::WithParamInterface<MetadataAuthCacheUpdateParams> {};
639 
TEST_P(MetadataAuthCacheUpdate,AuthCacheUpdate)640 TEST_P(MetadataAuthCacheUpdate, AuthCacheUpdate) {
641   set_mock_metadata(GetParam().first_auth_cache_data_set, cluster_http_port,
642                     cluster_id, cluster_node_port, false, 0, view_id);
643 
644   SCOPED_TRACE("// Launch the router with the initial state file");
645   const std::string metadata_cache_section =
646       get_metadata_cache_section(kTTL, kAuthCacheTTL, kAuthCacheRefreshRate);
647   launch_router(metadata_cache_section);
648 
649   ASSERT_TRUE(wait_for_rest_endpoint_ready(uri, http_server_port));
650   EXPECT_GT(wait_for_rest_auth_query(2, cluster_http_port), 0);
651 
652   IOContext io_ctx;
653   RestClient rest_client(io_ctx, "127.0.0.1", http_server_port, "foobar",
654                          "password");
655 
656   JsonDocument json_doc;
657   ASSERT_NO_FATAL_FAILURE(request_json(
658       rest_client, uri, HttpMethod::Get, GetParam().first_http_response.code,
659       json_doc, GetParam().first_http_response.type));
660 
661   // Update authentication metadata
662   set_mock_metadata(GetParam().second_auth_cache_data_set, cluster_http_port,
663                     cluster_id, cluster_node_port, false, 0, view_id);
664 
665   // auth_cache is updated
666   EXPECT_GT(wait_for_rest_auth_query(2, cluster_http_port), 0);
667 
668   ASSERT_NO_FATAL_FAILURE(request_json(
669       rest_client, uri, HttpMethod::Get, GetParam().second_http_response.code,
670       json_doc, GetParam().second_http_response.type));
671 }
672 
673 INSTANTIATE_TEST_SUITE_P(
674     AuthCacheUpdate, MetadataAuthCacheUpdate,
675     ::testing::Values(
676         // add user
677         MetadataAuthCacheUpdateParams{{{kTestUser2, ""}},
678                                       ResponseUnauthorized,
679                                       {{kTestUser1, ""}, {kTestUser2, ""}},
680                                       ResponseOk},
681         // add user privileges
682         MetadataAuthCacheUpdateParams{{{kTestUser1, ""}},
683                                       ResponseOk,
684                                       {{kTestUser1, "{\"foo\": \"bar\"}"}},
685                                       ResponseForbidden},
686         // change password
687         MetadataAuthCacheUpdateParams{
688             {{kTestUser1, ""}},
689             ResponseOk,
690             {{{kTestUser1.username, kTestUser2.password_hash}, ""}},
691             ResponseUnauthorized},
692         // rm user
693         MetadataAuthCacheUpdateParams{{{kTestUser1, ""}, {kTestUser2, ""}},
694                                       ResponseOk,
695                                       {{kTestUser2, ""}},
696                                       ResponseUnauthorized}));
697 
main(int argc,char * argv[])698 int main(int argc, char *argv[]) {
699   init_windows_sockets();
700   g_origin_path = Path(argv[0]).dirname();
701   ::testing::InitGoogleTest(&argc, argv);
702   return RUN_ALL_TESTS();
703 }
704