Centos6 uses a very old version of SQLite without support for URI filenames. PAM authenticator must use a file-based database. Commit cherry-picked to 2.4.0 from 2.3.
		
			
				
	
	
		
			415 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			415 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
 * 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-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 <string>
 | 
						|
#include <string.h>
 | 
						|
#include <maxscale/jansson.hh>
 | 
						|
#include <maxscale/paths.h>
 | 
						|
#include <maxscale/secrets.h>
 | 
						|
#include <maxscale/mysql_utils.hh>
 | 
						|
 | 
						|
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.
 | 
						|
    // TODO: Once Centos6 is no longer needed and Sqlite version 3.7+ can be assumed,
 | 
						|
    // use a memory-only db with a URI filename (e.g. file:pam.db?mode=memory&cache=shared)
 | 
						|
    const string pam_db_fname = string(get_cachedir()) + "/pam_db.sqlite3";
 | 
						|
    // The table name where we store the users
 | 
						|
    const string pam_table_name = "pam_users";
 | 
						|
    /** Deletion statement for the in-memory table */
 | 
						|
    const string drop_sql = string("DROP TABLE IF EXISTS ") + pam_table_name + ";";
 | 
						|
    /** CREATE TABLE statement for the in-memory table */
 | 
						|
    const string create_sql = string("CREATE TABLE ") + pam_table_name
 | 
						|
        + " (" + FIELD_USER + " varchar(255), "
 | 
						|
        + FIELD_HOST + " varchar(255), "
 | 
						|
        + FIELD_DB + " varchar(255), "
 | 
						|
        + FIELD_ANYDB + " boolean, "
 | 
						|
        + FIELD_AUTHSTR + " text, "
 | 
						|
        + FIELD_PROXY + " boolean);";
 | 
						|
 | 
						|
    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;
 | 
						|
    const char* filename = pam_db_fname.c_str();
 | 
						|
    if (sqlite3_open_v2(filename, &dbhandle, db_flags, NULL) != SQLITE_OK)
 | 
						|
    {
 | 
						|
        // Even if the open failed, the handle may exist and an error message can be read.
 | 
						|
        if (dbhandle)
 | 
						|
        {
 | 
						|
            MXS_ERROR(SQLITE_OPEN_FAIL, filename, sqlite3_errmsg(dbhandle));
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            // This means memory allocation failed.
 | 
						|
            MXS_ERROR(SQLITE_OPEN_OOM, filename);
 | 
						|
        }
 | 
						|
        error = true;
 | 
						|
    }
 | 
						|
 | 
						|
    char *err = NULL;
 | 
						|
    if (!error && sqlite3_exec(dbhandle, drop_sql.c_str(), NULL, NULL, &err) != SQLITE_OK)
 | 
						|
    {
 | 
						|
        MXS_ERROR("Failed to drop table: '%s'", err);
 | 
						|
        sqlite3_free(err);
 | 
						|
        error = true;
 | 
						|
    }
 | 
						|
    if (!error && sqlite3_exec(dbhandle, create_sql.c_str(), NULL, NULL, &err) != SQLITE_OK)
 | 
						|
    {
 | 
						|
        MXS_ERROR("Failed to create table: '%s'", err);
 | 
						|
        sqlite3_free(err);
 | 
						|
        error = true;
 | 
						|
    }
 | 
						|
 | 
						|
    PamInstance *instance = NULL;
 | 
						|
    if (!error &&
 | 
						|
        ((instance = new (std::nothrow) PamInstance(dbhandle, pam_db_fname, pam_table_name)) == NULL))
 | 
						|
    {
 | 
						|
        error = true;
 | 
						|
    }
 | 
						|
 | 
						|
    if (error)
 | 
						|
    {
 | 
						|
        // Close the handle even if never opened.
 | 
						|
        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
 | 
						|
 * @param proxy  Is the user anonymous with a proxy grant
 | 
						|
 */
 | 
						|
void PamInstance::add_pam_user(const char* user, const char* host, const char* db, bool anydb,
 | 
						|
                               const char* pam_service, bool proxy)
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * 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, '%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(),
 | 
						|
            proxy ? "1" : "0");
 | 
						|
 | 
						|
    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);
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
        if (proxy)
 | 
						|
        {
 | 
						|
            MXS_INFO("Added anonymous PAM user ''@'%s' with proxy grants using service %s.",
 | 
						|
                     host, service_str.c_str());
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            MXS_INFO("Added normal PAM user '%s'@'%s' using service %s.", user, host, service_str.c_str());
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @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' AND (d.db IS NOT NULL OR u.select_priv = 'Y')) "
 | 
						|
        "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' AND t.db IS NOT NULL AND u.select_priv = 'N') "
 | 
						|
        "ORDER BY user";
 | 
						|
#if defined (SS_DEBUG)
 | 
						|
    const unsigned int PAM_USERS_QUERY_NUM_FIELDS = 5;
 | 
						|
#endif
 | 
						|
 | 
						|
    const char* user;
 | 
						|
    const char* password;
 | 
						|
    serviceGetUser(service, &user, &password);
 | 
						|
    int rval = MXS_AUTH_LOADUSERS_ERROR;
 | 
						|
    char* pw;
 | 
						|
 | 
						|
    if ((pw = decrypt_password(password)))
 | 
						|
    {
 | 
						|
        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->name(), mysql_error(mysql));
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                    MYSQL_RES* res = mysql_store_result(mysql);
 | 
						|
                    delete_old_users();
 | 
						|
                    if (res)
 | 
						|
                    {
 | 
						|
                        mxb_assert(mysql_num_fields(res) == PAM_USERS_QUERY_NUM_FIELDS);
 | 
						|
                        MYSQL_ROW row;
 | 
						|
                        while ((row = mysql_fetch_row(res)))
 | 
						|
                        {
 | 
						|
                            add_pam_user(row[0], row[1],                                // user, host
 | 
						|
                                         row[2], row[3] && strcasecmp(row[3], "Y") == 0,// db, anydb
 | 
						|
                                         row[4],                                        // pam service
 | 
						|
                                         false);                                        // not a proxy
 | 
						|
                        }
 | 
						|
                        mysql_free_result(res);
 | 
						|
                    }
 | 
						|
 | 
						|
                    if (fetch_anon_proxy_users(servers->server, mysql))
 | 
						|
                    {
 | 
						|
                        rval = MXS_AUTH_LOADUSERS_OK;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                mysql_close(mysql);
 | 
						|
 | 
						|
                if (rval == MXS_AUTH_LOADUSERS_OK)
 | 
						|
                {
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        MXS_FREE(pw);
 | 
						|
    }
 | 
						|
    return rval;
 | 
						|
}
 | 
						|
 | 
						|
void PamInstance::diagnostic(DCB* dcb)
 | 
						|
{
 | 
						|
    json_t* array = diagnostic_json();
 | 
						|
    mxb_assert(json_is_array(array));
 | 
						|
 | 
						|
    string result, separator;
 | 
						|
    size_t index;
 | 
						|
    json_t* value;
 | 
						|
    json_array_foreach(array, index, value)
 | 
						|
    {
 | 
						|
        // Only print user@host for the non-json version, as this should fit nicely on the console. Add the
 | 
						|
        // other fields if deemed useful.
 | 
						|
        const char* user = json_string_value(json_object_get(value, FIELD_USER.c_str()));
 | 
						|
        const char* host = json_string_value(json_object_get(value, FIELD_HOST.c_str()));
 | 
						|
        if (user && host)
 | 
						|
        {
 | 
						|
            result += separator + user + "@" + host;
 | 
						|
            separator = " ";
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if (!result.empty())
 | 
						|
    {
 | 
						|
        dcb_printf(dcb, "%s", result.c_str());
 | 
						|
    }
 | 
						|
    json_decref(array);
 | 
						|
}
 | 
						|
 | 
						|
static int diag_cb_json(void* data, int columns, char** row, char** field_names)
 | 
						|
{
 | 
						|
    mxb_assert(columns == NUM_FIELDS);
 | 
						|
    json_t* obj = json_object();
 | 
						|
    for (int i = 0; i < columns; i++)
 | 
						|
    {
 | 
						|
        json_object_set_new(obj, field_names[i], json_string(row[i]));
 | 
						|
    }
 | 
						|
    json_t* arr = static_cast<json_t*>(data);
 | 
						|
    json_array_append_new(arr, obj);
 | 
						|
    return 0;
 | 
						|
}
 | 
						|
 | 
						|
json_t* PamInstance::diagnostic_json()
 | 
						|
{
 | 
						|
    json_t* rval = json_array();
 | 
						|
    char* err;
 | 
						|
    string select = "SELECT * FROM " + m_tablename + ";";
 | 
						|
    if (sqlite3_exec(m_dbhandle, select.c_str(), diag_cb_json, rval, &err) != SQLITE_OK)
 | 
						|
    {
 | 
						|
        MXS_ERROR("Failed to print users: %s", err);
 | 
						|
        sqlite3_free(err);
 | 
						|
    }
 | 
						|
 | 
						|
    return rval;
 | 
						|
}
 | 
						|
 | 
						|
bool PamInstance::fetch_anon_proxy_users(SERVER* server, MYSQL* conn)
 | 
						|
{
 | 
						|
    bool success = true;
 | 
						|
    const char ANON_USER_QUERY[] = "SELECT host,authentication_string FROM mysql.user WHERE "
 | 
						|
                                   "(plugin = 'pam' AND user = '');";
 | 
						|
 | 
						|
    const char GRANT_PROXY[] = "GRANT PROXY ON";
 | 
						|
 | 
						|
    // Query for the anonymous user which is used with group mappings
 | 
						|
    if (mysql_query(conn, ANON_USER_QUERY))
 | 
						|
    {
 | 
						|
        MXS_ERROR("Failed to query server '%s' for anonymous PAM users: '%s'.",
 | 
						|
                  server->name(), mysql_error(conn));
 | 
						|
        success = false;
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
        // Temporary storage of host,authentication_string for anonymous pam users.
 | 
						|
        std::vector<std::pair<string, string>> anon_users_info;
 | 
						|
        MYSQL_RES* res = mysql_store_result(conn);
 | 
						|
        if (res)
 | 
						|
        {
 | 
						|
            MYSQL_ROW row;
 | 
						|
            while ((row = mysql_fetch_row(res)))
 | 
						|
            {
 | 
						|
                string host = row[0] ? row[0] : "";
 | 
						|
                string auth_str = row[1] ? row[1] : "";
 | 
						|
                anon_users_info.push_back(std::make_pair(host, auth_str));
 | 
						|
            }
 | 
						|
            mysql_free_result(res);
 | 
						|
        }
 | 
						|
 | 
						|
        if (!anon_users_info.empty())
 | 
						|
        {
 | 
						|
            MXS_INFO("Found %lu anonymous PAM user(s). Checking them for proxy grants.",
 | 
						|
                     anon_users_info.size());
 | 
						|
        }
 | 
						|
 | 
						|
        for (const auto& elem : anon_users_info)
 | 
						|
        {
 | 
						|
            string query = "SHOW GRANTS FOR ''@'" + elem.first + "';";
 | 
						|
            // Check that the anon user has a proxy grant.
 | 
						|
            if (mysql_query(conn, query.c_str()))
 | 
						|
            {
 | 
						|
                MXS_ERROR("Failed to query server '%s' for grants of anonymous PAM user ''@'%s': '%s'.",
 | 
						|
                          server->name(), elem.first.c_str(), mysql_error(conn));
 | 
						|
                success = false;
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                if ((res = mysql_store_result(conn)))
 | 
						|
                {
 | 
						|
                    // The user may have multiple proxy grants, but is only added once.
 | 
						|
                    MYSQL_ROW row;
 | 
						|
                    while ((row = mysql_fetch_row(res)))
 | 
						|
                    {
 | 
						|
                        if (row[0] && strncmp(row[0], GRANT_PROXY, sizeof(GRANT_PROXY) - 1) == 0)
 | 
						|
                        {
 | 
						|
                            add_pam_user("", elem.first.c_str(),    // user, host
 | 
						|
                                         NULL, false,               // Unused
 | 
						|
                                         elem.second.c_str(), true);// service, proxy
 | 
						|
                            break;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    mysql_free_result(res);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return success;
 | 
						|
}
 |