2024-09-27 10:19:39 +00:00
import logging
import os . path
import ssl
import urllib . parse
import urllib . request
from os import remove
2022-02-18 14:12:03 +00:00
import pytest
2024-09-27 10:19:39 +00:00
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
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 >
2024-07-22 06:46:41 +00:00
< verificationMode > strict < / verificationMode >
2023-03-14 22:10:08 +00:00
< certificateFile > { certificateFile } < / certificateFile >
< privateKeyFile > { privateKeyFile } < / privateKeyFile >
< caConfig > { caConfig } < / caConfig >
< / 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 "
)
2024-02-13 10:02:47 +00:00
assert " unknown ca " in str ( err . value )
2023-04-21 14:22:14 +00:00
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-09-21 08:19:09 +00:00
# TODO: Add non-flaky tests for:
# - Wrong certificate: self-signed certificate.
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-09-21 08:19:09 +00:00
# TODO: Add non-flaky tests for:
# - sending wrong cert
2022-02-18 18:01:30 +00:00
def test_create_user ( ) :
2024-09-02 12:59:28 +00:00
instance . query ( " DROP USER IF EXISTS emma " )
2022-02-18 18:01:30 +00:00
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 "
)
2024-07-28 13:04:38 +00:00
== " emma \t [ ' ssl_certificate ' ] \t [ ' { \" common_names \" :[ \" client2 \" ]} ' ] \n "
' lucy \t [ \' ssl_certificate \' ] \t [ \' { " common_names " :[ " client2 " , " client3 " ]} \' ] \n '
2022-03-22 16:39:58 +00:00
)
2024-06-03 15:38:40 +00:00
2024-09-02 12:59:28 +00:00
instance . query ( " DROP USER IF EXISTS emma " )
2024-06-03 15:38:40 +00:00
def test_x509_san_support ( ) :
2024-09-02 12:59:28 +00:00
instance . query ( " DROP USER IF EXISTS jemma " )
2024-06-03 15:38:40 +00:00
assert (
execute_query_native (
instance , " SELECT currentUser() " , user = " jerome " , cert_name = " client4 "
)
== " jerome \n "
)
assert (
execute_query_https ( " SELECT currentUser() " , user = " jerome " , cert_name = " client4 " )
== " jerome \n "
)
assert (
instance . query (
" SELECT name, auth_type, auth_params FROM system.users WHERE name= ' jerome ' "
)
2024-07-28 13:04:38 +00:00
== ' jerome \t [ \' ssl_certificate \' ] \t [ \' { " subject_alt_names " :[ " URI:spiffe: \\ \\ / \\ \\ /foo.com \\ \\ /bar " , " URI:spiffe: \\ \\ / \\ \\ /foo.com \\ \\ /baz " ]} \' ] \n '
2024-06-03 15:38:40 +00:00
)
# user `jerome` is configured via xml config, but `show create` should work regardless.
assert (
instance . query ( " SHOW CREATE USER jerome " )
== " CREATE USER jerome IDENTIFIED WITH ssl_certificate SAN \\ ' URI:spiffe://foo.com/bar \\ ' , \\ ' URI:spiffe://foo.com/baz \\ ' \n "
)
instance . query (
" CREATE USER jemma IDENTIFIED WITH ssl_certificate SAN ' URI:spiffe://foo.com/bar ' , ' URI:spiffe://foo.com/baz ' "
)
assert (
execute_query_https ( " SELECT currentUser() " , user = " jemma " , cert_name = " client4 " )
== " jemma \n "
)
assert (
instance . query ( " SHOW CREATE USER jemma " )
== " CREATE USER jemma IDENTIFIED WITH ssl_certificate SAN \\ ' URI:spiffe://foo.com/bar \\ ' , \\ ' URI:spiffe://foo.com/baz \\ ' \n "
)
2024-09-02 12:59:28 +00:00
instance . query ( " DROP USER IF EXISTS jemma " )
2024-09-12 00:45:54 +00:00
2024-09-03 09:37:03 +00:00
2024-08-12 10:05:10 +00:00
def test_x509_san_wildcard_support ( ) :
assert (
2024-09-03 09:37:03 +00:00
execute_query_native (
instance , " SELECT currentUser() " , user = " stewie " , cert_name = " client5 "
)
2024-08-12 10:05:10 +00:00
== " stewie \n "
)
assert (
2024-09-03 09:37:03 +00:00
instance . query (
" SELECT name, auth_type, auth_params FROM system.users WHERE name= ' stewie ' "
)
2024-09-13 12:04:14 +00:00
== " stewie \t [ ' ssl_certificate ' ] \t [ ' { \" subject_alt_names \" :[ \" URI:spiffe: \\ \\ / \\ \\ /bar.com \\ \\ /foo \\ \\ /* \\ \\ /far \" ]} ' ] \n "
2024-08-12 10:05:10 +00:00
)
assert (
instance . query ( " SHOW CREATE USER stewie " )
== " CREATE USER stewie IDENTIFIED WITH ssl_certificate SAN \\ ' URI:spiffe://bar.com/foo/*/far \\ ' \n "
)
instance . query (
" CREATE USER brian IDENTIFIED WITH ssl_certificate SAN ' URI:spiffe://bar.com/foo/*/far ' "
)
assert (
execute_query_https ( " SELECT currentUser() " , user = " brian " , cert_name = " client6 " )
== " brian \n "
)
assert (
instance . query ( " SHOW CREATE USER brian " )
== " CREATE USER brian IDENTIFIED WITH ssl_certificate SAN \\ ' URI:spiffe://bar.com/foo/*/far \\ ' \n "
)
2024-09-12 00:45:54 +00:00
instance . query ( " DROP USER brian " )