Merge branch '2.3' into develop
This commit is contained in:
commit
bed28db3fd
@ -40,9 +40,25 @@ public:
|
||||
* @param password Password
|
||||
* @param service Which PAM service is the user logging to
|
||||
* @param expected_msg The first expected message from the PAM authentication system.
|
||||
* Typically "Password: ", which is also the default value. If set to empty, the message is not checked.
|
||||
* Typically "Password: ". If set to empty, the message is not checked.
|
||||
* @return A result struct with the result and an error message.
|
||||
*/
|
||||
PamResult pam_authenticate(const std::string& user, const std::string& password,
|
||||
const std::string& service, const std::string& expected_msg = "Password: ");
|
||||
|
||||
/**
|
||||
* Check if the user & password can log into the given PAM service. This function will block until the
|
||||
* operation completes.
|
||||
*
|
||||
* @param user Username
|
||||
* @param password Password
|
||||
* @param client_remote Client address, used for logging
|
||||
* @param service Which PAM service is the user logging to
|
||||
* @param expected_msg The first expected message from the PAM authentication system.
|
||||
* Typically "Password: ". If set to empty, the message is not checked.
|
||||
* @return A result struct with the result and an error message.
|
||||
*/
|
||||
PamResult
|
||||
pam_authenticate(const std::string& user, const std::string& password, const std::string& client_remote,
|
||||
const std::string& service, const std::string& expected_msg);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
#include <security/pam_appl.h>
|
||||
#include <maxbase/alloc.h>
|
||||
#include <maxbase/assert.h>
|
||||
#include <maxbase/log.hh>
|
||||
#include <maxbase/format.hh>
|
||||
|
||||
@ -23,21 +24,21 @@ using std::string;
|
||||
namespace
|
||||
{
|
||||
|
||||
const char GENERAL_ERRMSG[] = "Only simple password-based PAM authentication with one call "
|
||||
"to the conversation function is supported.";
|
||||
|
||||
/** Used by the PAM conversation function */
|
||||
class ConversationData
|
||||
{
|
||||
public:
|
||||
int m_counter {0};
|
||||
string m_client;
|
||||
string m_password;
|
||||
int m_counter {0};
|
||||
string m_client_remote; // Client address
|
||||
string m_expected_msg;
|
||||
|
||||
ConversationData(const string& client, const string& password, const string& expected_msg)
|
||||
ConversationData(const string& client, const string& password, const string& client_remote,
|
||||
const string& expected_msg)
|
||||
: m_client(client)
|
||||
, m_password(password)
|
||||
, m_client_remote(client_remote)
|
||||
, m_expected_msg(expected_msg)
|
||||
{
|
||||
}
|
||||
@ -50,66 +51,91 @@ public:
|
||||
* 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,
|
||||
int conversation_func(int num_msg, const struct pam_message** messages, struct pam_response** responses_out,
|
||||
void* appdata_ptr)
|
||||
{
|
||||
MXB_DEBUG("Entering PAM conversation function.");
|
||||
int rval = PAM_CONV_ERR;
|
||||
ConversationData* data = static_cast<ConversationData*>(appdata_ptr);
|
||||
data->m_counter++;
|
||||
if (data->m_counter > 1)
|
||||
|
||||
// The responses are saved as an array of structures. This is unlike the input messages, which is an
|
||||
// array of pointers to struct. Each message should have an answer, even if empty.
|
||||
auto responses = static_cast<pam_response*>(MXS_CALLOC(num_msg, sizeof(pam_response)));
|
||||
if (!responses)
|
||||
{
|
||||
MXB_ERROR("Multiple calls to conversation function for client '%s'. %s",
|
||||
data->m_client.c_str(), GENERAL_ERRMSG);
|
||||
return PAM_BUF_ERR;
|
||||
}
|
||||
else if (num_msg != 1)
|
||||
|
||||
bool conv_error = false;
|
||||
for (int i = 0; i < num_msg; i++)
|
||||
{
|
||||
MXB_ERROR("Conversation function received '%d' messages from API. Only singular messages are "
|
||||
"supported.", num_msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
pam_message first = *msg[0];
|
||||
// Check that the first message from the PAM system is as expected.
|
||||
if (first.msg_style != PAM_PROMPT_ECHO_OFF && first.msg_style != PAM_PROMPT_ECHO_ON)
|
||||
const pam_message* message = messages[i]; // This may crash on Solaris, see PAM documentation.
|
||||
pam_response* response = &responses[i];
|
||||
int msg_type = message->msg_style;
|
||||
// In an ideal world, these messages would be sent to the client instead of the log. The problem
|
||||
// is that the messages should be sent with the AuthSwitchRequest-packet, requiring the blocking
|
||||
// PAM api to work with worker-threads. Not worth the trouble unless really required.
|
||||
if (msg_type == PAM_ERROR_MSG)
|
||||
{
|
||||
MXB_ERROR("Unexpected PAM message type '%i' when '%i' or '%i' was expected.",
|
||||
first.msg_style, PAM_PROMPT_ECHO_OFF, PAM_PROMPT_ECHO_ON);
|
||||
MXB_WARNING("Error message from PAM api: %s", message->msg);
|
||||
}
|
||||
else if (!data->m_expected_msg.empty() && data->m_expected_msg != first.msg)
|
||||
else if (msg_type == PAM_TEXT_INFO)
|
||||
{
|
||||
MXB_ERROR("Unexpected PAM message contents '%s' when '%s' was expected.",
|
||||
first.msg, data->m_expected_msg.c_str());
|
||||
MXB_NOTICE("Message from PAM api: '%s'", message->msg);
|
||||
}
|
||||
else if (msg_type == PAM_PROMPT_ECHO_ON || msg_type == PAM_PROMPT_ECHO_OFF)
|
||||
{
|
||||
auto exp = data->m_expected_msg;
|
||||
// PAM system is asking for something. We only know how to answer the expected question,
|
||||
// anything else is an error.
|
||||
if (data->m_expected_msg.empty() || message->msg == data->m_expected_msg)
|
||||
{
|
||||
response->resp = MXS_STRDUP(data->m_password.c_str());
|
||||
// retcode should be already 0.
|
||||
}
|
||||
else
|
||||
{
|
||||
MXB_ERROR("Unexpected prompt from PAM api: '%s'. Only '%s' is allowed.",
|
||||
message->msg, data->m_expected_msg.c_str());
|
||||
conv_error = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pam_response* response = static_cast<pam_response*>(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;
|
||||
}
|
||||
// Faulty PAM system or perhaps different api version.
|
||||
MXB_ERROR("Unknown PAM message type '%i'.", msg_type);
|
||||
conv_error = true;
|
||||
mxb_assert(!true);
|
||||
|
||||
}
|
||||
}
|
||||
return rval;
|
||||
|
||||
data->m_counter++;
|
||||
if (conv_error)
|
||||
{
|
||||
// On error, the response output should not be set.
|
||||
MXS_FREE(responses);
|
||||
return PAM_CONV_ERR;
|
||||
}
|
||||
else
|
||||
{
|
||||
*responses_out = responses;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace maxbase
|
||||
{
|
||||
|
||||
PamResult pam_authenticate(const string& user, const string& password, const string& service,
|
||||
const string& expected_msg)
|
||||
PamResult
|
||||
pam_authenticate(const std::string& user, const std::string& password, const std::string& client_remote,
|
||||
const std::string& service, const std::string& expected_msg)
|
||||
{
|
||||
const char PAM_START_ERR_MSG[] = "Failed to start PAM authentication of user '%s': '%s'.";
|
||||
const char PAM_AUTH_ERR_MSG[] = "PAM authentication of user '%s' to service '%s' failed: '%s'.";
|
||||
const char PAM_ACC_ERR_MSG[] = "PAM account check of user '%s' to service '%s' failed: '%s'.";
|
||||
|
||||
ConversationData appdata(user, password, expected_msg);
|
||||
ConversationData appdata(user, password, client_remote, expected_msg);
|
||||
pam_conv conv_struct = {conversation_func, &appdata};
|
||||
|
||||
PamResult result;
|
||||
@ -170,4 +196,10 @@ PamResult pam_authenticate(const string& user, const string& password, const str
|
||||
pam_end(pam_handle, pam_status);
|
||||
return result;
|
||||
}
|
||||
|
||||
PamResult pam_authenticate(const string& user, const string& password, const string& service,
|
||||
const string& expected_msg)
|
||||
{
|
||||
return pam_authenticate(user, password, "", service, expected_msg);
|
||||
}
|
||||
}
|
||||
|
@ -77,9 +77,7 @@ int user_services_cb(void* data, int columns, char** column_vals, char** column_
|
||||
}
|
||||
|
||||
PamClientSession::PamClientSession(sqlite3* dbhandle, const PamInstance& instance)
|
||||
: m_state(PAM_AUTH_INIT)
|
||||
, m_sequence(0)
|
||||
, m_dbhandle(dbhandle)
|
||||
: m_dbhandle(dbhandle)
|
||||
, m_instance(instance)
|
||||
{
|
||||
}
|
||||
@ -206,7 +204,7 @@ Buffer PamClientSession::create_auth_change_packet() const
|
||||
gw_mysql_set_byte3(pData, plen);
|
||||
pData += 3;
|
||||
*pData++ = m_sequence;
|
||||
*pData++ = 0xfe; // AuthSwitchRequest command
|
||||
*pData++ = MYSQL_REPLY_AUTHSWITCHREQUEST;
|
||||
memcpy(pData, DIALOG.c_str(), DIALOG_SIZE); // Plugin name
|
||||
pData += DIALOG_SIZE;
|
||||
*pData++ = DIALOG_ECHO_DISABLED;
|
||||
@ -223,7 +221,7 @@ int PamClientSession::authenticate(DCB* dcb)
|
||||
if (*ses->user)
|
||||
{
|
||||
rval = MXS_AUTH_FAILED;
|
||||
if (m_state == PAM_AUTH_INIT)
|
||||
if (m_state == State::INIT)
|
||||
{
|
||||
/** We need to send the authentication switch packet to change the
|
||||
* authentication to something other than the 'mysql_native_password'
|
||||
@ -231,11 +229,11 @@ int PamClientSession::authenticate(DCB* dcb)
|
||||
Buffer authbuf = create_auth_change_packet();
|
||||
if (authbuf.length() && dcb->func.write(dcb, authbuf.release()))
|
||||
{
|
||||
m_state = PAM_AUTH_DATA_SENT;
|
||||
m_state = State::ASKED_FOR_PW;
|
||||
rval = MXS_AUTH_INCOMPLETE;
|
||||
}
|
||||
}
|
||||
else if (m_state == PAM_AUTH_DATA_SENT)
|
||||
else if (m_state == State::PW_RECEIVED)
|
||||
{
|
||||
/** We sent the authentication change packet + plugin name and the client
|
||||
* responded with the password. Try to continue authentication without more
|
||||
@ -275,8 +273,8 @@ int PamClientSession::authenticate(DCB* dcb)
|
||||
service = "mysql";
|
||||
}
|
||||
|
||||
mxb::PamResult res = mxb::pam_authenticate(ses->user, password, service,
|
||||
PASSWORD);
|
||||
mxb::PamResult res = mxb::pam_authenticate(ses->user, password, dcb->remote,
|
||||
service, PASSWORD);
|
||||
if (res.type == mxb::PamResult::Result::SUCCESS)
|
||||
{
|
||||
authenticated = true;
|
||||
@ -294,6 +292,7 @@ int PamClientSession::authenticate(DCB* dcb)
|
||||
{
|
||||
rval = MXS_AUTH_SUCCEEDED;
|
||||
}
|
||||
m_state = State::DONE;
|
||||
}
|
||||
}
|
||||
return rval;
|
||||
@ -307,20 +306,22 @@ bool PamClientSession::extract(DCB* dcb, GWBUF* buffer)
|
||||
|
||||
switch (m_state)
|
||||
{
|
||||
case PAM_AUTH_INIT:
|
||||
// The buffer doesn't have any PAM-specific data yet
|
||||
case State::INIT:
|
||||
// The buffer doesn't have any PAM-specific data yet, as it's the normal HandShakeResponse.
|
||||
rval = true;
|
||||
break;
|
||||
|
||||
case PAM_AUTH_DATA_SENT:
|
||||
case State::ASKED_FOR_PW:
|
||||
// Client should have responses with password.
|
||||
if (store_client_password(dcb, buffer))
|
||||
{
|
||||
m_state = State::PW_RECEIVED;
|
||||
rval = true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
MXS_ERROR("Unexpected authentication state: %d", m_state);
|
||||
MXS_ERROR("Unexpected authentication state: %d", static_cast<int>(m_state));
|
||||
mxb_assert(!true);
|
||||
break;
|
||||
}
|
||||
|
@ -38,8 +38,16 @@ private:
|
||||
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 */
|
||||
enum class State
|
||||
{
|
||||
INIT,
|
||||
ASKED_FOR_PW,
|
||||
PW_RECEIVED,
|
||||
DONE
|
||||
};
|
||||
|
||||
State m_state {State::INIT}; /**< Authentication state*/
|
||||
uint8_t m_sequence {0}; /**< The next packet seqence number */
|
||||
sqlite3* const m_dbhandle; /**< SQLite3 database handle */
|
||||
const PamInstance& m_instance; /**< Authenticator instance */
|
||||
};
|
||||
|
@ -20,8 +20,7 @@
|
||||
|
||||
static void* pam_backend_auth_alloc(void* instance)
|
||||
{
|
||||
PamBackendSession* pses = new(std::nothrow) PamBackendSession();
|
||||
return pses;
|
||||
return new(std::nothrow) PamBackendSession();
|
||||
}
|
||||
|
||||
static void pam_backend_auth_free(void* data)
|
||||
|
@ -16,80 +16,129 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* Check that the AuthSwitchRequest packet is as expected. Inverse of
|
||||
* create_auth_change_packet() in pam_auth.cc.
|
||||
* Parse packet type and plugin name from packet data. Advances pointer.
|
||||
*
|
||||
* @param dcb Backend DCB
|
||||
* @param buffer Buffer containing an AuthSwitchRequest packet
|
||||
* @return True on success, false on error
|
||||
* @param data Data from server. The pointer is advanced.
|
||||
* @param end Pointer to after the end of data
|
||||
* @param server_name Server name for logging
|
||||
* @return True if all expected fields were parsed
|
||||
*/
|
||||
bool check_auth_switch_request(DCB* dcb, GWBUF* buffer)
|
||||
bool parse_authswitchreq(const uint8_t** data, const uint8_t* end, const char* server_name)
|
||||
{
|
||||
/**
|
||||
* 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))
|
||||
const uint8_t* ptr = *data;
|
||||
if (ptr >= end)
|
||||
{
|
||||
/** 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->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)
|
||||
bool success = false;
|
||||
uint8_t cmdbyte = *ptr++;
|
||||
if (cmdbyte == MYSQL_REPLY_AUTHSWITCHREQUEST)
|
||||
{
|
||||
rval = true;
|
||||
// Correct packet type.
|
||||
if (ptr < end)
|
||||
{
|
||||
const char* plugin_name = reinterpret_cast<const char*>(ptr);
|
||||
if (strcmp(plugin_name, DIALOG.c_str()) == 0)
|
||||
{
|
||||
// Correct plugin.
|
||||
ptr += DIALOG_SIZE;
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
MXB_ERROR("'%s' asked for authentication plugin '%s' when '%s' was expected.",
|
||||
server_name, plugin_name, DIALOG.c_str());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MXB_ERROR("Received malformed AuthSwitchRequest-packet from '%s'.", server_name);
|
||||
}
|
||||
}
|
||||
else if (cmdbyte == MYSQL_REPLY_OK)
|
||||
{
|
||||
// Authentication is already done? Maybe the server authenticated us as the anonymous user. This
|
||||
// is quite insecure. */
|
||||
MXB_ERROR("Authentication to '%s' was complete before it even started, anonymous users may "
|
||||
"be enabled.", server_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("AuthSwitchRequest packet contents unexpected. %s", GENERAL_ERRMSG);
|
||||
MXB_ERROR("Expected AuthSwitchRequest-packet from '%s' but received %#x.", server_name, cmdbyte);
|
||||
}
|
||||
return rval;
|
||||
|
||||
if (success)
|
||||
{
|
||||
*data = ptr;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
PamBackendSession::PamBackendSession()
|
||||
: m_state(PAM_AUTH_INIT)
|
||||
, m_sequence(0)
|
||||
|
||||
/**
|
||||
* Parse prompt type and message text from packet data. Advances pointer.
|
||||
*
|
||||
* @param data Data from server. The pointer is advanced.
|
||||
* @param end Pointer to after the end of data
|
||||
* @param server_name Server name for logging
|
||||
* @return True if all expected fields were parsed
|
||||
*/
|
||||
bool parse_password_prompt(const uint8_t** data, const uint8_t* end, const char* server_name)
|
||||
{
|
||||
const uint8_t* ptr = *data;
|
||||
if (end - ptr < 2) // Need at least message type + message
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
int msg_type = *ptr++;
|
||||
if (msg_type == DIALOG_ECHO_ENABLED || msg_type == DIALOG_ECHO_DISABLED)
|
||||
{
|
||||
const char* messages = reinterpret_cast<const char*>(ptr);
|
||||
// The rest of the buffer contains a message.
|
||||
// The server separates messages with linebreaks. Search for the last.
|
||||
const char* linebrk_pos = strrchr(messages, '\n');
|
||||
const char* prompt;
|
||||
if (linebrk_pos)
|
||||
{
|
||||
int msg_len = linebrk_pos - messages;
|
||||
MXS_INFO("PAM plugin of '%s' sent message: '%.*s'", server_name, msg_len, messages);
|
||||
prompt = linebrk_pos + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
prompt = messages; // No additional messages.
|
||||
}
|
||||
|
||||
if (prompt == PASSWORD)
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
MXB_ERROR("'%s' asked for '%s' when '%s was expected.", server_name, prompt, PASSWORD.c_str());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MXB_ERROR("'%s' sent an unknown message type %i.", server_name, msg_type);
|
||||
}
|
||||
|
||||
if (success)
|
||||
{
|
||||
*data = ptr;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PamBackendSession::PamBackendSession()
|
||||
{}
|
||||
|
||||
/**
|
||||
* Send password to server
|
||||
*
|
||||
@ -98,7 +147,6 @@ PamBackendSession::PamBackendSession()
|
||||
*/
|
||||
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];
|
||||
@ -110,55 +158,134 @@ bool PamBackendSession::send_client_password(DCB* dcb)
|
||||
|
||||
bool PamBackendSession::extract(DCB* dcb, GWBUF* buffer)
|
||||
{
|
||||
gwbuf_copy_data(buffer, MYSQL_SEQ_OFFSET, 1, &m_sequence);
|
||||
m_sequence++;
|
||||
bool rval = false;
|
||||
/**
|
||||
* The server PAM plugin sends data usually once, at the moment it gets a prompt-type message
|
||||
* from the api. The "message"-segment may contain multiple messages from the api separated by \n.
|
||||
* MaxScale should ignore this text and search for "Password: " near the end of the message. See
|
||||
* https://github.com/MariaDB/server/blob/10.3/plugin/auth_pam/auth_pam.c
|
||||
* for how communication is handled on the other side.
|
||||
*
|
||||
* The AuthSwitchRequest packet:
|
||||
* 4 bytes - Header
|
||||
* 0xfe - Command byte
|
||||
* string[NUL] - Auth plugin name, should be "dialog"
|
||||
* byte - Message type, 2 or 4
|
||||
* string[EOF] - Message(s)
|
||||
*
|
||||
* Additional prompts after AuthSwitchRequest:
|
||||
* 4 bytes - Header
|
||||
* byte - Message type, 2 or 4
|
||||
* string[EOF] - Message(s)
|
||||
*
|
||||
* Authenticators receive complete packets from protocol.
|
||||
*/
|
||||
|
||||
if (m_state == PAM_AUTH_INIT && check_auth_switch_request(dcb, buffer))
|
||||
// Smallest buffer that is parsed, header + (cmd-byte/msg-type + message).
|
||||
const int min_readable_buflen = MYSQL_HEADER_LEN + 1 + 1;
|
||||
// The buffer should be reasonable size. Large buffers likely mean that the auth scheme is complicated.
|
||||
const int MAX_BUFLEN = 2000;
|
||||
const char* srv_name = dcb->server->name();
|
||||
const int buflen = gwbuf_length(buffer);
|
||||
if (buflen <= min_readable_buflen || buflen > MAX_BUFLEN)
|
||||
{
|
||||
rval = true;
|
||||
MXB_ERROR("Received packet of size %i from '%s' during authentication. Expected packet size is "
|
||||
"between %i and %i.", buflen, srv_name, min_readable_buflen, MAX_BUFLEN);
|
||||
return false;
|
||||
}
|
||||
else if (m_state == PAM_AUTH_DATA_SENT)
|
||||
|
||||
uint8_t data[buflen + 1]; // + 1 to ensure that the end has a zero.
|
||||
data[buflen] = 0;
|
||||
gwbuf_copy_data(buffer, 0, buflen, data);
|
||||
m_sequence = data[MYSQL_SEQ_OFFSET] + 1;
|
||||
const uint8_t* data_ptr = data + MYSQL_COM_OFFSET;
|
||||
const uint8_t* end_ptr = data + buflen;
|
||||
bool success = false;
|
||||
bool unexpected_data = false;
|
||||
|
||||
switch (m_state)
|
||||
{
|
||||
/** Read authentication response */
|
||||
if (mxs_mysql_is_ok_packet(buffer))
|
||||
case State::INIT:
|
||||
// Server should have sent the AuthSwitchRequest + 1st prompt
|
||||
if (parse_authswitchreq(&data_ptr, end_ptr, srv_name)
|
||||
&& parse_password_prompt(&data_ptr, end_ptr, srv_name))
|
||||
{
|
||||
MXS_DEBUG("pam_backend_auth_extract received ok packet from '%s'.",
|
||||
dcb->server->name());
|
||||
m_state = PAM_AUTH_OK;
|
||||
rval = true;
|
||||
m_state = State::RECEIVED_PROMT;
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("Expected ok from server but got something else. Authentication"
|
||||
" failed.");
|
||||
unexpected_data = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::PW_SENT:
|
||||
{
|
||||
/** Read authentication response. This is typically either OK packet or ERROR, but can be another
|
||||
* prompt. */
|
||||
uint8_t cmdbyte = data[MYSQL_COM_OFFSET];
|
||||
if (cmdbyte == MYSQL_REPLY_OK)
|
||||
{
|
||||
MXS_DEBUG("pam_backend_auth_extract received ok packet from '%s'.", srv_name);
|
||||
m_state = State::DONE;
|
||||
success = true;
|
||||
}
|
||||
else if (cmdbyte == MYSQL_REPLY_ERR)
|
||||
{
|
||||
MXS_DEBUG("pam_backend_auth_extract received error packet from '%s'.", srv_name);
|
||||
m_state = State::DONE;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The packet may contain another prompt, try parse it. Currently, it's expected to be
|
||||
// another "Password: ", in the future other setups may be supported.
|
||||
if (parse_password_prompt(&data_ptr, end_ptr, srv_name))
|
||||
{
|
||||
m_state = State::RECEIVED_PROMT;
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("Expected OK, ERR or PAM prompt from '%s' but received something else. ",
|
||||
srv_name);
|
||||
unexpected_data = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
// This implicates an error in either PAM authenticator or backend protocol.
|
||||
mxb_assert(!true);
|
||||
unexpected_data = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!rval)
|
||||
if (unexpected_data)
|
||||
{
|
||||
MXS_DEBUG("pam_backend_auth_extract to backend '%s' failed for user '%s'.",
|
||||
dcb->server->name(),
|
||||
dcb->user);
|
||||
MXS_ERROR("Failed to read data from '%s' when authenticating user '%s'.", srv_name, dcb->user);
|
||||
}
|
||||
return rval;
|
||||
return success;
|
||||
}
|
||||
|
||||
int PamBackendSession::authenticate(DCB* dcb)
|
||||
{
|
||||
int rval = MXS_AUTH_FAILED;
|
||||
|
||||
if (m_state == PAM_AUTH_INIT)
|
||||
if (m_state == State::RECEIVED_PROMT)
|
||||
{
|
||||
MXS_DEBUG("pam_backend_auth_authenticate sending password to '%s'.",
|
||||
dcb->server->name());
|
||||
MXS_DEBUG("pam_backend_auth_authenticate sending password to '%s'.", dcb->server->name());
|
||||
if (send_client_password(dcb))
|
||||
{
|
||||
m_state = State::PW_SENT;
|
||||
rval = MXS_AUTH_INCOMPLETE;
|
||||
m_state = PAM_AUTH_DATA_SENT;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_state = State::DONE;
|
||||
}
|
||||
}
|
||||
else if (m_state == PAM_AUTH_OK)
|
||||
else if (m_state == State::DONE)
|
||||
{
|
||||
rval = MXS_AUTH_SUCCEEDED;
|
||||
}
|
||||
|
@ -18,9 +18,10 @@
|
||||
|
||||
class PamBackendSession
|
||||
{
|
||||
PamBackendSession(const PamBackendSession& orig);
|
||||
PamBackendSession& operator=(const PamBackendSession&);
|
||||
public:
|
||||
PamBackendSession(const PamBackendSession& orig) = delete;
|
||||
PamBackendSession& operator=(const PamBackendSession&) = delete;
|
||||
|
||||
PamBackendSession();
|
||||
bool extract(DCB* dcb, GWBUF* buffer);
|
||||
int authenticate(DCB* dcb);
|
||||
@ -28,6 +29,14 @@ public:
|
||||
private:
|
||||
bool send_client_password(DCB* dcb);
|
||||
|
||||
pam_auth_state m_state; /**< Authentication state*/
|
||||
uint8_t m_sequence; /**< The next packet seqence number */
|
||||
enum class State
|
||||
{
|
||||
INIT,
|
||||
RECEIVED_PROMT,
|
||||
PW_SENT,
|
||||
DONE
|
||||
};
|
||||
|
||||
State m_state {State::INIT}; /**< Authentication state*/
|
||||
uint8_t m_sequence {0}; /**< The next packet sequence number */
|
||||
};
|
||||
|
@ -22,5 +22,4 @@ const std::string DIALOG = "dialog";
|
||||
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.";
|
||||
|
||||
|
@ -21,7 +21,6 @@
|
||||
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
|
||||
@ -33,7 +32,7 @@ enum pam_auth_state
|
||||
};
|
||||
|
||||
/* Magic numbers from server source
|
||||
* https://github.com/MariaDB/server/blob/10.2/plugin/auth_pam/auth_pam.c */
|
||||
* https://github.com/MariaDB/server/blob/10.2/plugin/auth_pam/auth_pam.c */
|
||||
enum dialog_plugin_msg_types
|
||||
{
|
||||
DIALOG_ECHO_ENABLED = 2,
|
||||
|
Loading…
x
Reference in New Issue
Block a user