diff --git a/dbms/programs/server/MySQLHandler.cpp b/dbms/programs/server/MySQLHandler.cpp index c2de9eb74e0..a147ccafba0 100644 --- a/dbms/programs/server/MySQLHandler.cpp +++ b/dbms/programs/server/MySQLHandler.cpp @@ -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(); } diff --git a/dbms/src/Access/Authentication.cpp b/dbms/src/Access/Authentication.cpp index 5b641e2906e..88738dfc837 100644 --- a/dbms/src/Access/Authentication.cpp +++ b/dbms/src/Access/Authentication.cpp @@ -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(type)), ErrorCodes::LOGICAL_ERROR); +} + + bool Authentication::isCorrectPassword(const String & password_) const { switch (type) @@ -168,7 +197,14 @@ bool Authentication::isCorrectPassword(const String & password_) const return true; case PLAINTEXT_PASSWORD: - return password_ == StringRef{reinterpret_cast(password_hash.data()), password_hash.size()}; + { + if (password_ == StringRef{reinterpret_cast(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(password_sha1.data()), password_sha1.size()}; + } case SHA256_PASSWORD: return encodeSHA256(password_) == password_hash; diff --git a/dbms/src/Access/Authentication.h b/dbms/src/Access/Authentication.h index d8fae6e03eb..dc528fdedb8 100644 --- a/dbms/src/Access/Authentication.h +++ b/dbms/src/Access/Authentication.h @@ -49,6 +49,10 @@ public: 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; diff --git a/dbms/src/Core/MySQLProtocol.h b/dbms/src/Core/MySQLProtocol.h index db7a8dae2fa..25ce6213641 100644 --- a/dbms/src/Core/MySQLProtocol.h +++ b/dbms/src/Core/MySQLProtocol.h @@ -953,10 +953,7 @@ public: 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; diff --git a/dbms/tests/integration/test_mysql_protocol/clients/mysql/docker_compose.yml b/dbms/tests/integration/test_mysql_protocol/clients/mysql/docker_compose.yml index 6e0558208e2..752e59a2d08 100644 --- a/dbms/tests/integration/test_mysql_protocol/clients/mysql/docker_compose.yml +++ b/dbms/tests/integration/test_mysql_protocol/clients/mysql/docker_compose.yml @@ -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 diff --git a/dbms/tests/integration/test_mysql_protocol/configs/users.xml b/dbms/tests/integration/test_mysql_protocol/configs/users.xml index ebcd1a297e1..b88dfbada37 100644 --- a/dbms/tests/integration/test_mysql_protocol/configs/users.xml +++ b/dbms/tests/integration/test_mysql_protocol/configs/users.xml @@ -15,6 +15,16 @@ default + + + 65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5 + + ::/0 + + default + default + + e395796d6546b1b65db9d665cd43f0e858dd4303 diff --git a/dbms/tests/integration/test_mysql_protocol/test.py b/dbms/tests/integration/test_mysql_protocol/test.py index 159bbc310c6..3f4f4e2a2f8 100644 --- a/dbms/tests/integration/test_mysql_protocol/test.py +++ b/dbms/tests/integration/test_mysql_protocol/test.py @@ -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 @@ -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') @@ -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');" @@ -128,7 +147,7 @@ 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;" @@ -136,7 +155,7 @@ def test_mysql_federated(mysql_client, server_address): 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;" @@ -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