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