Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compatibility with MySQL clients which don't support sha2_password auth plugin #8036

Merged
merged 11 commits into from
Dec 9, 2019
3 changes: 2 additions & 1 deletion dbms/programs/server/MySQLHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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();
}
Expand Down
38 changes: 37 additions & 1 deletion dbms/src/Access/Authentication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
switch (type)
Expand All @@ -168,7 +197,14 @@ bool Authentication::isCorrectPassword(const String & password_) const
return true;

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:
return encodeSHA256(password_) == password_hash;
Expand Down
4 changes: 4 additions & 0 deletions dbms/src/Access/Authentication.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class Authentication
void setPasswordHashBinary(const Digest & 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.
bool isCorrectPassword(const String & password) const;

Expand Down
5 changes: 1 addition & 4 deletions dbms/src/Core/MySQLProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -953,10 +953,7 @@ class Native41 : public IPlugin

auto user = context.getUser(user_name);

if (user->authentication.getType() != DB::Authentication::DOUBLE_SHA1_PASSWORD)
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();
Poco::SHA1Engine::Digest double_sha1_value = user->authentication.getPasswordDoubleSHA1();
assert(double_sha1_value.size() == Poco::SHA1Engine::DIGEST_SIZE);

Poco::SHA1Engine engine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ services:
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 1
command: --federated --socket /var/run/mysqld/mysqld.sock
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
interval: 1s
timeout: 2s
retries: 100
10 changes: 10 additions & 0 deletions dbms/tests/integration/test_mysql_protocol/configs/users.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
<quota>default</quota>
</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>
<!-- echo -n abacaba | openssl dgst -sha1 -binary | openssl dgst -sha1 !-->
<password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
Expand Down
38 changes: 28 additions & 10 deletions dbms/tests/integration/test_mysql_protocol/test.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# coding: utf-8

import docker
import datetime
import math
import os
import pytest
import subprocess
import time

import docker
import pymysql.connections

from docker.models.containers import Container

from helpers.cluster import ClickHouseCluster
Expand Down Expand Up @@ -39,6 +39,25 @@ def mysql_client():
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')
def golang_container():
docker_compose = os.path.join(SCRIPT_DIR, 'clients', 'golang', 'docker_compose.yml')
Expand Down Expand Up @@ -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', ''])

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('''CREATE DATABASE mysql_federated''', 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"})


code, (_, stderr) = mysql_client.exec_run('''
code, (_, stderr) = mysql_server.exec_run('''
mysql
-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');"
Expand All @@ -128,15 +147,15 @@ def test_mysql_federated(mysql_client, server_address):

assert code == 0

code, (stdout, stderr) = mysql_client.exec_run('''
code, (stdout, stderr) = mysql_server.exec_run('''
mysql
-e "CREATE TABLE mysql_federated.test(`col` int UNSIGNED) ENGINE=FEDERATED CONNECTION='clickhouse';"
-e "SELECT * FROM mysql_federated.test ORDER BY col;"
'''.format(host=server_address, port=server_port), demux=True)

assert stdout == '\n'.join(['col', '0', '1', '5', ''])

code, (stdout, stderr) = mysql_client.exec_run('''
code, (stdout, stderr) = mysql_server.exec_run('''
mysql
-e "INSERT INTO mysql_federated.test VALUES (0), (1), (5);"
-e "SELECT * FROM mysql_federated.test ORDER BY col;"
Expand Down Expand Up @@ -233,13 +252,12 @@ def test_php_client(server_address, php_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 '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)
assert code == 1
assert 'MySQL is requesting the sha256_password authentication method, which is not supported.' in stderr
assert code == 0

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
Expand Down