Skip to content

Commit 3ace9f4

Browse files
authored
Merge pull request #1865 from ClickHouse/v2_ssl_authentication
[client-v2] SSL Authentication
2 parents 11e258c + ffa0c07 commit 3ace9f4

File tree

10 files changed

+187
-14
lines changed

10 files changed

+187
-14
lines changed

clickhouse-client/src/test/resources/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,11 @@ openssl x509 -req -in server.csr -CA myCA.crt -CAkey myCA.key -CAcreateserial -o
1818
openssl req -nodes -subj "/CN=me" -newkey rsa:2048 -keyout client.key -out client.csr
1919
openssl x509 -req -in client.csr -out client.crt -CAcreateserial -CA myCA.crt -CAkey myCA.key -days 36500
2020
```
21+
22+
### Some_user
23+
24+
```bash
25+
openssl req -nodes -subj "/CN=some_user" -newkey rsa:2048 -keyout some_user.key -out some_user.csr
26+
openssl x509 -req -in some_user.csr -out some_user.crt -CAcreateserial -CA marsnet_ca.crt -CAkey marsnet_ca.key -days 36500
27+
28+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICtDCCAZwCFBbI6UQK2g1r8o4XRXu+9wvQBHmnMA0GCSqGSIb3DQEBCwUAMBcx
3+
FTATBgNVBAMMDGxvY2FsaG9zdCBDQTAgFw0yNDEwMTAxNjM3MzhaGA8yMTI0MDkx
4+
NjE2MzczOFowFDESMBAGA1UEAwwJc29tZV91c2VyMIIBIjANBgkqhkiG9w0BAQEF
5+
AAOCAQ8AMIIBCgKCAQEA68bBZlvBT64suwLa61eob9roTVXlJQmB9tGvX2cnJacP
6+
NBx2h6W8Ow43doRLBRt32SopV06O1i2c0L84pRoliJcGrUhKyxAsVxVv11mFd4qg
7+
962TeYe3VawSKK2w83GNfVhjQFwuNEDuzJT0I7J0jH/uNclMxAtFZNkKVMA2GOK2
8+
c3Pib8zCmqITWAX5XXWUUvS0LWsASaBAEVh4R7StYbDl0L3VeiHCw6fKpdevVfw5
9+
eDb+KuwMUOCPak0v31izEsXtcAyc7hxEZLfUMA+00zAdUENTC38GOJNTqirg0YmD
10+
+wxPdp3quWwkF/b831UTczAHkK7GP3swPjfciMN8nwIDAQABMA0GCSqGSIb3DQEB
11+
CwUAA4IBAQB+1poCA9p/XyKf5jxnAkaZQzoRW+fNqZvz8Eld2gGLqw7ZZUiBW6zo
12+
d4aCAeuNehw5zJEOf1ew5EZzdWYRdxXUarjs3HOSQalfYTS8HqNI19sgWYD6Zcx+
13+
sygJqswtplvPAB6phk9zyhQDLFNuJ8dp28xRgGuywYtVMnvLG1wapPf/fnqkRcOW
14+
yTBS4BBvtmzKPzMMZl/qB4Ol/STgVphceMFmI71HQQFUPb56E5tAQ+m3fezjdAJ2
15+
gZ+/LsApHLwhEV0ZGyIe/MNx0nDrkfWYWa7BsqvG6uuxyPXxgXSQofNjJN+RahRm
16+
oHREhAYRL40BS1F20aLRFRupzLJngLBh
17+
-----END CERTIFICATE-----
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-----BEGIN CERTIFICATE REQUEST-----
2+
MIICWTCCAUECAQAwFDESMBAGA1UEAwwJc29tZV91c2VyMIIBIjANBgkqhkiG9w0B
3+
AQEFAAOCAQ8AMIIBCgKCAQEA68bBZlvBT64suwLa61eob9roTVXlJQmB9tGvX2cn
4+
JacPNBx2h6W8Ow43doRLBRt32SopV06O1i2c0L84pRoliJcGrUhKyxAsVxVv11mF
5+
d4qg962TeYe3VawSKK2w83GNfVhjQFwuNEDuzJT0I7J0jH/uNclMxAtFZNkKVMA2
6+
GOK2c3Pib8zCmqITWAX5XXWUUvS0LWsASaBAEVh4R7StYbDl0L3VeiHCw6fKpdev
7+
Vfw5eDb+KuwMUOCPak0v31izEsXtcAyc7hxEZLfUMA+00zAdUENTC38GOJNTqirg
8+
0YmD+wxPdp3quWwkF/b831UTczAHkK7GP3swPjfciMN8nwIDAQABoAAwDQYJKoZI
9+
hvcNAQELBQADggEBAA6cpW7rdV0a8FDxEBfZoStJPEVUisqS5pUT43UjFJ7M55kC
10+
LGQ9Vl2Ua0nA4BwX5Le/IWVwwnhnnIJWvoPEbka9TWBVGujOPvt/WwBbEN2yHgGD
11+
QgFrIq/zOaFVj3J3EuJtIXL2jOylDK14j+2k4MN4OJobVtQhyUHpmRTPgq4EVJIw
12+
/PU6Lltgr2V4pTs3m9Ey2pIHF04HQIzr6Tt6MRJkKGEYWvOZlYuCbXA5bPLMyq5g
13+
rs0kC1DMF5C3VsBND8oGQt0ULbc2AQy6AFJegdD/ZT+d4eeh+ejymc0nmB+kbxaM
14+
tAxp2yTsRKUsGu7TBeMY1DxoP1xG5lAHkGznESg=
15+
-----END CERTIFICATE REQUEST-----
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDrxsFmW8FPriy7
3+
AtrrV6hv2uhNVeUlCYH20a9fZyclpw80HHaHpbw7Djd2hEsFG3fZKilXTo7WLZzQ
4+
vzilGiWIlwatSErLECxXFW/XWYV3iqD3rZN5h7dVrBIorbDzcY19WGNAXC40QO7M
5+
lPQjsnSMf+41yUzEC0Vk2QpUwDYY4rZzc+JvzMKaohNYBflddZRS9LQtawBJoEAR
6+
WHhHtK1hsOXQvdV6IcLDp8ql169V/Dl4Nv4q7AxQ4I9qTS/fWLMSxe1wDJzuHERk
7+
t9QwD7TTMB1QQ1MLfwY4k1OqKuDRiYP7DE92neq5bCQX9vzfVRNzMAeQrsY/ezA+
8+
N9yIw3yfAgMBAAECggEAG+dVD976al/ehbAepkKkub7fxk33mrdc17qqNYbDlcux
9+
2297lwMw2zu2qa1EzvvDZoKnC4ujEPWrUkiHP4Ga1pGqeyCL+tX/rBC/60Mk2L3M
10+
iMjUpB9BPdTpqJch0uCUp7R/DpNk7nnkKSHUdlMSQxHdkyUEk6ESheRqj2wuGtSj
11+
zVjXqUQa1yUiD1RZsg+o1v0Jk1bPvljMAblWslD4fBicX82MslExkuG4Kv411hss
12+
EBfkbGAQAAVHGiQijaiJ1nuwuiiqHNgaNRUZSqnIZm6+TGCbCDbXjzRIVBzMEvfd
13+
kB1DmpmxpzsVMDN6CK+RSpZXOgq2yTwcYAAbR5NHUQKBgQD/87ZGygLCHxzlj7T6
14+
MpinZq8QEoB4OCqPn8gSvWI237U1Kr32KxjPHSHBu5DPDPNy6bf/upha3kqJ+53c
15+
KVy3vZ9rEfkkXO9+5lNmlRWzG3+2TavH0SuihMQa1rK1aReyaDNG4xN8l9JxrVjx
16+
iKo/lTdBKpSZjxC11mbbWBTPEQKBgQDr0hMoxazSMcCxYO2WXYVejVyiEueoQqSQ
17+
BSDBqRKbv0+Gt5geM1dPxInRYCPJDhzgTbTvp33NSnl9LkhIBy/g2Z/jKZxxQB8f
18+
LQiN+yoja6kYTagBfogCLHObdPl/VV5/hBEaffeM94KLwGfOxGXbWUzA/sdoHjxc
19+
EcD/ncrwrwKBgQDw3H7WxPmthiviV8cegAip1/a8cDzXZTugJuPXxsKrEwBqxQs4
20+
ojvZg/elYYYXYn+izxBpJkaDlJaenNtkOMRY4Kgp0SMcthxm1gb8DSX7g9A+VX9n
21+
LY8bhEcrXomUMA6txGMkvUI0SIcwlMmTmmFkLl5uA80NaNV32Qi4N351kQKBgQCp
22+
/Ic1B7D430ZAVldM4WMG8i1I4wm73zYSXq/rCT3RqQjhWiw78NRKOqkBlSSWhCbK
23+
hRkc+4YSWlHSq28NBKk9koHPVKphdFA6v9J/zgHlAHEmhKvLT/MoZfR7pclHQTlZ
24+
/8/4Yb71DWE77dimUin+AJP0NnN1GP53e5C8cXjdHwKBgCeCtZtNpj0O2dt4s6CC
25+
392etUExPNvV5vyLfAlgTGI9SDPHApxomeu4wmsdBn8pIKY4apdP7MDXRgeU85Ql
26+
DNOFDngldtgzvTS7PjyJ3JORqDdzidKnmQ0YLlLvzdoD1xQtI+YIkZoMB6dgugfC
27+
tG/1B7aaPnbRlHz98DJBpEk4
28+
-----END PRIVATE KEY-----

clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpProto.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public class ClickHouseHttpProto {
4646

4747
public static final String HEADER_DB_PASSWORD = "X-ClickHouse-Key";
4848

49+
public static final String HEADER_SSL_CERT_AUTH = "x-clickhouse-ssl-certificate-auth";
50+
4951
/**
5052
* Query parameter to specify the query ID.
5153
*/

client-v2/src/main/java/com/clickhouse/client/api/Client.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,17 @@ public Builder setAccessToken(String accessToken) {
320320
return this;
321321
}
322322

323+
/**
324+
* Makes client to use SSL Client Certificate to authenticate with server.
325+
* Client certificate should be set as well. {@link Client.Builder#setClientCertificate(String)}
326+
* @param useSSLAuthentication
327+
* @return
328+
*/
329+
public Builder useSSLAuthentication(boolean useSSLAuthentication) {
330+
this.configuration.put("ssl_authentication", String.valueOf(useSSLAuthentication));
331+
return this;
332+
}
333+
323334
/**
324335
* Configures client to use build-in connection pool
325336
* @param enable - if connection pool should be enabled
@@ -854,12 +865,24 @@ public Client build() {
854865
throw new IllegalArgumentException("At least one endpoint is required");
855866
}
856867
// check if username and password are empty. so can not initiate client?
857-
if (!this.configuration.containsKey("access_token") && (!this.configuration.containsKey("user") || !this.configuration.containsKey("password"))) {
858-
throw new IllegalArgumentException("Username and password are required");
868+
if (!this.configuration.containsKey("access_token") &&
869+
(!this.configuration.containsKey("user") || !this.configuration.containsKey("password")) &&
870+
!MapUtils.getFlag(this.configuration, "ssl_authentication")) {
871+
throw new IllegalArgumentException("Username and password (or access token, or SSL authentication) are required");
872+
}
873+
874+
if (this.configuration.containsKey("ssl_authentication") &&
875+
(this.configuration.containsKey("password") || this.configuration.containsKey("access_token"))) {
876+
throw new IllegalArgumentException("Only one of password, access token or SSL authentication can be used per client.");
877+
}
878+
879+
if (this.configuration.containsKey("ssl_authentication") &&
880+
!this.configuration.containsKey(ClickHouseClientOption.SSL_CERTIFICATE.getKey())) {
881+
throw new IllegalArgumentException("SSL authentication requires a client certificate");
859882
}
860883

861-
if (this.configuration.containsKey(ClickHouseClientOption.TRUST_STORE) &&
862-
this.configuration.containsKey(ClickHouseClientOption.SSL_CERTIFICATE)) {
884+
if (this.configuration.containsKey(ClickHouseClientOption.TRUST_STORE.getKey()) &&
885+
this.configuration.containsKey(ClickHouseClientOption.SSL_CERTIFICATE.getKey())) {
863886
throw new IllegalArgumentException("Trust store and certificates cannot be used together");
864887
}
865888

client-v2/src/main/java/com/clickhouse/client/api/command/CommandResponse.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import com.clickhouse.client.api.metrics.ServerMetrics;
66
import com.clickhouse.client.api.query.QueryResponse;
77

8-
public class CommandResponse{
8+
public class CommandResponse implements AutoCloseable {
99

1010
private final QueryResponse response;
1111

@@ -71,4 +71,9 @@ public long getWrittenBytes() {
7171
public long getServerTime() {
7272
return response.getServerTime();
7373
}
74+
75+
@Override
76+
public void close() throws Exception {
77+
response.close();
78+
}
7479
}

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -384,18 +384,19 @@ public ClassicHttpResponse executeRequest(ClickHouseNode server, Map<String, Obj
384384

385385
private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String, Object> requestConfig) {
386386
req.addHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE.getMimeType());
387-
if (requestConfig != null) {
388-
if (requestConfig.containsKey(ClickHouseClientOption.FORMAT.getKey())) {
389-
req.addHeader(ClickHouseHttpProto.HEADER_FORMAT, requestConfig.get(ClickHouseClientOption.FORMAT.getKey()));
390-
}
391-
if (requestConfig.containsKey(ClickHouseClientOption.QUERY_ID.getKey())) {
392-
req.addHeader(ClickHouseHttpProto.HEADER_QUERY_ID, requestConfig.get(ClickHouseClientOption.QUERY_ID.getKey()).toString());
393-
}
387+
if (requestConfig.containsKey(ClickHouseClientOption.FORMAT.getKey())) {
388+
req.addHeader(ClickHouseHttpProto.HEADER_FORMAT, requestConfig.get(ClickHouseClientOption.FORMAT.getKey()));
389+
}
390+
if (requestConfig.containsKey(ClickHouseClientOption.QUERY_ID.getKey())) {
391+
req.addHeader(ClickHouseHttpProto.HEADER_QUERY_ID, requestConfig.get(ClickHouseClientOption.QUERY_ID.getKey()).toString());
394392
}
395393
req.addHeader(ClickHouseHttpProto.HEADER_DATABASE, chConfig.get(ClickHouseClientOption.DATABASE.getKey()));
396394
req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, chConfig.get(ClickHouseDefaults.USER.getKey()));
397-
req.addHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD, chConfig.get(ClickHouseDefaults.PASSWORD.getKey()));
398-
395+
if (MapUtils.getFlag(chConfig, "ssl_authentication", false)) {
396+
req.addHeader(ClickHouseHttpProto.HEADER_SSL_CERT_AUTH, "on");
397+
} else {
398+
req.addHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD, chConfig.get(ClickHouseDefaults.PASSWORD.getKey()));
399+
}
399400
if (proxyAuthHeaderValue != null) {
400401
req.addHeader(HttpHeaders.PROXY_AUTHORIZATION, proxyAuthHeaderValue);
401402
}

client-v2/src/main/java/com/clickhouse/client/api/internal/MapUtils.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,21 @@ public static boolean getFlag(Map<String, String> map, String key) {
6868
throw new IllegalArgumentException("Invalid non-boolean value for the key '" + key + "': '" + val + "'");
6969
}
7070

71+
public static boolean getFlag(Map<String, String> map, String key, boolean defaultValue) {
72+
String val = map.get(key);
73+
if (val == null) {
74+
return defaultValue;
75+
}
76+
if (val.equalsIgnoreCase("true")) {
77+
return true;
78+
} else if (val.equalsIgnoreCase("false")) {
79+
return false;
80+
}
81+
82+
throw new IllegalArgumentException("Invalid non-boolean value for the key '" + key + "': '" + val + "'");
83+
}
84+
85+
7186
public static boolean getFlag(Map<String, ?> p1, Map<String, ?> p2, String key) {
7287
Object val = p1.get(key);
7388
if (val == null) {

client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.clickhouse.client.api.ConnectionInitiationException;
77
import com.clickhouse.client.api.ConnectionReuseStrategy;
88
import com.clickhouse.client.api.ServerException;
9+
import com.clickhouse.client.api.command.CommandResponse;
910
import com.clickhouse.client.api.enums.Protocol;
1011
import com.clickhouse.client.api.enums.ProxyType;
1112
import com.clickhouse.client.api.insert.InsertResponse;
@@ -42,6 +43,7 @@
4243
import java.util.concurrent.atomic.AtomicInteger;
4344

4445
import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
46+
import static org.junit.Assert.fail;
4547

4648
public class HttpTransportTests extends BaseIntegrationTest {
4749

@@ -414,6 +416,63 @@ public void testServerSettings() {
414416
Assert.fail("Unexpected exception", e);
415417
}
416418
}
419+
}
420+
421+
static {
422+
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "DEBUG");
423+
}
424+
@Test(groups = { "integration" })
425+
public void testSSLAuthentication() throws Exception {
426+
if (isCloud()) {
427+
return; // Current test is working only with local server because of self-signed certificates.
428+
}
429+
ClickHouseNode server = getSecureServer(ClickHouseProtocol.HTTP);
430+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), true)
431+
.setUsername("default")
432+
.setPassword("")
433+
.setRootCertificate("containers/clickhouse-server/certs/localhost.crt")
434+
.build()) {
435+
436+
try (CommandResponse resp = client.execute("DROP USER IF EXISTS some_user").get()) {
437+
}
438+
try (CommandResponse resp = client.execute("CREATE USER some_user IDENTIFIED WITH ssl_certificate CN 'some_user'").get()) {
439+
}
440+
}
417441

442+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), true)
443+
.useSSLAuthentication(true)
444+
.setUsername("some_user")
445+
.setRootCertificate("containers/clickhouse-server/certs/localhost.crt")
446+
.setClientCertificate("some_user.crt")
447+
.setClientKey("some_user.key")
448+
.compressServerResponse(false)
449+
.build()) {
450+
451+
try (QueryResponse resp = client.query("SELECT 1").get()) {
452+
Assert.assertEquals(resp.getReadRows(), 1);
453+
}
454+
}
455+
}
456+
457+
@Test(groups = { "integration" })
458+
public void testSSLAuthentication_invalidConfig() throws Exception {
459+
if (isCloud()) {
460+
return; // Current test is working only with local server because of self-signed certificates.
461+
}
462+
ClickHouseNode server = getSecureServer(ClickHouseProtocol.HTTP);
463+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), true)
464+
.useSSLAuthentication(true)
465+
.setUsername("some_user")
466+
.setPassword("s3cret")
467+
.setRootCertificate("containers/clickhouse-server/certs/localhost.crt")
468+
.setClientCertificate("some_user.crt")
469+
.setClientKey("some_user.key")
470+
.compressServerResponse(false)
471+
.build()) {
472+
fail("Expected exception");
473+
} catch (IllegalArgumentException e) {
474+
e.printStackTrace();
475+
Assert.assertTrue(e.getMessage().startsWith("Only one of password, access token or SSL authentication"));
476+
}
418477
}
419478
}

0 commit comments

Comments
 (0)