diff --git a/cmake/FindPAM.cmake b/cmake/FindPAM.cmake new file mode 100644 index 000000000..056b9d323 --- /dev/null +++ b/cmake/FindPAM.cmake @@ -0,0 +1,19 @@ +# This CMake-file locates PAM libraries on the system +# +# The following variables are set: +# PAM_FOUND - If the PAM library was found +# PAM_LIBRARIES - Path to the library +# PAM_INCLUDE_DIR - Path to headers + +find_path(PAM_INCLUDE_DIR pam_appl.h PATH_SUFFIXES security) +find_library(PAM_LIBRARIES NAMES pam) +message(STATUS "Found PAM include dirs: ${PAM_INCLUDE_DIR}") +if (PAM_INCLUDE_DIR AND PAM_LIBRARIES) + message(STATUS "Found PAM: ${PAM_LIBRARIES}") + set(PAM_FOUND TRUE BOOL) +else() + message(STATUS "PAM libraries not found") + set(PAM_FOUND FALSE BOOL) +endif() + + diff --git a/server/modules/authenticator/PAM/CMakeLists.txt b/server/modules/authenticator/PAM/CMakeLists.txt index e57ac96ca..4bf2af87e 100644 --- a/server/modules/authenticator/PAM/CMakeLists.txt +++ b/server/modules/authenticator/PAM/CMakeLists.txt @@ -1,25 +1,8 @@ -# This CMake file locates the Jansson libraries and headers -# -# The following variables are set: -# PAM_FOUND - If the PAM library was found -# PAM_LIBRARIES - Path to the library -# PAM_INCLUDE_DIR - Path to headers - -find_path(PAM_INCLUDE_DIR pam_appl.h PATH_SUFFIXES security) -find_library(PAM_LIBRARIES NAMES pam) -message(STATUS "Found PAM include dirs: ${PAM_INCLUDE_DIR}") -if (PAM_INCLUDE_DIR AND PAM_LIBRARIES) - message(STATUS "Found PAM: ${PAM_LIBRARIES}") - set(PAM_FOUND TRUE) -else() - message(STATUS "Could not find PAM") -endif() - +find_package(PAM) if (PAM_FOUND AND SQLITE_FOUND) - #include_directories(${PAM_INCLUDE_DIR}) include_directories(${SQLITE_INCLUDE_DIR}) add_subdirectory(PAMAuth) add_subdirectory(PAMBackendAuth) else() - message(STATUS "No PAM libraries or SQLite found, not building PAM authenticator. Current SQLite version is #${SQLITE_VERSION}") + message(STATUS "No PAM libraries or SQLite found, not building PAM authenticator.") endif() diff --git a/server/modules/authenticator/PAM/PAMAuth/CMakeLists.txt b/server/modules/authenticator/PAM/PAMAuth/CMakeLists.txt index 3177a214f..468be540b 100644 --- a/server/modules/authenticator/PAM/PAMAuth/CMakeLists.txt +++ b/server/modules/authenticator/PAM/PAMAuth/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(PAMAuth SHARED pam_auth.cc ../pam_auth_common.cc) +add_library(PAMAuth SHARED pam_auth.cc ../pam_auth_common.cc pam_client_session.cc pam_instance.cc) target_link_libraries(PAMAuth maxscale-common ${PAM_LIBRARIES} ${SQLITE_LIBRARIES} MySQLCommon) set_target_properties(PAMAuth PROPERTIES VERSION "1.0.0") install_module(PAMAuth core) diff --git a/server/modules/authenticator/PAM/PAMAuth/pam_auth.cc b/server/modules/authenticator/PAM/PAMAuth/pam_auth.cc index b14f5b215..491b8b19c 100644 --- a/server/modules/authenticator/PAM/PAMAuth/pam_auth.cc +++ b/server/modules/authenticator/PAM/PAMAuth/pam_auth.cc @@ -10,465 +10,37 @@ * of this software will be governed by version 2 or later of the General * Public License. */ +#include"pam_auth.hh" -#define MXS_MODULE_NAME "PAMAuth" - -#include "../pam_auth.hh" - -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -using std::string; -using maxscale::Buffer; +#include "pam_instance.hh" +#include "pam_client_session.hh" +#include "../pam_auth_common.hh" -#define PAM_USERS_QUERY_NUM_FIELDS 5 -/** Name of the in-memory database */ -#define PAM_DATABASE_NAME "file:pam.db?mode=memory&cache=shared" -/** The table name where we store the users */ -#define PAM_TABLE_NAME "pam_users" - -/** Flags for sqlite3_open_v2() */ -static int db_flags = SQLITE_OPEN_READWRITE | - SQLITE_OPEN_CREATE | - SQLITE_OPEN_URI | - SQLITE_OPEN_SHAREDCACHE; - -/** The instance structure for the client side PAM authenticator, created in pam_auth_init() */ -struct PamInstance -{ - sqlite3 *m_dbhandle; /**< SQLite3 database handle */ -}; - -/** Used by the PAM conversation function */ -struct ConversationData -{ - string password; - int counter; - DCB* client; -}; - -namespace -{ -/** - * @brief Add new PAM user entry to the internal user database - * - * @param handle Database handle - * @param user Username - * @param host Host - * @param db Database - * @param anydb Global access to databases - * @param pam_service The PAM service used - */ -void add_pam_user(sqlite3 *handle, const char *user, const char *host, - const char *db, bool anydb, const char *pam_service) -{ - /** - * The insert query template which adds users to the pam_users table. - * - * Note that 'db' and 'pam_service' are strings that can be NULL and thus they have - * no quotes around them. The quotes for strings are added in this function. - */ - const char insert_sql_pattern[] = - "INSERT INTO " PAM_TABLE_NAME " VALUES ('%s', '%s', %s, '%s', %s)"; - - /** Used for NULL value creation in the INSERT query */ - const char NULL_TOKEN[] = "NULL"; - - size_t dblen = db ? strlen(db) + 2 : sizeof(NULL_TOKEN); /** +2 for single quotes */ - char dbstr[dblen + 1]; - - if (db) - { - sprintf(dbstr, "'%s'", db); - } - else - { - strcpy(dbstr, NULL_TOKEN); - } - - size_t servlen = (pam_service && *pam_service) ? strlen(pam_service) + 2 : - sizeof(NULL_TOKEN); /** +2 for single quotes */ - char service_string[servlen + 1]; - - if (pam_service && *pam_service) - { - sprintf(service_string, "'%s'", pam_service); - } - else - { - strcpy(service_string, NULL_TOKEN); - } - - size_t len = sizeof(insert_sql_pattern) + strlen(user) + strlen(host) + dblen + servlen + 1; - - char insert_sql[len + 1]; - sprintf(insert_sql, insert_sql_pattern, user, host, dbstr, anydb ? "1" : "0", service_string); - - char *err; - if (sqlite3_exec(handle, insert_sql, NULL, NULL, &err) != SQLITE_OK) - { - MXS_ERROR("Failed to insert user: %s", err); - sqlite3_free(err); - } - - MXS_INFO("Added user: %s", insert_sql); -} - -/** Callback for print_db */ -int print_DB_cb(void *data, int columns, char** column_vals, char** column_names) -{ - StringArray* results = static_cast(data); - std::stringstream message; - message << "Row with " << columns << " items: "; - - for (int i = 0; i < columns; i++) - { - if (column_vals[i]) - { - message << column_names[i] << " = " << column_vals[i]; - } - else - { - message << column_names[i] << " = (null)"; - } - message << " ,"; - } - MXS_DEBUG("%s", message.str().c_str()); - - if (column_vals[4]) - { - results->push_back(column_vals[4]); - } - else - { - results->push_back(""); - } - return 0; -} - -int user_services_cb(void *data, int columns, char** column_vals, char** column_names) -{ - if (columns == 1) - { - StringArray* results = static_cast(data); - if (column_vals[0]) - { - results->push_back(column_vals[0]); - } - else - { - // Empty is a valid value. - results->push_back(""); - } - } - else - { - ss_dassert(!true); - } - return 0; -} - -/** - * @brief Create an AuthSwitchRequest packet - * - * This function also contains the first part of the PAM authentication. The server - * (MaxScale) sends the plugin name "dialog" to the client with the first password - * prompt. We want to avoid calling the PAM conversation function more than once - * because it blocks, so we "emulate" its behaviour here. This obviously only works - * with the basic password authentication scheme. - * - * @return Allocated packet - * @see https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest - */ -Buffer create_auth_change_packet(PamSession *pses) -{ - /** - * The AuthSwitchRequest packet: - * 4 bytes - Header - * 0xfe - Command byte - * string[NUL] - Auth plugin name - * byte - Message type - * string[EOF] - Message - */ - size_t plen = 1 + sizeof(DIALOG) + 1 + sizeof(PASSWORD) - 1; - size_t buflen = MYSQL_HEADER_LEN + plen; - uint8_t data[buflen]; - uint8_t* pData = data; - gw_mysql_set_byte3(pData, plen); - pData += 3; - *pData++ = ++pses->m_sequence; // Second packet - *pData++ = 0xfe; // AuthSwitchRequest command - memcpy(pData, DIALOG, sizeof(DIALOG)); // Plugin name - pData += sizeof(DIALOG); - *pData++ = DIALOG_ECHO_DISABLED; - memcpy(pData, PASSWORD, sizeof(PASSWORD) - 1); // First message - - Buffer buffer(data, buflen); - return buffer; -} - -/** - * @brief Read the client's password, store it to MySQL-session - * - * @param dcb Client DCB - * @param buffer Buffer containing the password - * - * @return True on success, false if memory allocation failed - */ -bool store_client_password(DCB *dcb, GWBUF *buffer) -{ - bool rval = false; - uint8_t header[MYSQL_HEADER_LEN]; - - if (gwbuf_copy_data(buffer, 0, MYSQL_HEADER_LEN, header) == MYSQL_HEADER_LEN) - { - size_t plen = gw_mysql_get_byte3(header); - MYSQL_session *ses = (MYSQL_session*)dcb->data; - ses->auth_token = (uint8_t *)MXS_CALLOC(plen, sizeof(uint8_t)); - if (ses->auth_token) - { - ses->auth_token_len = gwbuf_copy_data(buffer, MYSQL_HEADER_LEN, plen, ses->auth_token); - rval = true; - } - } - return rval; -} - -int conversation_func(int num_msg, const struct pam_message **msg, - struct pam_response **resp_out, void *appdata_ptr) -{ - MXS_DEBUG("Entering PAM conversation function."); - ConversationData* data = static_cast(appdata_ptr); - if (num_msg == 1) - { - pam_message first = *msg[0]; - MXS_DEBUG("Message type: '%d', contents:'%s'", first.msg_style, first.msg); - pam_response* response = static_cast(MXS_MALLOC(sizeof(pam_response))); - if (response) - { - response[0].resp_retcode = 0; - response[0].resp = MXS_STRDUP(data->password.c_str()); - } - *resp_out = response; - } - else - { - MXS_ERROR("Conversation function received more than one message ('%d') from API.", num_msg); - } - data->counter++; - return 0; -} - -/** - * @brief Check if the client token is valid - * - * @param token Client token - * @param len Length of the token - * @param output Pointer where the client principal name is stored - * @return True if client token is valid - */ -bool validate_pam_password(string user, string password, string service, DCB* client) -{ - ConversationData appdata = {password, 0, client}; - pam_conv conv_struct = {conversation_func, &appdata}; - bool authenticated = false; - bool account_ok = false; - pam_handle_t* pam_handle = NULL; - int pam_status = pam_start(service.c_str(), user.c_str(), &conv_struct, &pam_handle); - if (pam_status == PAM_SUCCESS) - { - pam_status = pam_authenticate(pam_handle, 0); - switch (pam_status) - { - case PAM_SUCCESS: - authenticated = true; - MXS_DEBUG("pam_authenticate returned success."); - break; - case PAM_AUTH_ERR: - MXS_DEBUG("pam_authenticate returned authentication failure" - " (wrong password)."); - // Normal failure - break; - default: - // This shouldn't happen, normally at least - MXS_ERROR("pam_authenticate returned error '%d'.", pam_status); - break; - } - } - else - { - MXS_ERROR("Failed to start PAM authentication for user '%s'.", user.c_str()); - } - if (authenticated) - { - pam_status = pam_acct_mgmt(pam_handle, 0); - account_ok = (pam_status == PAM_SUCCESS); - } - pam_end(pam_handle, pam_status); - return account_ok; -} - -/** - * @brief Verify the user has access to the database - * - * @param auth Authenticator session - * @param dcb Client DCB - * @param session MySQL session - * @param princ Client principal name - * @return True if the user has access to the database - */ -StringArray get_pam_user_services(PamSession *auth, DCB *dcb, MYSQL_session *session) -{ - string sql = string("SELECT authentication_string FROM " PAM_TABLE_NAME " WHERE user = '") + - session->user + "' AND '" + dcb->remote + "' LIKE host AND (anydb = '1' OR '" + - session->db + "' = '' OR '" + session->db + "' LIKE db) ORDER BY authentication_string"; - bool try_again = true; - char *err; - MXS_DEBUG("PAM services search sql: '%s'.", sql.c_str()); - StringArray service_names; - /** - * Try search twice: first time with the current users, second - * time with fresh users. - */ - for (int i = 0; i < 2 && try_again; i++) - { - if (sqlite3_exec(auth->m_dbhandle, sql.c_str(), user_services_cb, - &service_names, &err) != SQLITE_OK) - { - MXS_ERROR("Failed to execute query: '%s'", err); - sqlite3_free(err); - } - else if (service_names.size()) - { - try_again = false; - MXS_INFO("User '%s' matched %lu rows in " PAM_TABLE_NAME " db.", - session->user, service_names.size()); - } - - if (try_again && !i) - { - try_again = !service_refresh_users(dcb->service); - } - } - return service_names; -} - -/** - * @brief Delete old users from the database - * @param handle Database handle - */ -void delete_old_users(sqlite3 *handle) -{ - /** Delete query used to clean up the database before loading new users */ - const char DELETE_QUERY[] = "DELETE FROM " PAM_TABLE_NAME; - char *err; - if (sqlite3_exec(handle, DELETE_QUERY, NULL, NULL, &err) != SQLITE_OK) - { - MXS_ERROR("Failed to delete old users: %s", err); - sqlite3_free(err); - } -} - -/** - * Prints auth database contents. Currently unused, may be useful for debugging. - * - * @param db Database handle - */ -void print_DB(sqlite3* db) -{ - MXS_NOTICE("PRINTING DB----------------"); - const char query[] = "SELECT * FROM " PAM_TABLE_NAME; - char* err = NULL; - StringArray services; - if (sqlite3_exec(db, query, print_DB_cb, &services, &err) != SQLITE_OK) - { - MXS_ERROR("Failed to execute auth query: %s", err); - sqlite3_free(err); - } - MXS_NOTICE("DONE, got %lu users----------------", services.size()); -} - -} /** * Initialize PAM authenticator * * @param options Listener options * - * @return Authenticator instance + * @return Authenticator instance, or NULL on error */ -void* pam_auth_init(char **options) +static void* pam_auth_init(char **options) { - /** CREATE TABLE statement for the in-memory table */ - const char CREATE_SQL[] = "CREATE TABLE IF NOT EXISTS " PAM_TABLE_NAME - " (user varchar(255), host varchar(255), db varchar(255), " - "anydb boolean, authentication_string text)"; - - PamInstance *instance = new (std::nothrow) PamInstance(); - if (instance) - { - if (sqlite3_threadsafe() == 0) - { - MXS_ERROR("SQLite3 was compiled with thread safety off. May cause " - "corruption of in-memory database."); - } - /* This handle may be used from multiple threads, set full mutex. */ - if (sqlite3_open_v2(PAM_DATABASE_NAME, &instance->m_dbhandle, - db_flags | SQLITE_OPEN_FULLMUTEX, NULL) != SQLITE_OK) - { - MXS_ERROR("Failed to open SQLite3 handle."); - delete instance; - return NULL; - } - - char *err; - if (sqlite3_exec(instance->m_dbhandle, CREATE_SQL, NULL, NULL, &err) != SQLITE_OK) - { - MXS_ERROR("Failed to create database: '%s'", err); - sqlite3_free(err); - delete instance; - return NULL; - } - } - return instance; + return PamInstance::create(options); } /** - * Allocate DCB-specific authenticator data + * Allocate DCB-specific authenticator data (session) * - * @param instance Authenticator instance + * @param instance Authenticator instance the session should be connected to * * @return Authenticator session */ -void* pam_auth_alloc(void *instance) +static void* pam_auth_alloc(void *instance) { - PamSession* pses = new (std::nothrow) PamSession; - if (pses) - { - // This handle is only used from one thread - if (sqlite3_open_v2(PAM_DATABASE_NAME, &pses->m_dbhandle, db_flags, NULL) == SQLITE_OK) - { - sqlite3_busy_timeout(pses->m_dbhandle, MXS_SQLITE_BUSY_TIMEOUT); - } - else - { - MXS_ERROR("Failed to open SQLite3 handle."); - delete pses; - pses = NULL; - } - } - return pses; + PamInstance* inst = static_cast(instance); + return PamClientSession::create(*inst); } /** @@ -476,9 +48,9 @@ void* pam_auth_alloc(void *instance) * * @param data PAM session */ -void pam_auth_free(void *data) +static void pam_auth_free(void *data) { - delete (PamSession*)data; + delete static_cast(data); } /** @@ -486,99 +58,52 @@ void pam_auth_free(void *data) * * @param dcb Client DCB * @param read_buffer Buffer containing the client's response + * * @return MXS_AUTH_SUCCEEDED if authentication can continue, MXS_AUTH_FAILED if * authentication failed */ static int pam_auth_extract(DCB *dcb, GWBUF *read_buffer) { - int rval = MXS_AUTH_FAILED; - PamSession *pses = static_cast(dcb->authenticator_data); - gwbuf_copy_data(read_buffer, MYSQL_SEQ_OFFSET, 1, &pses->m_sequence); - - switch (pses->m_state) - { - case PAM_AUTH_INIT: - rval = MXS_AUTH_SUCCEEDED; - break; - - case PAM_AUTH_DATA_SENT: - store_client_password(dcb, read_buffer); - rval = MXS_AUTH_SUCCEEDED; - break; - - default: - MXS_ERROR("Unexpected authentication state: %d", pses->m_state); - ss_dassert(!true); - break; - } - return rval; + PamClientSession *pses = static_cast(dcb->authenticator_data); + return pses->extract(dcb, read_buffer); } /** * @brief Is the client SSL capable * * @param dcb Client DCB + * * @return True if client supports SSL */ -bool pam_auth_connectssl(DCB *dcb) +static bool pam_auth_connectssl(DCB *dcb) { MySQLProtocol *protocol = (MySQLProtocol*)dcb->protocol; return protocol->client_capabilities & GW_MYSQL_CAPABILITIES_SSL; } /** - * @brief Authenticate the client + * @brief Authenticate the client. Should be called after pam_auth_extract(). * * @param dcb Client DCB - * @return MXS_AUTH_INCOMPLETE if authentication is not yet complete, MXS_AUTH_SUCCEEDED - * if authentication was successfully completed or MXS_AUTH_FAILED if authentication + * + * @return MXS_AUTH_INCOMPLETE if authentication is not yet complete. MXS_AUTH_SUCCEEDED + * if authentication was successfully completed. MXS_AUTH_FAILED if authentication * has failed. */ -int pam_auth_authenticate(DCB *dcb) +static int pam_auth_authenticate(DCB *dcb) { - int rval = MXS_AUTH_FAILED; - PamSession *auth_ses = static_cast(dcb->authenticator_data); - - if (auth_ses->m_state == PAM_AUTH_INIT) - { - /** We need to send the authentication switch packet to change the - * authentication to something other than the 'mysql_native_password' - * method */ - Buffer authbuf = create_auth_change_packet(auth_ses); - if (authbuf.length() && dcb->func.write(dcb, authbuf.release())) - { - auth_ses->m_state = PAM_AUTH_DATA_SENT; - rval = MXS_AUTH_INCOMPLETE; - } - } - else if (auth_ses->m_state == PAM_AUTH_DATA_SENT) - { - /** We sent the authentication change packet + plugin name and the client - * responded with the password. Try to continue authentication without more - * messages to client. */ - MYSQL_session *ses = (MYSQL_session*)dcb->data; - string password((char*)ses->auth_token, ses->auth_token_len); - StringArray services = get_pam_user_services(auth_ses, dcb, ses); - - bool pam_passed = false; - for (size_t i = 0; i < services.size() && !pam_passed; i++) - { - pam_passed = validate_pam_password(ses->user, password, services.at(i), dcb); - } - if (pam_passed) - { - rval = MXS_AUTH_SUCCEEDED; - } - } - return rval; + PamClientSession* pses = static_cast(dcb->authenticator_data); + return pses->authenticate(dcb); } /** - * @brief Free authenticator data from a DCB + * Free general authenticator data from a DCB. This is data that is not specific + * to the client authenticator session and may be used by the backend authenticator + * session to log onto backends. * - * @param dcb DCB to free + * @param dcb DCB to free data from */ -void pam_auth_free_data(DCB *dcb) +static void pam_auth_free_data(DCB *dcb) { if (dcb->data) { @@ -596,70 +121,13 @@ void pam_auth_free_data(DCB *dcb) * give more precise error messages to the clients when authentication fails. * * @param listener Listener definition + * * @return MXS_AUTH_LOADUSERS_OK on success, MXS_AUTH_LOADUSERS_ERROR on error */ -int pam_auth_load_users(SERV_LISTENER *listener) +static int pam_auth_load_users(SERV_LISTENER *listener) { - /** Query that gets all users that authenticate via the pam plugin */ - const char PAM_USERS_QUERY[] = - "SELECT u.user, u.host, d.db, u.select_priv, u.authentication_string FROM " - "mysql.user AS u LEFT JOIN mysql.db AS d " - "ON (u.user = d.user AND u.host = d.host) WHERE u.plugin = 'pam' " - "UNION " - "SELECT u.user, u.host, t.db, u.select_priv, u.authentication_string FROM " - "mysql.user AS u LEFT JOIN mysql.tables_priv AS t " - "ON (u.user = t.user AND u.host = t.host) WHERE u.plugin = 'pam' " - "ORDER BY user"; - - char *user, *pw; - int rval = MXS_AUTH_LOADUSERS_ERROR; - PamInstance *inst = (PamInstance*)listener->auth_instance; - - if (serviceGetUser(listener->service, &user, &pw) && (pw = decrypt_password(pw))) - { - for (SERVER_REF *servers = listener->service->dbref; servers; servers = servers->next) - { - MYSQL *mysql = mysql_init(NULL); - if (mxs_mysql_real_connect(mysql, servers->server, user, pw)) - { - if (mysql_query(mysql, PAM_USERS_QUERY)) - { - MXS_ERROR("Failed to query server '%s' for PAM users: '%s'.", - servers->server->unique_name, mysql_error(mysql)); - } - else - { - MYSQL_RES *res = mysql_store_result(mysql); - delete_old_users(inst->m_dbhandle); - - if (res) - { - ss_dassert(mysql_num_fields(res) == PAM_USERS_QUERY_NUM_FIELDS); - MXS_NOTICE("Read %llu rows when fetching users.", mysql_num_rows(res)); - MYSQL_ROW row; - while ((row = mysql_fetch_row(res))) - { - add_pam_user(inst->m_dbhandle, row[0], row[1], row[2], - row[3] && strcasecmp(row[3], "Y") == 0, - row[4]); - } - rval = MXS_AUTH_LOADUSERS_OK; - mysql_free_result(res); - } - } - - mysql_close(mysql); - - if (rval == MXS_AUTH_LOADUSERS_OK) - { - break; - } - } - } - MXS_FREE(pw); - } - - return rval; + PamInstance *inst = static_cast(listener->auth_instance); + return inst->load_users(listener->service); } MXS_BEGIN_DECLS diff --git a/server/modules/authenticator/PAM/PAMAuth/pam_auth.hh b/server/modules/authenticator/PAM/PAMAuth/pam_auth.hh new file mode 100644 index 000000000..ae721fa41 --- /dev/null +++ b/server/modules/authenticator/PAM/PAMAuth/pam_auth.hh @@ -0,0 +1,27 @@ +#pragma once +/* + * 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: 2020-01-01 + * + * 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. + */ + +/* + * Common definitions and includes for PAM client authenticator + */ +#define MXS_MODULE_NAME "PAMAuth" + +#include + +#include +#include +#include +#include + + diff --git a/server/modules/authenticator/PAM/PAMAuth/pam_client_session.cc b/server/modules/authenticator/PAM/PAMAuth/pam_client_session.cc new file mode 100644 index 000000000..9fd49bbd6 --- /dev/null +++ b/server/modules/authenticator/PAM/PAMAuth/pam_client_session.cc @@ -0,0 +1,367 @@ +/* + * 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: 2020-01-01 + * + * 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 "pam_client_session.hh" + +#include +#include + +using maxscale::Buffer; +using std::string; + +namespace +{ +/** + * @brief Read the client's password, store it to MySQL-session + * + * @param dcb Client DCB + * @param buffer Buffer containing the password + * + * @return True on success, false if memory allocation failed + */ +bool store_client_password(DCB *dcb, GWBUF *buffer) +{ + bool rval = false; + uint8_t header[MYSQL_HEADER_LEN]; + + if (gwbuf_copy_data(buffer, 0, MYSQL_HEADER_LEN, header) == MYSQL_HEADER_LEN) + { + size_t plen = gw_mysql_get_byte3(header); + MYSQL_session *ses = (MYSQL_session*)dcb->data; + ses->auth_token = (uint8_t *)MXS_CALLOC(plen, sizeof(uint8_t)); + if (ses->auth_token) + { + ses->auth_token_len = gwbuf_copy_data(buffer, MYSQL_HEADER_LEN, plen, ses->auth_token); + rval = true; + } + } + return rval; +} + +/** + * Helper callback for PamClientSession::get_pam_user_services(). See SQLite3 + * documentation for more information. + * + * @param data Application data + * @param columns Number of columns, must be 1 + * @param column_vals Column values + * @param column_names Column names + * @return Always 0 + */ +int user_services_cb(void *data, int columns, char** column_vals, char** column_names) +{ + ss_dassert(columns == 1); + PamClientSession::StringVector* results = static_cast(data); + if (column_vals[0]) + { + results->push_back(column_vals[0]); + } + else + { + // Empty is a valid value. + results->push_back(""); + } + return 0; +} + +/** Used by the PAM conversation function */ +struct ConversationData +{ + DCB* m_client; + int m_counter; + string m_password; + + ConversationData(DCB* client, int counter, const string& password) + : m_client(client), + m_counter(counter), + m_password(password) + { + } +}; + +/** + * PAM conversation function. The implementation "cheats" by not actually doing + * I/O with the client. This should only be called once per client when + * authenticating. See + * http://www.linux-pam.org/Linux-PAM-html/adg-interface-of-app-expected.html#adg-pam_conv + * for more information. + */ +int conversation_func(int num_msg, const struct pam_message **msg, + struct pam_response **resp_out, void *appdata_ptr) +{ + MXS_DEBUG("Entering PAM conversation function."); + int rval = PAM_CONV_ERR; + ConversationData* data = static_cast(appdata_ptr); + if (data->m_counter > 1) + { + MXS_ERROR("Multiple calls to conversation function for client '%s'. %s", + data->m_client->user, GENERAL_ERRMSG); + } + else if (num_msg == 1) + { + pam_message first = *msg[0]; + if ((first.msg_style != PAM_PROMPT_ECHO_OFF && first.msg_style != PAM_PROMPT_ECHO_ON) || + PASSWORD != first.msg) + { + MXS_ERROR("Unexpected PAM message: type='%d', contents='%s'", + first.msg_style, first.msg); + } + else + { + pam_response* response = static_cast(MXS_MALLOC(sizeof(pam_response))); + if (response) + { + response->resp_retcode = 0; + response->resp = MXS_STRDUP(data->m_password.c_str()); + *resp_out = response; + rval = PAM_SUCCESS; + } + } + } + else + { + MXS_ERROR("Conversation function received '%d' messages from API. Only " + "singular messages are supported.", num_msg); + } + data->m_counter++; + return rval; +} + +/** + * @brief Check if the client token is valid + * + * @param token Client token + * @param len Length of the token + * @param output Pointer where the client principal name is stored + * @return True if client token is valid + */ +bool validate_pam_password(const string& user, const string& password, + const string& service, DCB* client) +{ + ConversationData appdata(client, 0, password); + pam_conv conv_struct = {conversation_func, &appdata}; + bool authenticated = false; + bool account_ok = false; + pam_handle_t* pam_handle = NULL; + int pam_status = pam_start(service.c_str(), user.c_str(), &conv_struct, &pam_handle); + if (pam_status == PAM_SUCCESS) + { + pam_status = pam_authenticate(pam_handle, 0); + switch (pam_status) + { + case PAM_SUCCESS: + authenticated = true; + MXS_DEBUG("pam_authenticate returned success."); + break; + case PAM_AUTH_ERR: + MXS_DEBUG("pam_authenticate returned authentication failure" + " (wrong password)."); + // Normal failure + break; + default: + MXS_ERROR("pam_authenticate returned error '%d'.", pam_status); + break; + } + } + else + { + MXS_ERROR("Failed to start PAM authentication for user '%s'.", user.c_str()); + } + if (authenticated) + { + pam_status = pam_acct_mgmt(pam_handle, 0); + account_ok = (pam_status == PAM_SUCCESS); + } + pam_end(pam_handle, pam_status); + return account_ok; +} + +} + +PamClientSession::PamClientSession(sqlite3* dbhandle, const PamInstance& instance) + : m_state(PAM_AUTH_INIT), + m_sequence(0), + m_dbhandle(dbhandle), + m_instance(instance) +{ +} + +PamClientSession:: ~PamClientSession() +{ + sqlite3_close_v2(m_dbhandle); +} + +PamClientSession* PamClientSession::create(const PamInstance& inst) +{ + int db_flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_SHAREDCACHE; + sqlite3* dbhandle = NULL; + // This handle is only used from one thread + if (sqlite3_open_v2(inst.m_dbname.c_str(), &dbhandle, db_flags, NULL) == SQLITE_OK) + { + sqlite3_busy_timeout(dbhandle, 1000); + } + else + { + MXS_ERROR("Failed to open SQLite3 handle."); + } + PamClientSession* rval = NULL; + if (!dbhandle || (rval = new (std::nothrow) PamClientSession(dbhandle, inst)) == NULL) + { + sqlite3_close_v2(dbhandle); + } + return rval; +} + +/** + * @brief Check which PAM services the session user has access to + * + * @param auth Authenticator session + * @param dcb Client DCB + * @param session MySQL session + * + * @return An array of PAM service names for the session user + */ +void PamClientSession::get_pam_user_services(const DCB* dcb, const MYSQL_session* session, + StringVector* services_out) +{ + string services_query = string("SELECT authentication_string FROM ") + + m_instance.m_tablename + " WHERE user = '" + session->user + + "' AND '" + dcb->remote + "' LIKE host AND (anydb = '1' OR '" + + session->db + "' = '' OR '" + session->db + + "' LIKE db) ORDER BY authentication_string"; + MXS_DEBUG("PAM services search sql: '%s'.", services_query.c_str()); + char *err; + /** + * Try search twice: first time with the current users, second + * time with fresh users. + */ + for (int i = 0; i < 2; i++) + { + if (i == 0 || service_refresh_users(dcb->service) == 0) + { + if (sqlite3_exec(m_dbhandle, services_query.c_str(), user_services_cb, + services_out, &err) != SQLITE_OK) + { + MXS_ERROR("Failed to execute query: '%s'", err); + sqlite3_free(err); + } + else if (!services_out->empty()) + { + MXS_DEBUG("User '%s' matched %lu rows in %s db.", session->user, + services_out->size(), m_instance.m_tablename.c_str()); + break; + } + } + } +} + +/** + * @brief Create an AuthSwitchRequest packet + * + * The server (MaxScale) sends the plugin name "dialog" to the client with the + * first password prompt. We want to avoid calling the PAM conversation function + * more than once because it blocks, so we "emulate" its behaviour here. + * This obviously only works with the basic password authentication scheme. + * + * @return Allocated packet + * @see https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest + */ +Buffer PamClientSession::create_auth_change_packet() const +{ + /** + * The AuthSwitchRequest packet: + * 4 bytes - Header + * 0xfe - Command byte + * string[NUL] - Auth plugin name + * byte - Message type + * string[EOF] - Message + */ + size_t plen = 1 + DIALOG_SIZE + 1 + PASSWORD.length(); + size_t buflen = MYSQL_HEADER_LEN + plen; + uint8_t bufdata[buflen]; + uint8_t* pData = bufdata; + gw_mysql_set_byte3(pData, plen); + pData += 3; + *pData++ = m_sequence; + *pData++ = 0xfe; // AuthSwitchRequest command + memcpy(pData, DIALOG.c_str(), DIALOG_SIZE); // Plugin name + pData += DIALOG_SIZE; + *pData++ = DIALOG_ECHO_DISABLED; + memcpy(pData, PASSWORD.c_str(), PASSWORD.length()); // First message + + Buffer buffer(bufdata, buflen); + return buffer; +} + +int PamClientSession::authenticate(DCB* dcb) +{ + int rval = MXS_AUTH_FAILED; + if (m_state == PAM_AUTH_INIT) + { + /** We need to send the authentication switch packet to change the + * authentication to something other than the 'mysql_native_password' + * method */ + Buffer authbuf = create_auth_change_packet(); + if (authbuf.length() && dcb->func.write(dcb, authbuf.release())) + { + m_state = PAM_AUTH_DATA_SENT; + rval = MXS_AUTH_INCOMPLETE; + } + } + else if (m_state == PAM_AUTH_DATA_SENT) + { + /** We sent the authentication change packet + plugin name and the client + * responded with the password. Try to continue authentication without more + * messages to client. */ + MYSQL_session *ses = (MYSQL_session*)dcb->data; + string password((char*)ses->auth_token, ses->auth_token_len); + StringVector services; + get_pam_user_services(dcb, ses, &services); + + for (StringVector::const_iterator i = services.begin(); i != services.end(); i++) + { + if (validate_pam_password(ses->user, password, *i, dcb)) + { + rval = MXS_AUTH_SUCCEEDED; + break; + } + } + } + return rval; +} + +int PamClientSession::extract(DCB *dcb, GWBUF *buffer) +{ + gwbuf_copy_data(buffer, MYSQL_SEQ_OFFSET, 1, &m_sequence); + m_sequence++; + int rval = MXS_AUTH_FAILED; + + switch (m_state) + { + case PAM_AUTH_INIT: + // The buffer doesn't have any PAM-specific data yet + rval = MXS_AUTH_SUCCEEDED; + break; + + case PAM_AUTH_DATA_SENT: + store_client_password(dcb, buffer); + rval = MXS_AUTH_SUCCEEDED; + break; + + default: + MXS_ERROR("Unexpected authentication state: %d", m_state); + ss_dassert(!true); + break; + } + return rval; +} \ No newline at end of file diff --git a/server/modules/authenticator/PAM/PAMAuth/pam_client_session.hh b/server/modules/authenticator/PAM/PAMAuth/pam_client_session.hh new file mode 100644 index 000000000..d13b171d4 --- /dev/null +++ b/server/modules/authenticator/PAM/PAMAuth/pam_client_session.hh @@ -0,0 +1,46 @@ +#pragma once +/* + * 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: 2020-01-01 + * + * 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 "pam_auth.hh" + +#include +#include +#include +#include +#include "pam_instance.hh" +#include "../pam_auth_common.hh" + +/** Client authenticator PAM-specific session data */ +class PamClientSession +{ + PamClientSession(const PamClientSession& orig); + PamClientSession& operator=(const PamClientSession&); +public: + typedef std::vector StringVector; + static PamClientSession* create(const PamInstance& inst); + ~PamClientSession(); + int authenticate(DCB* client); + int extract(DCB *dcb, GWBUF *read_buffer); +private: + PamClientSession(sqlite3* dbhandle, const PamInstance& instance); + void get_pam_user_services(const DCB* dcb, const MYSQL_session* session, + StringVector* services_out); + maxscale::Buffer create_auth_change_packet() const; + + pam_auth_state m_state; /**< Authentication state*/ + uint8_t m_sequence; /**< The next packet seqence number */ + sqlite3* const m_dbhandle; /**< SQLite3 database handle */ + const PamInstance& m_instance; /**< Authenticator instance */ +}; + + diff --git a/server/modules/authenticator/PAM/PAMAuth/pam_instance.cc b/server/modules/authenticator/PAM/PAMAuth/pam_instance.cc new file mode 100644 index 000000000..cacadfa0a --- /dev/null +++ b/server/modules/authenticator/PAM/PAMAuth/pam_instance.cc @@ -0,0 +1,232 @@ +/* + * 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: 2020-01-01 + * + * 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 "pam_instance.hh" + +#include +#include +#include +#include +#include + +#define DEFAULT_PAM_DATABASE_NAME "file:pam.db?mode=memory&cache=shared" +#define DEFAULT_PAM_TABLE_NAME "pam_users" +using std::string; + +/** + * Create an instance. + * + * @param options Listener options + * @return New client authenticator instance or NULL on error + */ +PamInstance* PamInstance::create(char **options) +{ + /** Name of the in-memory database */ + const string pam_db_name = DEFAULT_PAM_DATABASE_NAME; + /** The table name where we store the users */ + const string pam_table_name = DEFAULT_PAM_TABLE_NAME; + /** CREATE TABLE statement for the in-memory table */ + const string create_sql = "CREATE TABLE IF NOT EXISTS " + pam_table_name + + " (user varchar(255), host varchar(255), db varchar(255), " + "anydb boolean, authentication_string text)"; + if (sqlite3_threadsafe() == 0) + { + MXS_WARNING("SQLite3 was compiled with thread safety off. May cause " + "corruption of in-memory database."); + } + bool error = false; + /* This handle may be used from multiple threads, set full mutex. */ + sqlite3* dbhandle = NULL; + int db_flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | + SQLITE_OPEN_SHAREDCACHE | SQLITE_OPEN_FULLMUTEX; + if (sqlite3_open_v2(pam_db_name.c_str(), &dbhandle, db_flags, NULL) != SQLITE_OK) + { + MXS_ERROR("Failed to open SQLite3 handle."); + error = true; + } + + char *err; + if (!error && sqlite3_exec(dbhandle, create_sql.c_str(), NULL, NULL, &err) != SQLITE_OK) + { + MXS_ERROR("Failed to create database: '%s'", err); + sqlite3_free(err); + error = true; + } + + PamInstance *instance = NULL; + if (!error && + ((instance = new (std::nothrow) PamInstance(dbhandle, pam_db_name, pam_table_name)) == NULL)) + { + sqlite3_close_v2(dbhandle); + } + return instance; +} + +/** + * Constructor. + * + * @param dbhandle Database handle + * @param dbname Text-form name of @c dbhandle + * @param tablename Name of table where authentication data is saved + */ +PamInstance::PamInstance(sqlite3* dbhandle, const string& dbname, const string& tablename) + : m_dbname(dbname) + , m_tablename(tablename) + , m_dbhandle(dbhandle) +{ +} + +/** + * @brief Add new PAM user entry to the internal user database + * + * @param user Username + * @param host Host + * @param db Database + * @param anydb Global access to databases + * @param pam_service The PAM service used + */ +void PamInstance::add_pam_user(const char *user, const char *host, + const char *db, bool anydb, const char *pam_service) +{ + /** + * The insert query template which adds users to the pam_users table. + * + * Note that 'db' and 'pam_service' are strings that can be NULL and thus they have + * no quotes around them. The quotes for strings are added in this function. + */ + const string insert_sql_template = + "INSERT INTO " + m_tablename + " VALUES ('%s', '%s', %s, '%s', %s)"; + + /** Used for NULL value creation in the INSERT query */ + const char NULL_TOKEN[] = "NULL"; + string db_str; + + if (db) + { + db_str = string("'") + db + "'"; + } + else + { + db_str = NULL_TOKEN; + } + + string service_str; + if (pam_service && *pam_service) + { + service_str = string("'") + pam_service + "'"; + } + else + { + service_str = NULL_TOKEN; + } + + size_t len = insert_sql_template.length() + strlen(user) + strlen(host) + db_str.length() + + service_str.length() + 1; + + char insert_sql[len + 1]; + sprintf(insert_sql, insert_sql_template.c_str(), user, host, db_str.c_str(), + anydb ? "1" : "0", service_str.c_str()); + + char *err; + if (sqlite3_exec(m_dbhandle, insert_sql, NULL, NULL, &err) != SQLITE_OK) + { + MXS_ERROR("Failed to insert user: %s", err); + sqlite3_free(err); + } +} + +/** + * @brief Delete old users from the database + */ +void PamInstance::delete_old_users() +{ + /** Delete query used to clean up the database before loading new users */ + const string delete_query = "DELETE FROM " + m_tablename; + char *err; + if (sqlite3_exec(m_dbhandle, delete_query.c_str(), NULL, NULL, &err) != SQLITE_OK) + { + MXS_ERROR("Failed to delete old users: %s", err); + sqlite3_free(err); + } +} + +/** + * @brief Populates the internal user database by reading from one of the backend servers + * + * @param service The service the users should be read from + * + * @return MXS_AUTH_LOADUSERS_OK on success, MXS_AUTH_LOADUSERS_ERROR on error + */ +int PamInstance::load_users(SERVICE* service) +{ + /** Query that gets all users that authenticate via the pam plugin */ + const char PAM_USERS_QUERY[] = + "SELECT u.user, u.host, d.db, u.select_priv, u.authentication_string FROM " + "mysql.user AS u LEFT JOIN mysql.db AS d " + "ON (u.user = d.user AND u.host = d.host) WHERE u.plugin = 'pam' " + "UNION " + "SELECT u.user, u.host, t.db, u.select_priv, u.authentication_string FROM " + "mysql.user AS u LEFT JOIN mysql.tables_priv AS t " + "ON (u.user = t.user AND u.host = t.host) WHERE u.plugin = 'pam' " + "ORDER BY user"; +#if defined(SS_DEBUG) + const int PAM_USERS_QUERY_NUM_FIELDS = 5; +#endif + + char *user, *pw; + int rval = MXS_AUTH_LOADUSERS_ERROR; + + if (serviceGetUser(service, &user, &pw) && (pw = decrypt_password(pw))) + { + for (SERVER_REF *servers = service->dbref; servers; servers = servers->next) + { + MYSQL *mysql = mysql_init(NULL); + if (mxs_mysql_real_connect(mysql, servers->server, user, pw)) + { + if (mysql_query(mysql, PAM_USERS_QUERY)) + { + MXS_ERROR("Failed to query server '%s' for PAM users: '%s'.", + servers->server->unique_name, mysql_error(mysql)); + } + else + { + MYSQL_RES *res = mysql_store_result(mysql); + delete_old_users(); + if (res) + { + ss_dassert(mysql_num_fields(res) == PAM_USERS_QUERY_NUM_FIELDS); + MXS_NOTICE("Loaded %llu users for service %s.", mysql_num_rows(res), + service->name); + MYSQL_ROW row; + while ((row = mysql_fetch_row(res))) + { + add_pam_user(row[0], row[1], row[2], + row[3] && strcasecmp(row[3], "Y") == 0, + row[4]); + } + rval = MXS_AUTH_LOADUSERS_OK; + mysql_free_result(res); + } + } + mysql_close(mysql); + + if (rval == MXS_AUTH_LOADUSERS_OK) + { + break; + } + } + } + MXS_FREE(pw); + } + return rval; +} \ No newline at end of file diff --git a/server/modules/authenticator/PAM/PAMAuth/pam_instance.hh b/server/modules/authenticator/PAM/PAMAuth/pam_instance.hh new file mode 100644 index 000000000..e0015fbcb --- /dev/null +++ b/server/modules/authenticator/PAM/PAMAuth/pam_instance.hh @@ -0,0 +1,41 @@ +#pragma once +/* + * 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: 2020-01-01 + * + * 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 "pam_auth.hh" + +#include +#include +#include + +/** The instance class for the client side PAM authenticator, created in pam_auth_init() */ +class PamInstance +{ + PamInstance(const PamInstance& orig); + PamInstance& operator=(const PamInstance&); +public: + static PamInstance* create(char **options); + ~PamInstance(); + int load_users(SERVICE* service); + + const std::string m_dbname; /**< Name of the in-memory database */ + const std::string m_tablename; /**< The table where users are stored */ +private: + PamInstance(sqlite3* dbhandle, const std::string& m_dbname, const std::string& tablename); + void add_pam_user(const char *user, const char *host, const char *db, bool anydb, + const char *pam_service); + void delete_old_users(); + + sqlite3 * const m_dbhandle; /**< SQLite3 database handle */ +}; + + diff --git a/server/modules/authenticator/PAM/PAMBackendAuth/CMakeLists.txt b/server/modules/authenticator/PAM/PAMBackendAuth/CMakeLists.txt index 219fb2dc1..fb9fb4539 100644 --- a/server/modules/authenticator/PAM/PAMBackendAuth/CMakeLists.txt +++ b/server/modules/authenticator/PAM/PAMBackendAuth/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(PAMBackendAuth SHARED pam_backend_auth.cc ../pam_auth_common.cc) +add_library(PAMBackendAuth SHARED pam_backend_auth.cc ../pam_auth_common.cc pam_backend_session.cc) target_link_libraries(PAMBackendAuth maxscale-common MySQLCommon ${SQLITE_LIBRARIES}) set_target_properties(PAMBackendAuth PROPERTIES VERSION "1.0.0") install_module(PAMBackendAuth core) diff --git a/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_auth.cc b/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_auth.cc index 174a65304..4b5b8cc28 100644 --- a/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_auth.cc +++ b/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_auth.cc @@ -11,172 +11,44 @@ * Public License. */ -#define MXS_MODULE_NAME "PAMBackendAuth" - -#include "../pam_auth.hh" +#include "pam_backend_auth.hh" #include -#include -#include -#include #include -#include +#include "pam_backend_session.hh" +#include "../pam_auth_common.hh" - -/** - * @file pam_backend_auth.c - PAM backend authenticator - */ - -namespace +static void* pam_backend_auth_alloc(void *instance) { -/** - * Send password to server - * - * @param dcb Backend DCB - * @return True on success, false on error - */ -bool send_client_password(DCB *dcb) -{ - bool rval = false; - MYSQL_session *ses = (MYSQL_session*)dcb->session->client_dcb->data; - PamSession* pses = static_cast(dcb->authenticator_data); - size_t buflen = MYSQL_HEADER_LEN + ses->auth_token_len; - uint8_t bufferdata[buflen]; - gw_mysql_set_byte3(bufferdata, ses->auth_token_len); - bufferdata[MYSQL_SEQ_OFFSET] = pses->m_sequence++; - memcpy(bufferdata + MYSQL_HEADER_LEN, ses->auth_token, ses->auth_token_len); - maxscale::Buffer pwbuf(bufferdata, buflen); - return dcb_write(dcb, pwbuf.release()); -} - -/** - * Check that the AuthSwitchRequest packet is as expected. Inverse of - * create_auth_change_packet() in pam_auth.cc. - * - * @param dcb Backend DCB - * @param buffer Buffer containing an AuthSwitchRequest packet - * @return True on success, false on error - */ -bool check_auth_switch_request(DCB *dcb, GWBUF *buffer) -{ - const char ERRMSG_P2[] = "Only simple password-based PAM authentication is supported."; - /** - * The AuthSwitchRequest packet: - * 4 bytes - Header - * 0xfe - Command byte - * string[NUL] - Auth plugin name - * byte - Message type - * string[EOF] - Message - */ - /** We know how long the packet should be in the simple case. */ - unsigned int expected_buflen = MYSQL_HEADER_LEN + 1 + sizeof(DIALOG) + - 1 + sizeof(PASSWORD) - 1 /* no terminating 0 */; - uint8_t data[expected_buflen]; - size_t copied = gwbuf_copy_data(buffer, 0, expected_buflen, data); - - /* Check that this is an AuthSwitchRequest. */ - if ((copied <= MYSQL_HEADER_LEN) || (data[MYSQL_HEADER_LEN] != MYSQL_REPLY_AUTHSWITCHREQUEST)) - { - /** Server responded with something we did not expect. If it's an OK packet, - * it's possible that the server authenticated us as the anonymous user. This - * means that the server is not secure. */ - bool was_ok_packet = copied > MYSQL_HEADER_LEN && - data[MYSQL_HEADER_LEN + 1] == MYSQL_REPLY_OK; - MXS_ERROR("Server '%s' returned an unexpected authentication response.%s", - dcb->server->unique_name, was_ok_packet ? - " Authentication was complete before it even started, " - "anonymous users might not be disabled." : ""); - return false; - } - unsigned int buflen = gwbuf_length(buffer); - if (buflen != expected_buflen) - { - MXS_ERROR("Length of server AuthSwitchRequest packet was '%u', expected '%u'. %s", - buflen, expected_buflen, ERRMSG_P2); - return false; - } - - PamSession *pses = static_cast(dcb->authenticator_data); - pses->m_sequence = data[MYSQL_SEQ_OFFSET] + 1; - - /* Check that the server is using the "dialog" plugin and asking for the password. */ - uint8_t* plugin_name_loc = data + MYSQL_HEADER_LEN + 1; - uint8_t* msg_type_loc = plugin_name_loc + sizeof(DIALOG); - uint8_t msg_type = *msg_type_loc; - uint8_t* msg_loc = msg_type_loc + 1; - - bool rval = false; - if ((strcmp((char*)plugin_name_loc, DIALOG) == 0) && - // 2 and 4 are constants used by the dialog plugin - (msg_type == 2 || msg_type == 4) && - strncmp((char*)msg_loc, PASSWORD, sizeof(PASSWORD) - 1) == 0) - { - rval = true; - } - else - { - MXS_ERROR("AuthSwitchRequest packet contents unexpected. %s", ERRMSG_P2); - } - return rval; -} - -} - -void* pam_backend_auth_alloc(void *instance) -{ - PamSession* pses = new PamSession; + PamBackendSession* pses = new (std::nothrow) PamBackendSession(); return pses; } -void pam_backend_auth_free(void *data) +static void pam_backend_auth_free(void *data) { - delete static_cast(data); + delete static_cast(data); } /** * @brief Extract data from a MySQL packet + * * @param dcb Backend DCB * @param buffer Buffer containing a complete packet + * * @return MXS_AUTH_INCOMPLETE if authentication is ongoing, MXS_AUTH_SUCCEEDED * if authentication is complete and MXS_AUTH_FAILED if authentication failed. */ static int pam_backend_auth_extract(DCB *dcb, GWBUF *buffer) { - int rval = MXS_AUTH_FAILED; - PamSession *pses = static_cast(dcb->authenticator_data); - - if (pses->m_state == PAM_AUTH_INIT && check_auth_switch_request(dcb, buffer)) - { - rval = MXS_AUTH_INCOMPLETE; - } - else if (pses->m_state == PAM_AUTH_DATA_SENT) - { - /** Read authentication response */ - if (mxs_mysql_is_ok_packet(buffer)) - { - MXS_DEBUG("pam_backend_auth_extract received ok packet from '%s'.", - dcb->server->unique_name); - pses->m_state = PAM_AUTH_OK; - rval = MXS_AUTH_SUCCEEDED; - } - else - { - MXS_ERROR("Expected ok from server but got something else. Authentication" - " failed."); - } - } - - if (rval == MXS_AUTH_FAILED) - { - MXS_DEBUG("pam_backend_auth_extract to backend '%s' failed for user '%s'.", - dcb->server->unique_name, dcb->user); - } - return rval; + PamBackendSession *pses = static_cast(dcb->authenticator_data); + return pses->extract(dcb, buffer); } /** * @brief Check whether the DCB supports SSL + * * @param dcb Backend DCB + * * @return True if DCB supports SSL */ static bool pam_backend_auth_connectssl(DCB *dcb) @@ -185,32 +57,17 @@ static bool pam_backend_auth_connectssl(DCB *dcb) } /** - * @brief Authenticate the backend connection + * @brief Authenticate to backend. Should be called after extract() + * * @param dcb Backend DCB + * * @return MXS_AUTH_INCOMPLETE if authentication is ongoing, MXS_AUTH_SUCCEEDED * if authentication is complete and MXS_AUTH_FAILED if authentication failed. */ static int pam_backend_auth_authenticate(DCB *dcb) { - int rval = MXS_AUTH_FAILED; - PamSession *pses = static_cast(dcb->authenticator_data); - - if (pses->m_state == PAM_AUTH_INIT) - { - MXS_DEBUG("pam_backend_auth_authenticate sending password to '%s'.", - dcb->server->unique_name); - if (send_client_password(dcb)) - { - rval = MXS_AUTH_INCOMPLETE; - pses->m_state = PAM_AUTH_DATA_SENT; - } - } - else if (pses->m_state == PAM_AUTH_OK) - { - rval = MXS_AUTH_SUCCEEDED; - } - - return rval; + PamBackendSession *pses = static_cast(dcb->authenticator_data); + return pses->authenticate(dcb); } MXS_BEGIN_DECLS @@ -221,17 +78,17 @@ MXS_MODULE* MXS_CREATE_MODULE() { static MXS_AUTHENTICATOR MyObject = { - NULL, /* No initialize entry point */ + NULL, /* No initialize entry point */ pam_backend_auth_alloc, /* Allocate authenticator data */ pam_backend_auth_extract, /* Extract data into structure */ pam_backend_auth_connectssl, /* Check if client supports SSL */ pam_backend_auth_authenticate, /* Authenticate user credentials */ - NULL, /* Client plugin will free shared data */ + NULL, /* Client plugin will free shared data */ pam_backend_auth_free, /* Free authenticator data */ - NULL, /* Load users from backend databases */ - NULL, /* No diagnostic */ + NULL, /* Load users from backend databases */ + NULL, /* No diagnostic */ NULL, - NULL /* No user reauthentication */ + NULL /* No user reauthentication */ }; static MXS_MODULE info = diff --git a/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_auth.hh b/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_auth.hh new file mode 100644 index 000000000..9a439eab4 --- /dev/null +++ b/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_auth.hh @@ -0,0 +1,29 @@ +/* + * 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: 2020-01-01 + * + * 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. + */ + +/** + * Common definitions and includes for PAMBackendAuth + */ +#ifndef PAM_BACKEND_AUTH_HH +#define PAM_BACKEND_AUTH_HH + +#define MXS_MODULE_NAME "PAMBackendAuth" + +#include + +#include +#include +#include + +#endif /* PAM_BACKEND_AUTH_HH */ + diff --git a/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_session.cc b/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_session.cc new file mode 100644 index 000000000..75191c191 --- /dev/null +++ b/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_session.cc @@ -0,0 +1,162 @@ +/* + * 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: 2020-01-01 + * + * 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 "pam_backend_session.hh" + +namespace +{ + /** + * Check that the AuthSwitchRequest packet is as expected. Inverse of + * create_auth_change_packet() in pam_auth.cc. + * + * @param dcb Backend DCB + * @param buffer Buffer containing an AuthSwitchRequest packet + * @return True on success, false on error + */ +bool check_auth_switch_request(DCB *dcb, GWBUF *buffer) +{ + /** + * The AuthSwitchRequest packet: + * 4 bytes - Header + * 0xfe - Command byte + * string[NUL] - Auth plugin name + * byte - Message type + * string[EOF] - Message + */ + /** We know how long the packet should be in the simple case. */ + unsigned int expected_buflen = MYSQL_HEADER_LEN + 1 + DIALOG_SIZE + 1 + PASSWORD.length(); + uint8_t data[expected_buflen]; + size_t copied = gwbuf_copy_data(buffer, 0, expected_buflen, data); + + /* Check that this is an AuthSwitchRequest. */ + if ((copied <= MYSQL_HEADER_LEN) || (data[MYSQL_HEADER_LEN] != MYSQL_REPLY_AUTHSWITCHREQUEST)) + { + /** Server responded with something we did not expect. If it's an OK packet, + * it's possible that the server authenticated us as the anonymous user. This + * means that the server is not secure. */ + bool was_ok_packet = copied > MYSQL_HEADER_LEN && + data[MYSQL_HEADER_LEN + 1] == MYSQL_REPLY_OK; + MXS_ERROR("Server '%s' returned an unexpected authentication response.%s", + dcb->server->unique_name, was_ok_packet ? + " Authentication was complete before it even started, " + "anonymous users might not be disabled." : ""); + return false; + } + unsigned int buflen = gwbuf_length(buffer); + if (buflen != expected_buflen) + { + MXS_ERROR("Length of server AuthSwitchRequest packet was '%u', expected '%u'. %s", + buflen, expected_buflen, GENERAL_ERRMSG); + return false; + } + + /* Check that the server is using the "dialog" plugin and asking for the password. */ + uint8_t* plugin_name_loc = data + MYSQL_HEADER_LEN + 1; + uint8_t* msg_type_loc = plugin_name_loc + DIALOG_SIZE; + uint8_t msg_type = *msg_type_loc; + uint8_t* msg_loc = msg_type_loc + 1; + + bool rval = false; + if ((DIALOG == (char*)plugin_name_loc) && + (msg_type == DIALOG_ECHO_ENABLED || msg_type == DIALOG_ECHO_DISABLED) && + PASSWORD.compare(0, PASSWORD.length(), (char*)msg_loc, PASSWORD.length()) == 0) + { + rval = true; + } + else + { + MXS_ERROR("AuthSwitchRequest packet contents unexpected. %s", GENERAL_ERRMSG); + } + return rval; +} +} +PamBackendSession::PamBackendSession() + : m_state(PAM_AUTH_INIT) + , m_sequence(0) +{ +} + +/** + * Send password to server + * + * @param dcb Backend DCB + * @return True on success, false on error + */ +bool PamBackendSession::send_client_password(DCB *dcb) +{ + bool rval = false; + MYSQL_session *ses = (MYSQL_session*)dcb->session->client_dcb->data; + size_t buflen = MYSQL_HEADER_LEN + ses->auth_token_len; + uint8_t bufferdata[buflen]; + gw_mysql_set_byte3(bufferdata, ses->auth_token_len); + bufferdata[MYSQL_SEQ_OFFSET] = m_sequence; + memcpy(bufferdata + MYSQL_HEADER_LEN, ses->auth_token, ses->auth_token_len); + return dcb_write(dcb, gwbuf_alloc_and_load(buflen, bufferdata)); +} + +int PamBackendSession::extract(DCB *dcb, GWBUF *buffer) +{ + gwbuf_copy_data(buffer, MYSQL_SEQ_OFFSET, 1, &m_sequence); + m_sequence++; + int rval = MXS_AUTH_FAILED; + + if (m_state == PAM_AUTH_INIT && check_auth_switch_request(dcb, buffer)) + { + rval = MXS_AUTH_INCOMPLETE; + } + else if (m_state == PAM_AUTH_DATA_SENT) + { + /** Read authentication response */ + if (mxs_mysql_is_ok_packet(buffer)) + { + MXS_DEBUG("pam_backend_auth_extract received ok packet from '%s'.", + dcb->server->unique_name); + m_state = PAM_AUTH_OK; + rval = MXS_AUTH_SUCCEEDED; + } + else + { + MXS_ERROR("Expected ok from server but got something else. Authentication" + " failed."); + } + } + + if (rval == MXS_AUTH_FAILED) + { + MXS_DEBUG("pam_backend_auth_extract to backend '%s' failed for user '%s'.", + dcb->server->unique_name, dcb->user); + } + return rval; +} + +int PamBackendSession::authenticate(DCB *dcb) +{ + int rval = MXS_AUTH_FAILED; + + if (m_state == PAM_AUTH_INIT) + { + MXS_DEBUG("pam_backend_auth_authenticate sending password to '%s'.", + dcb->server->unique_name); + if (send_client_password(dcb)) + { + rval = MXS_AUTH_INCOMPLETE; + m_state = PAM_AUTH_DATA_SENT; + } + } + else if (m_state == PAM_AUTH_OK) + { + rval = MXS_AUTH_SUCCEEDED; + } + + return rval; +} diff --git a/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_session.hh b/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_session.hh new file mode 100644 index 000000000..263c30d96 --- /dev/null +++ b/server/modules/authenticator/PAM/PAMBackendAuth/pam_backend_session.hh @@ -0,0 +1,39 @@ +/* + * 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: 2020-01-01 + * + * 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. + */ + +#ifndef PAMBACKENDSESSION_HH +#define PAMBACKENDSESSION_HH + +#include "pam_backend_auth.hh" + +#include +#include "../pam_auth_common.hh" + +class PamBackendSession +{ + PamBackendSession(const PamBackendSession& orig); + PamBackendSession& operator=(const PamBackendSession&); +public: + PamBackendSession(); + int extract(DCB *dcb, GWBUF *buffer); + int authenticate(DCB *dcb); + +private: + bool send_client_password(DCB *dcb); + + pam_auth_state m_state; /**< Authentication state*/ + uint8_t m_sequence; /**< The next packet seqence number */ +}; + +#endif /* PAMBACKENDSESSION_HH */ + diff --git a/server/modules/authenticator/PAM/pam_auth.hh b/server/modules/authenticator/PAM/pam_auth.hh deleted file mode 100644 index cb17c3678..000000000 --- a/server/modules/authenticator/PAM/pam_auth.hh +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once -#ifndef _PAM_AUTH_H -#define _PAM_AUTH_H -/* - * 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: 2020-01-01 - * - * 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 - -#include -#include -#include -#include -#include -#include -#include - -typedef std::vector StringArray; - -/** Name if the PAM client helper plugin */ -const char DIALOG[] = "dialog"; // or "mysql_clear_password" -const char PASSWORD[] = "Password: "; - -/** PAM authentication states */ -enum pam_auth_state -{ - PAM_AUTH_INIT = 0, - PAM_AUTH_DATA_SENT, - PAM_AUTH_OK, - PAM_AUTH_FAILED -}; - -// Magic numbers from server source https://github.com/MariaDB/server/blob/10.2/plugin/auth_pam/auth_pam.c -enum dialog_plugin_msg_types -{ - DIALOG_ECHO_ENABLED = 2, - DIALOG_ECHO_DISABLED = 4 -}; - -/** Common structure for both backend and client authenticators */ -struct PamSession -{ - PamSession() - { - m_state = PAM_AUTH_INIT; - m_sequence = 0; - m_dbhandle = NULL; - } - ~PamSession() - { - sqlite3_close_v2(m_dbhandle); - } - - pam_auth_state m_state; /**< Authentication state*/ - uint8_t m_sequence; /**< The next packet seqence number */ - sqlite3* m_dbhandle; /**< SQLite3 database handle */ -}; - -#endif diff --git a/server/modules/authenticator/PAM/pam_auth_common.cc b/server/modules/authenticator/PAM/pam_auth_common.cc index ec2138c88..83b89de2b 100644 --- a/server/modules/authenticator/PAM/pam_auth_common.cc +++ b/server/modules/authenticator/PAM/pam_auth_common.cc @@ -11,7 +11,16 @@ * Public License. */ -#include "pam_auth.hh" +/** + * Set values for constants shared between both PAMAuth and PAMBackendAuth + */ +#include "pam_auth_common.hh" -#include -#include +/* PAM client helper plugin name, TODO: add support for "mysql_clear_password" */ +const std::string DIALOG = "dialog"; +/* The total storage required */ +const int DIALOG_SIZE = DIALOG.length() + 1; +/* First query from server */ +const std::string PASSWORD = "Password: "; +const char GENERAL_ERRMSG[] = "Only simple password-based PAM authentication with one call " + "to the conversation function is supported."; diff --git a/server/modules/authenticator/PAM/pam_auth_common.hh b/server/modules/authenticator/PAM/pam_auth_common.hh new file mode 100644 index 000000000..f4bcb6ee3 --- /dev/null +++ b/server/modules/authenticator/PAM/pam_auth_common.hh @@ -0,0 +1,43 @@ +#pragma once + +/* + * 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: 2020-01-01 + * + * 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. + */ + +/** + * Common declarations for both PAMAuth and PAMBackendAuth + */ +#include +#include + +extern const std::string DIALOG; +extern const std::string PASSWORD; +extern const int DIALOG_SIZE; +extern const char GENERAL_ERRMSG[]; + +/** PAM authentication states */ +enum pam_auth_state +{ + PAM_AUTH_INIT = 0, + PAM_AUTH_DATA_SENT, + PAM_AUTH_OK, + PAM_AUTH_FAILED +}; + +/* Magic numbers from server source + https://github.com/MariaDB/server/blob/10.2/plugin/auth_pam/auth_pam.c */ +enum dialog_plugin_msg_types +{ + DIALOG_ECHO_ENABLED = 2, + DIALOG_ECHO_DISABLED = 4 +}; +