Merge pull request #8036 from yurriy/mysql

Compatibility with MySQL clients which don't support sha2_password auth plugin
This commit is contained in:
alexey-milovidov 2019-12-09 23:11:31 +03:00 committed by GitHub
commit cce78ef018
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 87 additions and 16 deletions

View File

@ -221,7 +221,8 @@ void MySQLHandler::authenticate(const String & user_name, const String & auth_pl
{ {
// For compatibility with JavaScript MySQL client, Native41 authentication plugin is used when possible (if password is specified using double SHA1). Otherwise SHA256 plugin is used. // For compatibility with JavaScript MySQL client, Native41 authentication plugin is used when possible (if password is specified using double SHA1). Otherwise SHA256 plugin is used.
auto user = connection_context.getUser(user_name); auto user = connection_context.getUser(user_name);
if (user->authentication.getType() != DB::Authentication::DOUBLE_SHA1_PASSWORD) const DB::Authentication::Type user_auth_type = user->authentication.getType();
if (user_auth_type != DB::Authentication::DOUBLE_SHA1_PASSWORD && user_auth_type != DB::Authentication::PLAINTEXT_PASSWORD && user_auth_type != DB::Authentication::NO_PASSWORD)
{ {
authPluginSSL(); authPluginSSL();
} }

View File

@ -160,6 +160,35 @@ void Authentication::setPasswordHashBinary(const Digest & hash)
} }
Digest Authentication::getPasswordDoubleSHA1() const
{
switch (type)
{
case NO_PASSWORD:
{
Poco::SHA1Engine engine;
return engine.digest();
}
case PLAINTEXT_PASSWORD:
{
Poco::SHA1Engine engine;
engine.update(getPassword());
const Digest & first_sha1 = engine.digest();
engine.update(first_sha1.data(), first_sha1.size());
return engine.digest();
}
case SHA256_PASSWORD:
throw Exception("Cannot get password double SHA1 for user with 'SHA256_PASSWORD' authentication.", ErrorCodes::BAD_ARGUMENTS);
case DOUBLE_SHA1_PASSWORD:
return password_hash;
}
throw Exception("Unknown authentication type: " + std::to_string(static_cast<int>(type)), ErrorCodes::LOGICAL_ERROR);
}
bool Authentication::isCorrectPassword(const String & password_) const bool Authentication::isCorrectPassword(const String & password_) const
{ {
switch (type) switch (type)
@ -168,7 +197,14 @@ bool Authentication::isCorrectPassword(const String & password_) const
return true; return true;
case PLAINTEXT_PASSWORD: case PLAINTEXT_PASSWORD:
return password_ == StringRef{reinterpret_cast<const char *>(password_hash.data()), password_hash.size()}; {
if (password_ == StringRef{reinterpret_cast<const char *>(password_hash.data()), password_hash.size()})
return true;
// For compatibility with MySQL clients which support only native authentication plugin, SHA1 can be passed instead of password.
auto password_sha1 = encodeSHA1(password_hash);
return password_ == StringRef{reinterpret_cast<const char *>(password_sha1.data()), password_sha1.size()};
}
case SHA256_PASSWORD: case SHA256_PASSWORD:
return encodeSHA256(password_) == password_hash; return encodeSHA256(password_) == password_hash;

View File

@ -49,6 +49,10 @@ public:
void setPasswordHashBinary(const Digest & hash); void setPasswordHashBinary(const Digest & hash);
const Digest & getPasswordHashBinary() const { return password_hash; } const Digest & getPasswordHashBinary() const { return password_hash; }
/// Returns SHA1(SHA1(password)) used by MySQL compatibility server for authentication.
/// Allowed to use for Type::NO_PASSWORD, Type::PLAINTEXT_PASSWORD, Type::DOUBLE_SHA1_PASSWORD.
Digest getPasswordDoubleSHA1() const;
/// Checks if the provided password is correct. Returns false if not. /// Checks if the provided password is correct. Returns false if not.
bool isCorrectPassword(const String & password) const; bool isCorrectPassword(const String & password) const;

View File

@ -953,10 +953,7 @@ public:
auto user = context.getUser(user_name); auto user = context.getUser(user_name);
if (user->authentication.getType() != DB::Authentication::DOUBLE_SHA1_PASSWORD) Poco::SHA1Engine::Digest double_sha1_value = user->authentication.getPasswordDoubleSHA1();
throw Exception("Cannot use " + getName() + " auth plugin for user " + user_name + " since its password isn't specified using double SHA1.", ErrorCodes::UNKNOWN_EXCEPTION);
Poco::SHA1Engine::Digest double_sha1_value = user->authentication.getPasswordHashBinary();
assert(double_sha1_value.size() == Poco::SHA1Engine::DIGEST_SIZE); assert(double_sha1_value.size() == Poco::SHA1Engine::DIGEST_SIZE);
Poco::SHA1Engine engine; Poco::SHA1Engine engine;

View File

@ -6,3 +6,8 @@ services:
environment: environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 1 MYSQL_ALLOW_EMPTY_PASSWORD: 1
command: --federated --socket /var/run/mysqld/mysqld.sock command: --federated --socket /var/run/mysqld/mysqld.sock
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
interval: 1s
timeout: 2s
retries: 100

View File

@ -15,6 +15,16 @@
<quota>default</quota> <quota>default</quota>
</default> </default>
<user_with_sha256>
<!-- echo -n abacaba | openssl dgst -sha256 !-->
<password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
<networks incl="networks" replace="replace">
<ip>::/0</ip>
</networks>
<profile>default</profile>
<quota>default</quota>
</user_with_sha256>
<user_with_double_sha1> <user_with_double_sha1>
<!-- echo -n abacaba | openssl dgst -sha1 -binary | openssl dgst -sha1 !--> <!-- echo -n abacaba | openssl dgst -sha1 -binary | openssl dgst -sha1 !-->
<password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex> <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>

View File

@ -1,14 +1,14 @@
# coding: utf-8 # coding: utf-8
import docker
import datetime import datetime
import math import math
import os import os
import pytest import pytest
import subprocess import subprocess
import time import time
import docker
import pymysql.connections import pymysql.connections
from docker.models.containers import Container from docker.models.containers import Container
from helpers.cluster import ClickHouseCluster from helpers.cluster import ClickHouseCluster
@ -39,6 +39,25 @@ def mysql_client():
yield docker.from_env().containers.get(cluster.project_name + '_mysql1_1') yield docker.from_env().containers.get(cluster.project_name + '_mysql1_1')
@pytest.fixture(scope='module')
def mysql_server(mysql_client):
"""Return MySQL container when it is healthy.
:type mysql_client: Container
:rtype: Container
"""
retries = 30
for i in range(retries):
info = mysql_client.client.api.inspect_container(mysql_client.name)
if info['State']['Health']['Status'] == 'healthy':
break
time.sleep(1)
else:
raise Exception('Mysql server has not started in %d seconds.' % retries)
return mysql_client
@pytest.fixture(scope='module') @pytest.fixture(scope='module')
def golang_container(): def golang_container():
docker_compose = os.path.join(SCRIPT_DIR, 'clients', 'golang', 'docker_compose.yml') docker_compose = os.path.join(SCRIPT_DIR, 'clients', 'golang', 'docker_compose.yml')
@ -111,14 +130,14 @@ def test_mysql_client(mysql_client, server_address):
assert stdout == '\n'.join(['column', '0', '0', '1', '1', '5', '5', 'tmp_column', '0', '1', '']) assert stdout == '\n'.join(['column', '0', '0', '1', '1', '5', '5', 'tmp_column', '0', '1', ''])
def test_mysql_federated(mysql_client, server_address):
def test_mysql_federated(mysql_server, server_address):
node.query('''DROP DATABASE IF EXISTS mysql_federated''', settings={"password": "123"}) node.query('''DROP DATABASE IF EXISTS mysql_federated''', settings={"password": "123"})
node.query('''CREATE DATABASE mysql_federated''', settings={"password": "123"}) node.query('''CREATE DATABASE mysql_federated''', settings={"password": "123"})
node.query('''CREATE TABLE mysql_federated.test (col UInt32) ENGINE = Log''', settings={"password": "123"}) node.query('''CREATE TABLE mysql_federated.test (col UInt32) ENGINE = Log''', settings={"password": "123"})
node.query('''INSERT INTO mysql_federated.test VALUES (0), (1), (5)''', settings={"password": "123"}) node.query('''INSERT INTO mysql_federated.test VALUES (0), (1), (5)''', settings={"password": "123"})
code, (_, stderr) = mysql_server.exec_run('''
code, (_, stderr) = mysql_client.exec_run('''
mysql mysql
-e "DROP SERVER IF EXISTS clickhouse;" -e "DROP SERVER IF EXISTS clickhouse;"
-e "CREATE SERVER clickhouse FOREIGN DATA WRAPPER mysql OPTIONS (USER 'default', PASSWORD '123', HOST '{host}', PORT {port}, DATABASE 'mysql_federated');" -e "CREATE SERVER clickhouse FOREIGN DATA WRAPPER mysql OPTIONS (USER 'default', PASSWORD '123', HOST '{host}', PORT {port}, DATABASE 'mysql_federated');"
@ -128,7 +147,7 @@ def test_mysql_federated(mysql_client, server_address):
assert code == 0 assert code == 0
code, (stdout, stderr) = mysql_client.exec_run(''' code, (stdout, stderr) = mysql_server.exec_run('''
mysql mysql
-e "CREATE TABLE mysql_federated.test(`col` int UNSIGNED) ENGINE=FEDERATED CONNECTION='clickhouse';" -e "CREATE TABLE mysql_federated.test(`col` int UNSIGNED) ENGINE=FEDERATED CONNECTION='clickhouse';"
-e "SELECT * FROM mysql_federated.test ORDER BY col;" -e "SELECT * FROM mysql_federated.test ORDER BY col;"
@ -136,7 +155,7 @@ def test_mysql_federated(mysql_client, server_address):
assert stdout == '\n'.join(['col', '0', '1', '5', '']) assert stdout == '\n'.join(['col', '0', '1', '5', ''])
code, (stdout, stderr) = mysql_client.exec_run(''' code, (stdout, stderr) = mysql_server.exec_run('''
mysql mysql
-e "INSERT INTO mysql_federated.test VALUES (0), (1), (5);" -e "INSERT INTO mysql_federated.test VALUES (0), (1), (5);"
-e "SELECT * FROM mysql_federated.test ORDER BY col;" -e "SELECT * FROM mysql_federated.test ORDER BY col;"
@ -233,13 +252,12 @@ def test_php_client(server_address, php_container):
def test_mysqljs_client(server_address, nodejs_container): def test_mysqljs_client(server_address, nodejs_container):
code, (_, stderr) = nodejs_container.exec_run('node test.js {host} {port} default 123'.format(host=server_address, port=server_port), demux=True) code, (_, stderr) = nodejs_container.exec_run('node test.js {host} {port} user_with_sha256 abacaba'.format(host=server_address, port=server_port), demux=True)
assert code == 1 assert code == 1
assert 'MySQL is requesting the sha256_password authentication method, which is not supported.' in stderr assert 'MySQL is requesting the sha256_password authentication method, which is not supported.' in stderr
code, (_, stderr) = nodejs_container.exec_run('node test.js {host} {port} user_with_empty_password ""'.format(host=server_address, port=server_port), demux=True) code, (_, stderr) = nodejs_container.exec_run('node test.js {host} {port} user_with_empty_password ""'.format(host=server_address, port=server_port), demux=True)
assert code == 1 assert code == 0
assert 'MySQL is requesting the sha256_password authentication method, which is not supported.' in stderr
code, (_, _) = nodejs_container.exec_run('node test.js {host} {port} user_with_double_sha1 abacaba'.format(host=server_address, port=server_port), demux=True) code, (_, _) = nodejs_container.exec_run('node test.js {host} {port} user_with_double_sha1 abacaba'.format(host=server_address, port=server_port), demux=True)
assert code == 0 assert code == 0