На Хабре уже писалось, что в MySQL появилась возможность подменять встроенную процедуру аутентификации, загрузив соответствующий плагин. В таком плагине можно реализовать совершенно произвольную политику аутентификации пользователей, полностью уходя от традиционной в MySQL схемы username/password в таблице mysql.user.
А недавно Оракл выпустил PAM authentication plugin. При использовании которого сервер не ищет пароли в mysql.user, а перекладывает задачу аутентификации на PAM, подсистему специально разработанную для решения задач аутентификации в различных приложениях и контекстах, с гибко настраиваемыми правилами и на лету подключаемыми модулями.
К сожалению, у этого плагина есть несколько недостатков. Во-первых, он распространяется только с коммерческой версией MySQL и его исходники закрыты. Во-вторых, он не поддерживает коммуникацию между пользователем и pam-модулем, и единственно возможной остается аутентификация по паролю.Что, как-бы, убивает всю идею.
«А почему-бы...» — подумал я. «Я напишу свой pam-плагин, с блэкджеком и шлюхами!»
Мне привычнее работать с MySQL-скими исходниками, так что качаю 5.5 и распаковываю. Хотя для такого плагина это и не обязательно — достаточно только пакета mysql-devel.
Теперь готовлю себе песочницу:
Как истинные джедаи мы ничего с нуля не пишем — так что я взял auth_socket.c и вначале убрал все лишнее. Получилось где-то так:
В самом низу — дескриптор плагина, у него структура для всех плагинов одинаковая. Чуть выше — дескриптор плагина аутентификации, а еще выше — пока пустая функция, из которой я и буду вызывать pam.
Поскольку основная идея этого pam-плагина — вести диалог с пользователем, надо как-то научить клиента получать с сервера вопросоы, и отсылать ответы, введенные пользователем. Для этого в MySQL есть клиентские плагины — плагины которые подгружаются в клиента (точнее, их грузит libmysqlclient, по указаниям сервера). Я от клиентского плагина не требую никакой экзотики — просто повторять вопрос/ответ до полного удовлетворения сервера. Такой плагин уже есть — называется «dialog» и находится, как ни странно, в файле dialog.c.
Этот плагин нужно указать во втором поле структуры st_mysql_auth, тогда сервер сообщит клиенту, что тому нужно загрузить dialog.so и направлять ему все, что моему плагину захочется послать.
Проверка. Создаю CMakeLists.txt (если честно — копирую из другого плагина и слегка подправляю), там всего одна строчка:
и компилирую
Работает, теперь пора курить man pam. В MySQL аутентификация сделана довольно просто. Плагин получает имя пользователя, которого надо аутентифицировать и vio хэндлер. У vio есть методы write_packet и read_packet, с помощью которых можно общаться с клиентом (в данном случае — с плагином «dialog»). В pam все немного сложнее, нужно использовать callback функцию, из которой я и буду вызывать write_packet и read_packet. В целом, работа с pam выглядит так:
При любой ошибке на любом этапе надо сразу переходить к последнему шагу — pam_end. У меня получилась такая вот функция:
Осталось написать conversation function — ту фукцию, которую pam будет вызывать, когда ему захочется что-то спросить. В эту функцию pam передаст список вопросов, а она ему выдаст список ответов. Кроме того в нее передается — как это всегда бывает с callback-ами — указатель для хранения дополнительных параметров и состояния. Поскольку указатель один а параметров много — создаю структуру:
Проблема в том, что «dialog» плагин понимает только команды в виде «выведи вот этот текст в качестве подсказки, прочти введенную пользовательем строку, и пошли ее серверу». А у pam-а есть целых четыре типа сообщений, два из которых чисто информационные и имеют семантику «выведи-ка вот это, вводить ничего не надо». Поэтому в моем плагине я их накапливаю в буфере не отсылая, пока не понадобится что-то ввести. Получается вот так:
Вот, собственно, и все. Собираю, инсталлирую — и оно не работает. Оказывается, что из-за бага 60745 клиенты не могут загрузить плагин «dialog». Ну что ж, решение очевидно
и можно аутентифицироваться в MySQL, например, помощью S/Key:
А недавно Оракл выпустил PAM authentication plugin. При использовании которого сервер не ищет пароли в mysql.user, а перекладывает задачу аутентификации на PAM, подсистему специально разработанную для решения задач аутентификации в различных приложениях и контекстах, с гибко настраиваемыми правилами и на лету подключаемыми модулями.
К сожалению, у этого плагина есть несколько недостатков. Во-первых, он распространяется только с коммерческой версией MySQL и его исходники закрыты. Во-вторых, он не поддерживает коммуникацию между пользователем и pam-модулем, и единственно возможной остается аутентификация по паролю.
«А почему-бы...» — подумал я. «Я напишу свой pam-плагин, с блэкджеком и шлюхами!»
Мне привычнее работать с MySQL-скими исходниками, так что качаю 5.5 и распаковываю. Хотя для такого плагина это и не обязательно — достаточно только пакета mysql-devel.
Теперь готовлю себе песочницу:
mysql-5.5.17 $ mkdir plugin/pam_auth mysql-5.5.17 $ cd plugin/pam_auth
Как истинные джедаи мы ничего с нуля не пишем — так что я взял auth_socket.c и вначале убрал все лишнее. Получилось где-то так:
#include <mysql/plugin_auth.h>
static int pam_auth(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info)
{
}
static struct st_mysql_auth pam_auth_handler =
{
MYSQL_AUTHENTICATION_INTERFACE_VERSION, /* auth API version */
"dialog", /* client plugin name */
pam_auth /* main auth function */
};
mysql_declare_plugin(pam_auth)
{
MYSQL_AUTHENTICATION_PLUGIN, /* plugin type */
&pam_auth_handler, /* auth plugin handler */
"pam_auth", /* plugin name */
"Sergei Golubchik", /* author */
"PAM based authentication", /* description */
PLUGIN_LICENSE_GPL, /* license */
NULL, /* init function */
NULL, /* deinit function */
0x0100, /* version 1.0 */
NULL, /* for SHOW STATUS */
NULL, /* for SHOW VARIABLES */
NULL, /* unused */
0, /* flags */
}
mysql_declare_plugin_end;
В самом низу — дескриптор плагина, у него структура для всех плагинов одинаковая. Чуть выше — дескриптор плагина аутентификации, а еще выше — пока пустая функция, из которой я и буду вызывать pam.
Поскольку основная идея этого pam-плагина — вести диалог с пользователем, надо как-то научить клиента получать с сервера вопросоы, и отсылать ответы, введенные пользователем. Для этого в MySQL есть клиентские плагины — плагины которые подгружаются в клиента (точнее, их грузит libmysqlclient, по указаниям сервера). Я от клиентского плагина не требую никакой экзотики — просто повторять вопрос/ответ до полного удовлетворения сервера. Такой плагин уже есть — называется «dialog» и находится, как ни странно, в файле dialog.c.
Этот плагин нужно указать во втором поле структуры st_mysql_auth, тогда сервер сообщит клиенту, что тому нужно загрузить dialog.so и направлять ему все, что моему плагину захочется послать.
Проверка. Создаю CMakeLists.txt (если честно — копирую из другого плагина и слегка подправляю), там всего одна строчка:
MYSQL_ADD_PLUGIN(pam_auth pam_auth.c LINK_LIBRARIES pam)
и компилирую
mysql-5.5.17 $ cmake . && make
Работает, теперь пора курить man pam. В MySQL аутентификация сделана довольно просто. Плагин получает имя пользователя, которого надо аутентифицировать и vio хэндлер. У vio есть методы write_packet и read_packet, с помощью которых можно общаться с клиентом (в данном случае — с плагином «dialog»). В pam все немного сложнее, нужно использовать callback функцию, из которой я и буду вызывать write_packet и read_packet. В целом, работа с pam выглядит так:
- инициализация — pam_start (тут мы говорим, какую функцию вызывать как callback)
- аутентификация — pam_authentificate (где-то внутри и может вызываться наш callback)
- проверка учетной записи pam_acct_mgmt
- проверка нового имени пользователя (если pam его поменял) — pam_get_item(PAM_USER)
- завершение pam_end
При любой ошибке на любом этапе надо сразу переходить к последнему шагу — pam_end. У меня получилась такая вот функция:
#include <string.h>
#include <security/pam_modules.h>
#include <security/pam_appl.h>
static int conv(int n, const struct pam_message **msg,
struct pam_response **resp, void *data)
{
}
#define DO_PAM(X) \
do { \
status = (X); \
if (status != PAM_SUCCESS) \
goto ret; \
} while(0)
static int pam_auth(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info)
{
pam_handle_t *pamh = NULL;
int status;
const char *new_username;
struct param param;
struct pam_conv c = { &conv, ¶m };
/* get the service name, as specified in
CREATE USER ... IDENTIFIED WITH pam_auth AS "service"
*/
const char *service = info->auth_string ? info->auth_string : "mysql";
param.ptr = param.buf + 1;
param.vio = vio;
DO_PAM(pam_start(service, info->user_name, &c, &pamh));
DO_PAM(pam_authenticate (pamh, 0));
DO_PAM(pam_acct_mgmt(pamh, 0));
DO_PAM(pam_get_item(pamh, PAM_USER, (const void**)&new_username));
if (new_username)
strncpy(info->authenticated_as, new_username, sizeof(info->authenticated_as));
ret:
pam_end(pamh, status);
return status == PAM_SUCCESS ? CR_OK : CR_ERROR;
}
Осталось написать conversation function — ту фукцию, которую pam будет вызывать, когда ему захочется что-то спросить. В эту функцию pam передаст список вопросов, а она ему выдаст список ответов. Кроме того в нее передается — как это всегда бывает с callback-ами — указатель для хранения дополнительных параметров и состояния. Поскольку указатель один а параметров много — создаю структуру:
struct param {
unsigned char buf[10240], *ptr;
MYSQL_PLUGIN_VIO *vio;
};
Проблема в том, что «dialog» плагин понимает только команды в виде «выведи вот этот текст в качестве подсказки, прочти введенную пользовательем строку, и пошли ее серверу». А у pam-а есть целых четыре типа сообщений, два из которых чисто информационные и имеют семантику «выведи-ка вот это, вводить ничего не надо». Поэтому в моем плагине я их накапливаю в буфере не отсылая, пока не понадобится что-то ввести. Получается вот так:
static int conv(int n, const struct pam_message **msg,
struct pam_response **resp, void *data)
{
struct param *param = (struct param *)data;
unsigned char *end = param->buf + sizeof(param->buf) - 1;
int i;
for (i= 0; i < n; i++) {
/* if there's a message - append it to the buffer */
if (msg[i]->msg) {
int len = strlen(msg[i]->msg);
if (len > end - param->ptr)
len = end - param->ptr;
memcpy(param->ptr, msg[i]->msg, len);
param->ptr+= len;
*(param->ptr)++ = '\n';
}
/* if the message style is *_PROMPT_*, meaning PAM asks a question,
send the accumulated text to the client, read the reply */
if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF ||
msg[i]->msg_style == PAM_PROMPT_ECHO_ON) {
int pkt_len;
unsigned char *pkt;
/* allocate the response array.
freeing it is the responsibility of the caller */
if (*resp == 0) {
*resp = calloc(sizeof(struct pam_response), n);
if (*resp == 0)
return PAM_BUF_ERR;
}
/* dialog plugin interprets the first byte of the packet
as the magic number.
2 means "read the input with the echo enabled"
4 means "password-like input, echo disabled"
C'est la vie. */
param->buf[0] = msg[i]->msg_style == PAM_PROMPT_ECHO_ON ? 2 : 4;
if (param->vio->write_packet(param->vio, param->buf, param->ptr - param->buf - 1))
return PAM_CONV_ERR;
pkt_len = param->vio->read_packet(param->vio, &pkt);
if (pkt_len < 0)
return PAM_CONV_ERR;
/* allocate and copy the reply to the response array */
(*resp)[i].resp= strndup((char*)pkt, pkt_len);
param->ptr = param->buf + 1;
}
}
return PAM_SUCCESS;
}
Вот, собственно, и все. Собираю, инсталлирую — и оно не работает. Оказывается, что из-за бага 60745 клиенты не могут загрузить плагин «dialog». Ну что ж, решение очевидно
mv auth.so dialog.so
и можно аутентифицироваться в MySQL, например, помощью S/Key:
$ mysql challenge otp-md5 99 th91334 password: (turning echo on) pasword: OMEN US HORN OMIT BACK AHOY mysql>