2022-02-18 14:12:03 +00:00
import pytest
2023-03-14 22:10:08 +00:00
from helpers . client import Client
2022-02-18 14:12:03 +00:00
from helpers . cluster import ClickHouseCluster
2022-08-09 09:54:28 +00:00
from helpers . ssl_context import WrapSSLContextWithSNI
2023-06-28 10:24:54 +00:00
import urllib . request , urllib . parse
2022-02-18 14:12:03 +00:00
import ssl
import os . path
2023-03-14 22:10:08 +00:00
from os import remove
2023-06-28 10:37:38 +00:00
import logging
2023-03-14 22:10:08 +00:00
2022-02-18 14:12:03 +00:00
2022-08-09 10:52:16 +00:00
# The test cluster is configured with certificate for that host name, see 'server-ext.cnf'.
2022-08-09 09:54:28 +00:00
# The client have to verify server certificate against that name. Client uses SNI
SSL_HOST = " integration-tests.clickhouse.com "
2022-02-18 14:12:03 +00:00
HTTPS_PORT = 8443
2022-08-01 11:16:12 +00:00
# It's important for the node to work at this IP because 'server-cert.pem' requires that (see server-ext.cnf).
2022-02-18 14:12:03 +00:00
SCRIPT_DIR = os . path . dirname ( os . path . realpath ( __file__ ) )
2023-06-28 10:24:54 +00:00
MAX_RETRY = 5
2022-02-18 14:12:03 +00:00
cluster = ClickHouseCluster ( __file__ )
2022-03-22 16:39:58 +00:00
instance = cluster . add_instance (
" node " ,
main_configs = [
" configs/ssl_config.xml " ,
" certs/server-key.pem " ,
" certs/server-cert.pem " ,
" certs/ca-cert.pem " ,
] ,
user_configs = [ " configs/users_with_ssl_auth.xml " ] ,
)
2022-02-18 14:12:03 +00:00
@pytest.fixture ( scope = " module " , autouse = True )
def started_cluster ( ) :
try :
cluster . start ( )
yield cluster
finally :
cluster . shutdown ( )
2023-03-14 22:10:08 +00:00
config = """ <clickhouse>
< openSSL >
< client >
< verificationMode > none < / verificationMode >
< certificateFile > { certificateFile } < / certificateFile >
< privateKeyFile > { privateKeyFile } < / privateKeyFile >
< caConfig > { caConfig } < / caConfig >
< invalidCertificateHandler >
< name > AcceptCertificateHandler < / name >
< / invalidCertificateHandler >
< / client >
< / openSSL >
< / clickhouse > """
2023-04-21 14:22:14 +00:00
def execute_query_native ( node , query , user , cert_name , password = None ) :
2023-03-14 22:10:08 +00:00
config_path = f " { SCRIPT_DIR } /configs/client.xml "
formatted = config . format (
certificateFile = f " { SCRIPT_DIR } /certs/ { cert_name } -cert.pem " ,
privateKeyFile = f " { SCRIPT_DIR } /certs/ { cert_name } -key.pem " ,
caConfig = f " { SCRIPT_DIR } /certs/ca-cert.pem " ,
)
file = open ( config_path , " w " )
file . write ( formatted )
file . close ( )
client = Client (
node . ip_address ,
9440 ,
command = cluster . client_bin_path ,
secure = True ,
config = config_path ,
)
try :
2023-04-21 14:22:14 +00:00
result = client . query ( query , user = user , password = password )
2023-03-14 22:10:08 +00:00
remove ( config_path )
return result
except :
remove ( config_path )
raise
2023-04-21 14:22:14 +00:00
def test_native ( ) :
2022-03-22 16:39:58 +00:00
assert (
2023-04-21 14:22:14 +00:00
execute_query_native (
instance , " SELECT currentUser() " , user = " john " , cert_name = " client1 "
)
2022-03-22 16:39:58 +00:00
== " john \n "
)
assert (
2023-04-21 14:22:14 +00:00
execute_query_native (
instance , " SELECT currentUser() " , user = " lucy " , cert_name = " client2 "
)
2022-03-22 16:39:58 +00:00
== " lucy \n "
)
assert (
2023-04-21 14:22:14 +00:00
execute_query_native (
instance , " SELECT currentUser() " , user = " lucy " , cert_name = " client3 "
)
2022-03-22 16:39:58 +00:00
== " lucy \n "
)
2022-02-18 14:12:03 +00:00
2023-04-21 14:22:14 +00:00
def test_native_wrong_cert ( ) :
# Wrong certificate: different user's certificate
with pytest . raises ( Exception ) as err :
execute_query_native (
instance , " SELECT currentUser() " , user = " john " , cert_name = " client2 "
)
assert " AUTHENTICATION_FAILED " in str ( err . value )
# Wrong certificate: self-signed certificate.
# In this case clickhouse-client itself will throw an error
with pytest . raises ( Exception ) as err :
execute_query_native (
instance , " SELECT currentUser() " , user = " john " , cert_name = " wrong "
)
assert " UNKNOWN_CA " in str ( err . value )
def test_native_fallback_to_password ( ) :
# Unrelated certificate, correct password
2023-03-14 22:10:08 +00:00
assert (
execute_query_native (
2023-04-21 14:22:14 +00:00
instance ,
" SELECT currentUser() " ,
user = " jane " ,
cert_name = " client2 " ,
password = " qwe123 " ,
2023-03-14 22:10:08 +00:00
)
2023-04-21 14:22:14 +00:00
== " jane \n "
2023-03-14 22:10:08 +00:00
)
2023-04-21 14:22:14 +00:00
# Unrelated certificate, wrong password
with pytest . raises ( Exception ) as err :
2023-03-14 22:10:08 +00:00
execute_query_native (
2023-04-21 14:22:14 +00:00
instance ,
" SELECT currentUser() " ,
user = " jane " ,
cert_name = " client2 " ,
password = " wrong " ,
)
assert " AUTHENTICATION_FAILED " in str ( err . value )
def get_ssl_context ( cert_name ) :
context = WrapSSLContextWithSNI ( SSL_HOST , ssl . PROTOCOL_TLS_CLIENT )
context . load_verify_locations ( cafile = f " { SCRIPT_DIR } /certs/ca-cert.pem " )
if cert_name :
context . load_cert_chain (
f " { SCRIPT_DIR } /certs/ { cert_name } -cert.pem " ,
f " { SCRIPT_DIR } /certs/ { cert_name } -key.pem " ,
2023-03-14 22:10:08 +00:00
)
2023-04-21 14:22:14 +00:00
context . verify_mode = ssl . CERT_REQUIRED
context . check_hostname = True
2023-08-19 13:01:24 +00:00
# Python 3.10 has removed many ciphers from the cipher suite.
# Hence based on https://github.com/urllib3/urllib3/issues/3100#issuecomment-1671106236
# we are expanding the list of cipher suites.
context . set_ciphers ( " DEFAULT " )
2023-04-21 14:22:14 +00:00
return context
def execute_query_https (
query , user , enable_ssl_auth = True , cert_name = None , password = None
) :
2023-06-28 10:24:54 +00:00
url = (
f " https:// { instance . ip_address } : { HTTPS_PORT } /?query= { urllib . parse . quote ( query ) } "
)
request = urllib . request . Request ( url )
request . add_header ( " X-ClickHouse-User " , user )
2023-05-22 08:20:45 +00:00
if enable_ssl_auth :
2023-06-28 10:24:54 +00:00
request . add_header ( " X-ClickHouse-SSL-Certificate-Auth " , " on " )
2023-05-22 08:20:45 +00:00
if password :
2023-06-28 10:24:54 +00:00
request . add_header ( " X-ClickHouse-Key " , password )
response = urllib . request . urlopen (
request , context = get_ssl_context ( cert_name )
) . read ( )
return response . decode ( " utf-8 " )
2023-05-18 07:20:38 +00:00
2023-04-21 14:22:14 +00:00
def test_https ( ) :
assert (
execute_query_https ( " SELECT currentUser() " , user = " john " , cert_name = " client1 " )
== " john \n "
)
assert (
execute_query_https ( " SELECT currentUser() " , user = " lucy " , cert_name = " client2 " )
2023-03-14 22:10:08 +00:00
== " lucy \n "
)
assert (
2023-04-21 14:22:14 +00:00
execute_query_https ( " SELECT currentUser() " , user = " lucy " , cert_name = " client3 " )
2023-03-14 22:10:08 +00:00
== " lucy \n "
)
2022-02-18 14:12:03 +00:00
def test_https_wrong_cert ( ) :
# Wrong certificate: different user's certificate
with pytest . raises ( Exception ) as err :
2022-03-22 16:39:58 +00:00
execute_query_https ( " SELECT currentUser() " , user = " john " , cert_name = " client2 " )
2023-05-18 07:20:38 +00:00
assert " 403 " in str ( err . value )
2022-02-18 14:12:03 +00:00
2023-06-28 10:24:54 +00:00
count = 0
2022-02-18 14:12:03 +00:00
# Wrong certificate: self-signed certificate.
2023-06-28 10:24:54 +00:00
while count < = MAX_RETRY :
with pytest . raises ( Exception ) as err :
execute_query_https ( " SELECT currentUser() " , user = " john " , cert_name = " wrong " )
err_str = str ( err . value )
2023-07-31 05:39:49 +00:00
if count < MAX_RETRY and (
( " Broken pipe " in err_str ) or ( " EOF occurred " in err_str )
) :
2023-06-28 10:24:54 +00:00
count = count + 1
logging . warning ( f " Failed attempt with wrong cert, err: { err_str } " )
continue
assert " unknown ca " in err_str
break
2022-02-18 14:12:03 +00:00
# No certificate.
with pytest . raises ( Exception ) as err :
execute_query_https ( " SELECT currentUser() " , user = " john " )
2023-05-18 07:20:38 +00:00
assert " 403 " in str ( err . value )
2022-02-18 14:12:03 +00:00
# No header enabling SSL authentication.
with pytest . raises ( Exception ) as err :
2022-03-22 16:39:58 +00:00
execute_query_https (
" SELECT currentUser() " ,
user = " john " ,
enable_ssl_auth = False ,
cert_name = " client1 " ,
)
2022-02-18 14:12:03 +00:00
def test_https_non_ssl_auth ( ) :
# Users with non-SSL authentication are allowed, in this case we can skip sending a client certificate at all (because "verificationMode" is set to "relaxed").
2022-03-22 16:39:58 +00:00
# assert execute_query_https("SELECT currentUser()", user="peter", enable_ssl_auth=False) == "peter\n"
assert (
execute_query_https (
" SELECT currentUser() " ,
user = " jane " ,
enable_ssl_auth = False ,
password = " qwe123 " ,
)
== " jane \n "
)
2022-02-18 14:12:03 +00:00
# But we still can send a certificate if we want.
2022-03-22 16:39:58 +00:00
assert (
execute_query_https (
" SELECT currentUser() " ,
user = " peter " ,
enable_ssl_auth = False ,
cert_name = " client1 " ,
)
== " peter \n "
)
assert (
execute_query_https (
" SELECT currentUser() " ,
user = " peter " ,
enable_ssl_auth = False ,
cert_name = " client2 " ,
)
== " peter \n "
)
assert (
execute_query_https (
" SELECT currentUser() " ,
user = " peter " ,
enable_ssl_auth = False ,
cert_name = " client3 " ,
)
== " peter \n "
)
assert (
execute_query_https (
" SELECT currentUser() " ,
user = " jane " ,
enable_ssl_auth = False ,
password = " qwe123 " ,
cert_name = " client1 " ,
)
== " jane \n "
)
assert (
execute_query_https (
" SELECT currentUser() " ,
user = " jane " ,
enable_ssl_auth = False ,
password = " qwe123 " ,
cert_name = " client2 " ,
)
== " jane \n "
)
assert (
execute_query_https (
" SELECT currentUser() " ,
user = " jane " ,
enable_ssl_auth = False ,
password = " qwe123 " ,
cert_name = " client3 " ,
)
== " jane \n "
)
2022-02-18 14:12:03 +00:00
2023-06-28 10:24:54 +00:00
count = 0
2022-02-18 14:12:03 +00:00
# However if we send a certificate it must not be wrong.
2023-06-28 10:24:54 +00:00
while count < = MAX_RETRY :
with pytest . raises ( Exception ) as err :
execute_query_https (
" SELECT currentUser() " ,
user = " peter " ,
enable_ssl_auth = False ,
cert_name = " wrong " ,
)
err_str = str ( err . value )
2023-07-31 05:39:49 +00:00
if count < MAX_RETRY and (
( " Broken pipe " in err_str ) or ( " EOF occurred " in err_str )
) :
2023-06-28 10:24:54 +00:00
count = count + 1
logging . warning (
f " Failed attempt with wrong cert, user: peter, err: { err_str } "
)
continue
assert " unknown ca " in err_str
break
count = 0
while count < = MAX_RETRY :
with pytest . raises ( Exception ) as err :
execute_query_https (
" SELECT currentUser() " ,
user = " jane " ,
enable_ssl_auth = False ,
password = " qwe123 " ,
cert_name = " wrong " ,
)
err_str = str ( err . value )
2023-07-31 05:39:49 +00:00
if count < MAX_RETRY and (
( " Broken pipe " in err_str ) or ( " EOF occurred " in err_str )
) :
2023-06-28 10:24:54 +00:00
count = count + 1
logging . warning (
f " Failed attempt with wrong cert, user: jane, err: { err_str } "
)
continue
assert " unknown ca " in err_str
break
2022-02-18 18:01:30 +00:00
def test_create_user ( ) :
instance . query ( " CREATE USER emma IDENTIFIED WITH ssl_certificate CN ' client3 ' " )
2022-03-22 16:39:58 +00:00
assert (
execute_query_https ( " SELECT currentUser() " , user = " emma " , cert_name = " client3 " )
== " emma \n "
)
assert (
instance . query ( " SHOW CREATE USER emma " )
== " CREATE USER emma IDENTIFIED WITH ssl_certificate CN \\ ' client3 \\ ' \n "
)
2022-02-18 18:01:30 +00:00
instance . query ( " ALTER USER emma IDENTIFIED WITH ssl_certificate CN ' client2 ' " )
2022-03-22 16:39:58 +00:00
assert (
execute_query_https ( " SELECT currentUser() " , user = " emma " , cert_name = " client2 " )
== " emma \n "
)
assert (
instance . query ( " SHOW CREATE USER emma " )
== " CREATE USER emma IDENTIFIED WITH ssl_certificate CN \\ ' client2 \\ ' \n "
)
2022-02-18 18:01:30 +00:00
with pytest . raises ( Exception ) as err :
2022-03-22 16:39:58 +00:00
execute_query_https ( " SELECT currentUser() " , user = " emma " , cert_name = " client3 " )
2023-05-18 07:20:38 +00:00
assert " 403 " in str ( err . value )
2022-02-18 18:01:30 +00:00
2022-03-22 16:39:58 +00:00
assert (
instance . query ( " SHOW CREATE USER lucy " )
== " CREATE USER lucy IDENTIFIED WITH ssl_certificate CN \\ ' client2 \\ ' , \\ ' client3 \\ ' \n "
)
assert (
instance . query (
" SELECT name, auth_type, auth_params FROM system.users WHERE name IN [ ' emma ' , ' lucy ' ] ORDER BY name "
)
== ' emma \t ssl_certificate \t { " common_names " :[ " client2 " ]} \n '
' lucy \t ssl_certificate \t { " common_names " :[ " client2 " , " client3 " ]} \n '
)