MXS-2292 PAM authenticator detects anonymous users with defined hosts

This allows anonymous user mapping from well-defined hosts.
This commit is contained in:
Esa Korhonen
2019-02-13 14:44:00 +02:00
parent 49c6244245
commit 48a6ab503e
6 changed files with 124 additions and 79 deletions

View File

@ -26,7 +26,8 @@ const string FIELD_HOST = "host";
const string FIELD_DB = "db"; const string FIELD_DB = "db";
const string FIELD_ANYDB = "anydb"; const string FIELD_ANYDB = "anydb";
const string FIELD_AUTHSTR = "authentication_string"; const string FIELD_AUTHSTR = "authentication_string";
const int NUM_FIELDS = 5; const string FIELD_PROXY = "proxy_grant";
const int NUM_FIELDS = 6;
/** /**
* Initialize PAM authenticator * Initialize PAM authenticator

View File

@ -32,4 +32,5 @@ extern const string FIELD_HOST;
extern const string FIELD_DB; extern const string FIELD_DB;
extern const string FIELD_ANYDB; extern const string FIELD_ANYDB;
extern const string FIELD_AUTHSTR; extern const string FIELD_AUTHSTR;
extern const string FIELD_PROXY;
extern const int NUM_FIELDS; extern const int NUM_FIELDS;

View File

@ -257,37 +257,62 @@ PamClientSession* PamClientSession::create(const PamInstance& inst)
* @param session MySQL session * @param session MySQL session
* @param services_out Output for services * @param services_out Output for services
*/ */
void PamClientSession::get_pam_user_services(const DCB* dcb, void PamClientSession::get_pam_user_services(const DCB* dcb, const MYSQL_session* session,
const MYSQL_session* session,
StringVector* services_out) StringVector* services_out)
{ {
string services_query = string("SELECT authentication_string FROM ") + m_instance.m_tablename string services_query = string("SELECT authentication_string FROM ") + m_instance.m_tablename + " WHERE "
+ " WHERE " + FIELD_USER + " = '" + session->user + "' AND '" + dcb->remote + FIELD_USER + " = '" + session->user + "'"
+ "' LIKE " + FIELD_HOST + " AND (" + FIELD_ANYDB + " = '1' OR '" + session->db + " AND '" + dcb->remote + "' LIKE " + FIELD_HOST
+ "' = '' OR '" + session->db + "' LIKE " + FIELD_DB + " AND (" + FIELD_ANYDB + " = '1' OR '" + session->db + "' = '' OR '"
+ ") ORDER BY authentication_string;"; + session->db + "' LIKE " + FIELD_DB + ")"
+ " AND " + FIELD_PROXY + " = '0' ORDER BY authentication_string;";
MXS_DEBUG("PAM services search sql: '%s'.", services_query.c_str()); MXS_DEBUG("PAM services search sql: '%s'.", services_query.c_str());
char* err; char* err;
if (sqlite3_exec(m_dbhandle, services_query.c_str(), user_services_cb, services_out, &err) != SQLITE_OK) 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); MXS_ERROR("Failed to execute query: '%s'", err);
sqlite3_free(err); sqlite3_free(err);
} }
MXS_DEBUG("User '%s' matched %lu rows in %s db.",
session->user,
services_out->size(),
m_instance.m_tablename.c_str());
if (services_out->empty()) auto word_entry = [](size_t num) -> const char* {
return (num == 1) ? "entry" : "entries";
};
if (!services_out->empty())
{ {
// No service found for user with correct username & password. Check if anonymous user exists. auto num_services = services_out->size();
MXS_INFO("Found %lu valid PAM user %s for '%s'@'%s'.",
num_services, word_entry(num_services), session->user, dcb->remote);
}
else
{
// No service found for user with correct username & host.
// Check if a matching anonymous user exists.
const string anon_query = string("SELECT authentication_string FROM ") + m_instance.m_tablename const string anon_query = string("SELECT authentication_string FROM ") + m_instance.m_tablename
+ " WHERE " + FIELD_USER + " = '' AND " + FIELD_HOST + " = '%';"; + " WHERE " + FIELD_USER + " = ''"
+ " AND '" + dcb->remote + "' LIKE " + FIELD_HOST +
+ " AND " + FIELD_PROXY + " = '1' ORDER BY authentication_string;";
MXS_DEBUG("PAM proxy user services search sql: '%s'.", anon_query.c_str());
if (sqlite3_exec(m_dbhandle, anon_query.c_str(), user_services_cb, services_out, &err) != SQLITE_OK) if (sqlite3_exec(m_dbhandle, anon_query.c_str(), user_services_cb, services_out, &err) != SQLITE_OK)
{ {
MXS_ERROR("Failed to execute query: '%s'", err); MXS_ERROR("Failed to execute query: '%s'", err);
sqlite3_free(err); sqlite3_free(err);
} }
else
{
auto num_services = services_out->size();
if (num_services == 0)
{
MXS_INFO("Found no PAM user entries for '%s'@'%s'.", session->user, dcb->remote);
}
else
{
MXS_INFO("Found %lu matching anonymous PAM user %s for '%s'@'%s'.",
num_services, word_entry(num_services), session->user, dcb->remote);
}
}
} }
} }

View File

@ -33,9 +33,7 @@ public:
bool extract(DCB* dcb, GWBUF* read_buffer); bool extract(DCB* dcb, GWBUF* read_buffer);
private: private:
PamClientSession(sqlite3* dbhandle, const PamInstance& instance); PamClientSession(sqlite3* dbhandle, const PamInstance& instance);
void get_pam_user_services(const DCB* dcb, void get_pam_user_services(const DCB* dcb, const MYSQL_session* session, StringVector* services_out);
const MYSQL_session* session,
StringVector* services_out);
maxscale::Buffer create_auth_change_packet() const; maxscale::Buffer create_auth_change_packet() const;
pam_auth_state m_state; /**< Authentication state*/ pam_auth_state m_state; /**< Authentication state*/

View File

@ -36,16 +36,23 @@ PamInstance* PamInstance::create(char** options)
const string pam_db_name = DEFAULT_PAM_DATABASE_NAME; const string pam_db_name = DEFAULT_PAM_DATABASE_NAME;
/** The table name where we store the users */ /** The table name where we store the users */
const string pam_table_name = DEFAULT_PAM_TABLE_NAME; const string pam_table_name = DEFAULT_PAM_TABLE_NAME;
/** 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 */ /** CREATE TABLE statement for the in-memory table */
const string create_sql = string("CREATE TABLE IF NOT EXISTS ") + pam_table_name const string create_sql = string("CREATE TABLE ") + pam_table_name
+ " (" + FIELD_USER + " varchar(255), " + FIELD_HOST + " varchar(255), " + " (" + FIELD_USER + " varchar(255), "
+ FIELD_DB + " varchar(255), " + FIELD_ANYDB + " boolean, " + FIELD_HOST + " varchar(255), "
+ FIELD_AUTHSTR + " text);"; + FIELD_DB + " varchar(255), "
+ FIELD_ANYDB + " boolean, "
+ FIELD_AUTHSTR + " text, "
+ FIELD_PROXY + " boolean);";
if (sqlite3_threadsafe() == 0) if (sqlite3_threadsafe() == 0)
{ {
MXS_WARNING("SQLite3 was compiled with thread safety off. May cause " MXS_WARNING("SQLite3 was compiled with thread safety off. May cause "
"corruption of in-memory database."); "corruption of in-memory database.");
} }
bool error = false; bool error = false;
/* This handle may be used from multiple threads, set full mutex. */ /* This handle may be used from multiple threads, set full mutex. */
sqlite3* dbhandle = NULL; sqlite3* dbhandle = NULL;
@ -58,9 +65,15 @@ PamInstance* PamInstance::create(char** options)
} }
char* err; char* err;
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) if (!error && sqlite3_exec(dbhandle, create_sql.c_str(), NULL, NULL, &err) != SQLITE_OK)
{ {
MXS_ERROR("Failed to create database: '%s'", err); MXS_ERROR("Failed to create table: '%s'", err);
sqlite3_free(err); sqlite3_free(err);
error = true; error = true;
} }
@ -96,12 +109,10 @@ PamInstance::PamInstance(sqlite3* dbhandle, const string& dbname, const string&
* @param db Database * @param db Database
* @param anydb Global access to databases * @param anydb Global access to databases
* @param pam_service The PAM service used * @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, void PamInstance::add_pam_user(const char* user, const char* host, const char* db, bool anydb,
const char* host, const char* pam_service, bool proxy)
const char* db,
bool anydb,
const char* pam_service)
{ {
/** /**
* The insert query template which adds users to the pam_users table. * The insert query template which adds users to the pam_users table.
@ -110,7 +121,7 @@ void PamInstance::add_pam_user(const char* user,
* no quotes around them. The quotes for strings are added in this function. * no quotes around them. The quotes for strings are added in this function.
*/ */
const string insert_sql_template = const string insert_sql_template =
"INSERT INTO " + m_tablename + " VALUES ('%s', '%s', %s, '%s', %s)"; "INSERT INTO " + m_tablename + " VALUES ('%s', '%s', %s, '%s', %s, '%s')";
/** Used for NULL value creation in the INSERT query */ /** Used for NULL value creation in the INSERT query */
const char NULL_TOKEN[] = "NULL"; const char NULL_TOKEN[] = "NULL";
@ -141,11 +152,10 @@ void PamInstance::add_pam_user(const char* user,
char insert_sql[len + 1]; char insert_sql[len + 1];
sprintf(insert_sql, sprintf(insert_sql,
insert_sql_template.c_str(), insert_sql_template.c_str(),
user, user, host,
host, db_str.c_str(), anydb ? "1" : "0",
db_str.c_str(), service_str.c_str(),
anydb ? "1" : "0", proxy ? "1" : "0");
service_str.c_str());
char* err; char* err;
if (sqlite3_exec(m_dbhandle, insert_sql, NULL, NULL, &err) != SQLITE_OK) if (sqlite3_exec(m_dbhandle, insert_sql, NULL, NULL, &err) != SQLITE_OK)
@ -153,6 +163,18 @@ void PamInstance::add_pam_user(const char* user,
MXS_ERROR("Failed to insert user: %s", err); MXS_ERROR("Failed to insert user: %s", err);
sqlite3_free(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());
}
}
} }
/** /**
@ -209,8 +231,7 @@ int PamInstance::load_users(SERVICE* service)
if (mysql_query(mysql, PAM_USERS_QUERY)) if (mysql_query(mysql, PAM_USERS_QUERY))
{ {
MXS_ERROR("Failed to query server '%s' for PAM users: '%s'.", MXS_ERROR("Failed to query server '%s' for PAM users: '%s'.",
servers->server->name, servers->server->name, mysql_error(mysql));
mysql_error(mysql));
} }
else else
{ {
@ -219,23 +240,20 @@ int PamInstance::load_users(SERVICE* service)
if (res) if (res)
{ {
mxb_assert(mysql_num_fields(res) == PAM_USERS_QUERY_NUM_FIELDS); mxb_assert(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; MYSQL_ROW row;
while ((row = mysql_fetch_row(res))) while ((row = mysql_fetch_row(res)))
{ {
add_pam_user(row[0], add_pam_user(row[0], row[1], // user, host
row[1], row[2], row[3] && strcasecmp(row[3], "Y") == 0, // db, anydb
row[2], row[4], // pam service
row[3] && strcasecmp(row[3], "Y") == 0, false); // not a proxy
row[4]);
} }
mysql_free_result(res); mysql_free_result(res);
if (query_anon_proxy_user(servers->server, mysql)) }
{
rval = MXS_AUTH_LOADUSERS_OK; if (fetch_anon_proxy_users(servers->server, mysql))
} {
rval = MXS_AUTH_LOADUSERS_OK;
} }
} }
mysql_close(mysql); mysql_close(mysql);
@ -306,63 +324,68 @@ json_t* PamInstance::diagnostic_json()
return rval; return rval;
} }
bool PamInstance::query_anon_proxy_user(SERVER* server, MYSQL* conn) bool PamInstance::fetch_anon_proxy_users(SERVER* server, MYSQL* conn)
{ {
bool success = true; bool success = true;
bool anon_user_found = false; const char ANON_USER_QUERY[] = "SELECT host,authentication_string FROM mysql.user WHERE "
string anon_pam_service; "(plugin = 'pam' AND user = '');";
const char ANON_USER_QUERY[] = "SELECT authentication_string FROM mysql.user WHERE "
"(plugin = 'pam' AND user = '' AND host = '%');";
const char ANON_GRANT_QUERY[] = "SHOW GRANTS FOR ''@'%';";
const char GRANT_PROXY[] = "GRANT PROXY ON"; const char GRANT_PROXY[] = "GRANT PROXY ON";
// Query for the anonymous user which is used with group mappings // Query for the anonymous user which is used with group mappings
if (mysql_query(conn, ANON_USER_QUERY)) if (mysql_query(conn, ANON_USER_QUERY))
{ {
MXS_ERROR("Failed to query server '%s' for the anonymous PAM user: '%s'.", MXS_ERROR("Failed to query server '%s' for anonymous PAM users: '%s'.",
server->name, server->name, mysql_error(conn));
mysql_error(conn));
success = false; success = false;
} }
else 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); MYSQL_RES* res = mysql_store_result(conn);
if (res) if (res)
{ {
MYSQL_ROW row = mysql_fetch_row(res); MYSQL_ROW row;
if (row) while ((row = mysql_fetch_row(res)))
{ {
anon_user_found = true; string host = row[0] ? row[0] : "";
if (row[0]) string auth_str = row[1] ? row[1] : "";
{ anon_users_info.push_back(std::make_pair(host, auth_str));
anon_pam_service = row[0];
}
} }
mysql_free_result(res); mysql_free_result(res);
} }
if (anon_user_found) if (!anon_users_info.empty())
{ {
// Check that the anon user has a proxy grant MXS_INFO("Found %lu anonymous PAM user(s). Checking them for proxy grants.",
if (mysql_query(conn, ANON_GRANT_QUERY)) 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 the grants of the anonymous PAM user: '%s'.", MXS_ERROR("Failed to query server '%s' for grants of anonymous PAM user ''@'%s': '%s'.",
server->name, server->name, elem.first.c_str(), mysql_error(conn));
mysql_error(conn));
success = false; success = false;
} }
else else
{ {
if ((res = mysql_store_result(conn))) if ((res = mysql_store_result(conn)))
{ {
// The user may have multiple proxy grants, but is only added once.
MYSQL_ROW row; MYSQL_ROW row;
while ((row = mysql_fetch_row(res))) while ((row = mysql_fetch_row(res)))
{ {
if (row[0] && strncmp(row[0], GRANT_PROXY, sizeof(GRANT_PROXY) - 1) == 0) if (row[0] && strncmp(row[0], GRANT_PROXY, sizeof(GRANT_PROXY) - 1) == 0)
{ {
MXS_NOTICE("Anonymous PAM user with proxy grant found. User account mapping " add_pam_user("", elem.first.c_str(), // user, host
"enabled."); NULL, false, // Unused
add_pam_user("", "%", NULL, false, anon_pam_service.c_str()); elem.second.c_str(), true); // service, proxy
break;
} }
} }
mysql_free_result(res); mysql_free_result(res);

View File

@ -33,13 +33,10 @@ public:
const std::string m_tablename; /**< The table where users are stored */ const std::string m_tablename; /**< The table where users are stored */
private: private:
PamInstance(sqlite3* dbhandle, const std::string& m_dbname, const std::string& tablename); PamInstance(sqlite3* dbhandle, const std::string& m_dbname, const std::string& tablename);
void add_pam_user(const char* user, void add_pam_user(const char* user, const char* host, const char* db, bool anydb,
const char* host, const char* pam_service, bool proxy);
const char* db,
bool anydb,
const char* pam_service);
void delete_old_users(); void delete_old_users();
bool query_anon_proxy_user(SERVER* server, MYSQL* conn); bool fetch_anon_proxy_users(SERVER* server, MYSQL* conn);
sqlite3* const m_dbhandle; /**< SQLite3 database handle */ sqlite3* const m_dbhandle; /**< SQLite3 database handle */
}; };