/* * Copyright (c) 2016 MariaDB Corporation Ab * * Use of this software is governed by the Business Source License included * in the LICENSE.TXT file and at www.mariadb.com/bsl11. * * Change Date: 2023-10-29 * * On the date above, in accordance with the Business Source License, use * of this software will be governed by version 2 or later of the General * Public License. */ #include "testconnections.h" #include "fail_switch_rejoin_common.cpp" #include #include using std::string; using std::cout; int main(int argc, char** argv) { TestConnections test(argc, argv); test.repl->connect(); delete_slave_binlogs(test); const char pam_user[] = "dduck"; const char pam_pw[] = "313"; const char pam_config_name[] = "duckburg"; const string add_user_cmd = (string)"useradd " + pam_user; const string add_pw_cmd = (string)"echo " + pam_user + ":" + pam_pw + " | chpasswd"; const string read_shadow = "chmod o+r /etc/shadow"; const string remove_user_cmd = (string)"userdel --remove " + pam_user; const string read_shadow_off = "chmod o-r /etc/shadow"; // To make most out of this test, use a custom pam service configuration. It needs to be written to // all backends. const string pam_config_file = (string)"/etc/pam.d/" + pam_config_name; const string pam_message_file = "/tmp/messages.txt"; const string pam_config_contents = "auth optional pam_echo.so file=" + pam_message_file + "\n" "auth required pam_unix.so audit\n" "auth optional pam_echo.so file=" + pam_message_file + "\n" "auth required pam_unix.so audit\n" "account required pam_unix.so audit\n"; const string pam_message_contents = "I know what you did last summer!"; const string create_pam_conf_cmd = "printf \"" + pam_config_contents + "\" > " + pam_config_file; const string create_pam_message_cmd = "printf \"" + pam_message_contents + "\" > " + pam_message_file; const string delete_pam_conf_cmd = "rm -f " + pam_config_file; const string delete_pam_message_cmd = "rm -f " + pam_message_file; test.repl->connect(); // Prepare the backends for PAM authentication. Enable the plugin and create a user. Also, // make /etc/shadow readable for all so that the server process can access it. for (int i = 0; i < test.repl->N; i++) { MYSQL* conn = test.repl->nodes[i]; test.try_query(conn, "INSTALL SONAME 'auth_pam';"); test.repl->ssh_node_f(i, true, "%s", add_user_cmd.c_str()); test.repl->ssh_node_f(i, true, "%s", add_pw_cmd.c_str()); test.repl->ssh_node_f(i, true, "%s", read_shadow.c_str()); // Also, create the custom pam config and message file. test.repl->ssh_node_f(i, true, "%s", create_pam_conf_cmd.c_str()); test.repl->ssh_node_f(i, true, "%s", create_pam_message_cmd.c_str()); } // Also create the user on the node running MaxScale, as the MaxScale PAM plugin compares against // local users. test.maxscales->ssh_node_f(0, true, "%s", add_user_cmd.c_str()); test.maxscales->ssh_node_f(0, true, "%s", add_pw_cmd.c_str()); test.maxscales->ssh_node_f(0, true, "%s", read_shadow.c_str()); test.maxscales->ssh_node_f(0, true, "%s", create_pam_conf_cmd.c_str()); test.maxscales->ssh_node_f(0, true, "%s", create_pam_message_cmd.c_str()); if (test.ok()) { cout << "PAM-plugin installed and users created on all servers. Starting MaxScale.\n"; } else { cout << "Test preparations failed.\n"; } auto expect_server_status = [&test](const string& server_name, const string& status) { auto set_to_string = [](const StringSet& str_set) -> string { string rval; string sep; for (const string& elem : str_set) { rval += elem + sep; sep = ", "; } return rval; }; auto status_set = test.maxscales->get_server_status(server_name.c_str()); string status_str = set_to_string(status_set); bool found = (status_set.count(status) == 1); test.expect(found, "%s was not %s as was expected. Status: %s.", server_name.c_str(), status.c_str(), status_str.c_str()); }; string server_names[] = {"server1", "server2", "server3", "server4"}; string master = "Master"; string slave = "Slave"; if (test.ok()) { get_output(test); print_gtids(test); expect_server_status(server_names[0], master); expect_server_status(server_names[1], slave); expect_server_status(server_names[2], slave); expect_server_status(server_names[3], slave); } // Helper function for checking PAM-login. auto try_log_in = [&test](const string& user, const string& pass) { const char* host = test.maxscales->IP[0]; int port = test.maxscales->ports[0][0]; printf("Trying to log in to [%s]:%i as %s.\n", host, port, user.c_str()); MYSQL* maxconn = mysql_init(NULL); test.expect(maxconn, "mysql_init failed"); if (maxconn) { // Need to set plugin directory so that dialog.so is found. const char plugin_path[] = "../connector-c/install/lib/mariadb/plugin"; mysql_optionsv(maxconn, MYSQL_PLUGIN_DIR, plugin_path); mysql_real_connect(maxconn, host, user.c_str(), pass.c_str(), NULL, port, NULL, 0); auto err = mysql_error(maxconn); if (*err) { test.expect(false, "Could not log in: '%s'", err); } else { test.try_query(maxconn, "SELECT rand();"); if (test.ok()) { cout << "Logged in and queried successfully.\n"; } else { cout << "Query rejected: '" << mysql_error(maxconn) << "'\n"; } } mysql_close(maxconn); } }; auto update_users = [&test]() { test.maxscales->execute_maxadmin_command(0, "reload dbusers RWSplit-Router"); }; const char create_pam_user_fmt[] = "CREATE OR REPLACE USER '%s'@'%%' IDENTIFIED VIA pam USING '%s';"; if (test.ok()) { MYSQL* conn = test.repl->nodes[0]; // Create the PAM user on the master, it will replicate. Use the standard password service for // authenticating. test.try_query(conn, create_pam_user_fmt, pam_user, pam_config_name); test.try_query(conn, "GRANT SELECT ON *.* TO '%s'@'%%';", pam_user); test.try_query(conn, "FLUSH PRIVILEGES;"); sleep(1); test.repl->sync_slaves(); update_users(); get_output(test); // If ok so far, try logging in with PAM. if (test.ok()) { cout << "Testing normal PAM user.\n"; try_log_in(pam_user, pam_pw); test.log_includes(0, pam_message_contents.c_str()); } // Remove the created user. test.try_query(conn, "DROP USER '%s'@'%%';", pam_user); } if (test.ok()) { const char dummy_user[] = "proxy-target"; const char dummy_pw[] = "unused_pw"; // Basic PAM authentication seems to be working. Now try with an anonymous user proxying to // the real user. The following does not actually do proper user mapping, as that requires further // setup on the backends. It does however demonstrate that MaxScale detects the anonymous user and // accepts the login of a non-existent user with PAM. MYSQL* conn = test.repl->nodes[0]; // Add a user which will be proxied. test.try_query(conn, "CREATE OR REPLACE USER '%s'@'%%' IDENTIFIED BY '%s';", dummy_user, dummy_pw); // Create the anonymous catch-all user and allow it to proxy as the "proxy-target", meaning it // gets the target's privileges. Granting the proxy privilege is a bit tricky since only the local // root user can give it. test.try_query(conn, create_pam_user_fmt, "", pam_config_name); test.repl->ssh_node_f(0, true, "echo \"GRANT PROXY ON '%s'@'%%' TO ''@'%%'; FLUSH PRIVILEGES;\" | " "mysql --user=root", dummy_user); sleep(1); test.repl->sync_slaves(); update_users(); get_output(test); if (test.ok()) { // Again, try logging in with the same user. cout << "Testing anonymous proxy user.\n"; try_log_in(pam_user, pam_pw); test.log_includes(0, pam_message_contents.c_str()); } // Remove the created users. test.try_query(conn, "DROP USER '%s'@'%%';", dummy_user); test.try_query(conn, "DROP USER ''@'%%';"); } // Cleanup: remove the linux users on the backends and MaxScale node, unload pam plugin. for (int i = 0; i < test.repl->N; i++) { MYSQL* conn = test.repl->nodes[i]; test.try_query(conn, "UNINSTALL SONAME 'auth_pam';"); test.repl->ssh_node_f(i, true, "%s", remove_user_cmd.c_str()); test.repl->ssh_node_f(i, true, "%s", read_shadow_off.c_str()); test.repl->ssh_node_f(i, true, "%s", delete_pam_conf_cmd.c_str()); test.repl->ssh_node_f(i, true, "%s", delete_pam_message_cmd.c_str()); } test.maxscales->ssh_node_f(0, true, "%s", remove_user_cmd.c_str()); test.maxscales->ssh_node_f(0, true, "%s", read_shadow_off.c_str()); test.maxscales->ssh_node_f(0, true, "%s", delete_pam_conf_cmd.c_str()); test.maxscales->ssh_node_f(0, true, "%s", delete_pam_message_cmd.c_str()); test.repl->disconnect(); return test.global_result; }