diff --git a/clickhouse-client/src/test/resources/README.md b/clickhouse-client/src/test/resources/README.md index 8ebbd1c97..0b20e768c 100644 --- a/clickhouse-client/src/test/resources/README.md +++ b/clickhouse-client/src/test/resources/README.md @@ -18,3 +18,11 @@ openssl x509 -req -in server.csr -CA myCA.crt -CAkey myCA.key -CAcreateserial -o openssl req -nodes -subj "/CN=me" -newkey rsa:2048 -keyout client.key -out client.csr openssl x509 -req -in client.csr -out client.crt -CAcreateserial -CA myCA.crt -CAkey myCA.key -days 36500 ``` + +### Some_user + +```bash +openssl req -nodes -subj "/CN=some_user" -newkey rsa:2048 -keyout some_user.key -out some_user.csr +openssl x509 -req -in some_user.csr -out some_user.crt -CAcreateserial -CA marsnet_ca.crt -CAkey marsnet_ca.key -days 36500 + +``` diff --git a/clickhouse-client/src/test/resources/some_user.crt b/clickhouse-client/src/test/resources/some_user.crt new file mode 100644 index 000000000..ab8ef73ec --- /dev/null +++ b/clickhouse-client/src/test/resources/some_user.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICtDCCAZwCFBbI6UQK2g1r8o4XRXu+9wvQBHmnMA0GCSqGSIb3DQEBCwUAMBcx +FTATBgNVBAMMDGxvY2FsaG9zdCBDQTAgFw0yNDEwMTAxNjM3MzhaGA8yMTI0MDkx +NjE2MzczOFowFDESMBAGA1UEAwwJc29tZV91c2VyMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA68bBZlvBT64suwLa61eob9roTVXlJQmB9tGvX2cnJacP +NBx2h6W8Ow43doRLBRt32SopV06O1i2c0L84pRoliJcGrUhKyxAsVxVv11mFd4qg +962TeYe3VawSKK2w83GNfVhjQFwuNEDuzJT0I7J0jH/uNclMxAtFZNkKVMA2GOK2 +c3Pib8zCmqITWAX5XXWUUvS0LWsASaBAEVh4R7StYbDl0L3VeiHCw6fKpdevVfw5 +eDb+KuwMUOCPak0v31izEsXtcAyc7hxEZLfUMA+00zAdUENTC38GOJNTqirg0YmD ++wxPdp3quWwkF/b831UTczAHkK7GP3swPjfciMN8nwIDAQABMA0GCSqGSIb3DQEB +CwUAA4IBAQB+1poCA9p/XyKf5jxnAkaZQzoRW+fNqZvz8Eld2gGLqw7ZZUiBW6zo +d4aCAeuNehw5zJEOf1ew5EZzdWYRdxXUarjs3HOSQalfYTS8HqNI19sgWYD6Zcx+ +sygJqswtplvPAB6phk9zyhQDLFNuJ8dp28xRgGuywYtVMnvLG1wapPf/fnqkRcOW +yTBS4BBvtmzKPzMMZl/qB4Ol/STgVphceMFmI71HQQFUPb56E5tAQ+m3fezjdAJ2 +gZ+/LsApHLwhEV0ZGyIe/MNx0nDrkfWYWa7BsqvG6uuxyPXxgXSQofNjJN+RahRm +oHREhAYRL40BS1F20aLRFRupzLJngLBh +-----END CERTIFICATE----- diff --git a/clickhouse-client/src/test/resources/some_user.csr b/clickhouse-client/src/test/resources/some_user.csr new file mode 100644 index 000000000..88faa4922 --- /dev/null +++ b/clickhouse-client/src/test/resources/some_user.csr @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICWTCCAUECAQAwFDESMBAGA1UEAwwJc29tZV91c2VyMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA68bBZlvBT64suwLa61eob9roTVXlJQmB9tGvX2cn +JacPNBx2h6W8Ow43doRLBRt32SopV06O1i2c0L84pRoliJcGrUhKyxAsVxVv11mF +d4qg962TeYe3VawSKK2w83GNfVhjQFwuNEDuzJT0I7J0jH/uNclMxAtFZNkKVMA2 +GOK2c3Pib8zCmqITWAX5XXWUUvS0LWsASaBAEVh4R7StYbDl0L3VeiHCw6fKpdev +Vfw5eDb+KuwMUOCPak0v31izEsXtcAyc7hxEZLfUMA+00zAdUENTC38GOJNTqirg +0YmD+wxPdp3quWwkF/b831UTczAHkK7GP3swPjfciMN8nwIDAQABoAAwDQYJKoZI +hvcNAQELBQADggEBAA6cpW7rdV0a8FDxEBfZoStJPEVUisqS5pUT43UjFJ7M55kC +LGQ9Vl2Ua0nA4BwX5Le/IWVwwnhnnIJWvoPEbka9TWBVGujOPvt/WwBbEN2yHgGD +QgFrIq/zOaFVj3J3EuJtIXL2jOylDK14j+2k4MN4OJobVtQhyUHpmRTPgq4EVJIw +/PU6Lltgr2V4pTs3m9Ey2pIHF04HQIzr6Tt6MRJkKGEYWvOZlYuCbXA5bPLMyq5g +rs0kC1DMF5C3VsBND8oGQt0ULbc2AQy6AFJegdD/ZT+d4eeh+ejymc0nmB+kbxaM +tAxp2yTsRKUsGu7TBeMY1DxoP1xG5lAHkGznESg= +-----END CERTIFICATE REQUEST----- diff --git a/clickhouse-client/src/test/resources/some_user.key b/clickhouse-client/src/test/resources/some_user.key new file mode 100644 index 000000000..df845ca94 --- /dev/null +++ b/clickhouse-client/src/test/resources/some_user.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDrxsFmW8FPriy7 +AtrrV6hv2uhNVeUlCYH20a9fZyclpw80HHaHpbw7Djd2hEsFG3fZKilXTo7WLZzQ +vzilGiWIlwatSErLECxXFW/XWYV3iqD3rZN5h7dVrBIorbDzcY19WGNAXC40QO7M +lPQjsnSMf+41yUzEC0Vk2QpUwDYY4rZzc+JvzMKaohNYBflddZRS9LQtawBJoEAR +WHhHtK1hsOXQvdV6IcLDp8ql169V/Dl4Nv4q7AxQ4I9qTS/fWLMSxe1wDJzuHERk +t9QwD7TTMB1QQ1MLfwY4k1OqKuDRiYP7DE92neq5bCQX9vzfVRNzMAeQrsY/ezA+ +N9yIw3yfAgMBAAECggEAG+dVD976al/ehbAepkKkub7fxk33mrdc17qqNYbDlcux +2297lwMw2zu2qa1EzvvDZoKnC4ujEPWrUkiHP4Ga1pGqeyCL+tX/rBC/60Mk2L3M +iMjUpB9BPdTpqJch0uCUp7R/DpNk7nnkKSHUdlMSQxHdkyUEk6ESheRqj2wuGtSj +zVjXqUQa1yUiD1RZsg+o1v0Jk1bPvljMAblWslD4fBicX82MslExkuG4Kv411hss +EBfkbGAQAAVHGiQijaiJ1nuwuiiqHNgaNRUZSqnIZm6+TGCbCDbXjzRIVBzMEvfd +kB1DmpmxpzsVMDN6CK+RSpZXOgq2yTwcYAAbR5NHUQKBgQD/87ZGygLCHxzlj7T6 +MpinZq8QEoB4OCqPn8gSvWI237U1Kr32KxjPHSHBu5DPDPNy6bf/upha3kqJ+53c +KVy3vZ9rEfkkXO9+5lNmlRWzG3+2TavH0SuihMQa1rK1aReyaDNG4xN8l9JxrVjx +iKo/lTdBKpSZjxC11mbbWBTPEQKBgQDr0hMoxazSMcCxYO2WXYVejVyiEueoQqSQ +BSDBqRKbv0+Gt5geM1dPxInRYCPJDhzgTbTvp33NSnl9LkhIBy/g2Z/jKZxxQB8f +LQiN+yoja6kYTagBfogCLHObdPl/VV5/hBEaffeM94KLwGfOxGXbWUzA/sdoHjxc +EcD/ncrwrwKBgQDw3H7WxPmthiviV8cegAip1/a8cDzXZTugJuPXxsKrEwBqxQs4 +ojvZg/elYYYXYn+izxBpJkaDlJaenNtkOMRY4Kgp0SMcthxm1gb8DSX7g9A+VX9n +LY8bhEcrXomUMA6txGMkvUI0SIcwlMmTmmFkLl5uA80NaNV32Qi4N351kQKBgQCp +/Ic1B7D430ZAVldM4WMG8i1I4wm73zYSXq/rCT3RqQjhWiw78NRKOqkBlSSWhCbK +hRkc+4YSWlHSq28NBKk9koHPVKphdFA6v9J/zgHlAHEmhKvLT/MoZfR7pclHQTlZ +/8/4Yb71DWE77dimUin+AJP0NnN1GP53e5C8cXjdHwKBgCeCtZtNpj0O2dt4s6CC +392etUExPNvV5vyLfAlgTGI9SDPHApxomeu4wmsdBn8pIKY4apdP7MDXRgeU85Ql +DNOFDngldtgzvTS7PjyJ3JORqDdzidKnmQ0YLlLvzdoD1xQtI+YIkZoMB6dgugfC +tG/1B7aaPnbRlHz98DJBpEk4 +-----END PRIVATE KEY----- diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpProto.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpProto.java index fe5be507f..ad385fde5 100644 --- a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpProto.java +++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpProto.java @@ -46,6 +46,8 @@ public class ClickHouseHttpProto { public static final String HEADER_DB_PASSWORD = "X-ClickHouse-Key"; + public static final String HEADER_SSL_CERT_AUTH = "x-clickhouse-ssl-certificate-auth"; + /** * Query parameter to specify the query ID. */ diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index f2d2f4ab2..086062475 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -320,6 +320,17 @@ public Builder setAccessToken(String accessToken) { return this; } + /** + * Makes client to use SSL Client Certificate to authenticate with server. + * Client certificate should be set as well. {@link Client.Builder#setClientCertificate(String)} + * @param useSSLAuthentication + * @return + */ + public Builder useSSLAuthentication(boolean useSSLAuthentication) { + this.configuration.put("ssl_authentication", String.valueOf(useSSLAuthentication)); + return this; + } + /** * Configures client to use build-in connection pool * @param enable - if connection pool should be enabled @@ -854,12 +865,24 @@ public Client build() { throw new IllegalArgumentException("At least one endpoint is required"); } // check if username and password are empty. so can not initiate client? - if (!this.configuration.containsKey("access_token") && (!this.configuration.containsKey("user") || !this.configuration.containsKey("password"))) { - throw new IllegalArgumentException("Username and password are required"); + if (!this.configuration.containsKey("access_token") && + (!this.configuration.containsKey("user") || !this.configuration.containsKey("password")) && + !MapUtils.getFlag(this.configuration, "ssl_authentication")) { + throw new IllegalArgumentException("Username and password (or access token, or SSL authentication) are required"); + } + + if (this.configuration.containsKey("ssl_authentication") && + (this.configuration.containsKey("password") || this.configuration.containsKey("access_token"))) { + throw new IllegalArgumentException("Only one of password, access token or SSL authentication can be used per client."); + } + + if (this.configuration.containsKey("ssl_authentication") && + !this.configuration.containsKey(ClickHouseClientOption.SSL_CERTIFICATE.getKey())) { + throw new IllegalArgumentException("SSL authentication requires a client certificate"); } - if (this.configuration.containsKey(ClickHouseClientOption.TRUST_STORE) && - this.configuration.containsKey(ClickHouseClientOption.SSL_CERTIFICATE)) { + if (this.configuration.containsKey(ClickHouseClientOption.TRUST_STORE.getKey()) && + this.configuration.containsKey(ClickHouseClientOption.SSL_CERTIFICATE.getKey())) { throw new IllegalArgumentException("Trust store and certificates cannot be used together"); } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/command/CommandResponse.java b/client-v2/src/main/java/com/clickhouse/client/api/command/CommandResponse.java index 90c3c1655..481edac4b 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/command/CommandResponse.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/command/CommandResponse.java @@ -5,7 +5,7 @@ import com.clickhouse.client.api.metrics.ServerMetrics; import com.clickhouse.client.api.query.QueryResponse; -public class CommandResponse{ +public class CommandResponse implements AutoCloseable { private final QueryResponse response; @@ -71,4 +71,9 @@ public long getWrittenBytes() { public long getServerTime() { return response.getServerTime(); } + + @Override + public void close() throws Exception { + response.close(); + } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java index cf7162eb4..b2402261c 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java @@ -384,18 +384,19 @@ public ClassicHttpResponse executeRequest(ClickHouseNode server, Map chConfig, Map requestConfig) { req.addHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE.getMimeType()); - if (requestConfig != null) { - if (requestConfig.containsKey(ClickHouseClientOption.FORMAT.getKey())) { - req.addHeader(ClickHouseHttpProto.HEADER_FORMAT, requestConfig.get(ClickHouseClientOption.FORMAT.getKey())); - } - if (requestConfig.containsKey(ClickHouseClientOption.QUERY_ID.getKey())) { - req.addHeader(ClickHouseHttpProto.HEADER_QUERY_ID, requestConfig.get(ClickHouseClientOption.QUERY_ID.getKey()).toString()); - } + if (requestConfig.containsKey(ClickHouseClientOption.FORMAT.getKey())) { + req.addHeader(ClickHouseHttpProto.HEADER_FORMAT, requestConfig.get(ClickHouseClientOption.FORMAT.getKey())); + } + if (requestConfig.containsKey(ClickHouseClientOption.QUERY_ID.getKey())) { + req.addHeader(ClickHouseHttpProto.HEADER_QUERY_ID, requestConfig.get(ClickHouseClientOption.QUERY_ID.getKey()).toString()); } req.addHeader(ClickHouseHttpProto.HEADER_DATABASE, chConfig.get(ClickHouseClientOption.DATABASE.getKey())); req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, chConfig.get(ClickHouseDefaults.USER.getKey())); - req.addHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD, chConfig.get(ClickHouseDefaults.PASSWORD.getKey())); - + if (MapUtils.getFlag(chConfig, "ssl_authentication", false)) { + req.addHeader(ClickHouseHttpProto.HEADER_SSL_CERT_AUTH, "on"); + } else { + req.addHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD, chConfig.get(ClickHouseDefaults.PASSWORD.getKey())); + } if (proxyAuthHeaderValue != null) { req.addHeader(HttpHeaders.PROXY_AUTHORIZATION, proxyAuthHeaderValue); } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/MapUtils.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/MapUtils.java index f4f7defb6..7358108e8 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/MapUtils.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/MapUtils.java @@ -68,6 +68,21 @@ public static boolean getFlag(Map map, String key) { throw new IllegalArgumentException("Invalid non-boolean value for the key '" + key + "': '" + val + "'"); } + public static boolean getFlag(Map map, String key, boolean defaultValue) { + String val = map.get(key); + if (val == null) { + return defaultValue; + } + if (val.equalsIgnoreCase("true")) { + return true; + } else if (val.equalsIgnoreCase("false")) { + return false; + } + + throw new IllegalArgumentException("Invalid non-boolean value for the key '" + key + "': '" + val + "'"); + } + + public static boolean getFlag(Map p1, Map p2, String key) { Object val = p1.get(key); if (val == null) { diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java index 471d0eea9..a2d4e581b 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -6,6 +6,7 @@ import com.clickhouse.client.api.ConnectionInitiationException; import com.clickhouse.client.api.ConnectionReuseStrategy; import com.clickhouse.client.api.ServerException; +import com.clickhouse.client.api.command.CommandResponse; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.enums.ProxyType; import com.clickhouse.client.api.insert.InsertResponse; @@ -42,6 +43,7 @@ import java.util.concurrent.atomic.AtomicInteger; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; +import static org.junit.Assert.fail; public class HttpTransportTests extends BaseIntegrationTest { @@ -414,6 +416,63 @@ public void testServerSettings() { Assert.fail("Unexpected exception", e); } } + } + + static { + System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "DEBUG"); + } + @Test(groups = { "integration" }) + public void testSSLAuthentication() throws Exception { + if (isCloud()) { + return; // Current test is working only with local server because of self-signed certificates. + } + ClickHouseNode server = getSecureServer(ClickHouseProtocol.HTTP); + try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), true) + .setUsername("default") + .setPassword("") + .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") + .build()) { + + try (CommandResponse resp = client.execute("DROP USER IF EXISTS some_user").get()) { + } + try (CommandResponse resp = client.execute("CREATE USER some_user IDENTIFIED WITH ssl_certificate CN 'some_user'").get()) { + } + } + try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), true) + .useSSLAuthentication(true) + .setUsername("some_user") + .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") + .setClientCertificate("some_user.crt") + .setClientKey("some_user.key") + .compressServerResponse(false) + .build()) { + + try (QueryResponse resp = client.query("SELECT 1").get()) { + Assert.assertEquals(resp.getReadRows(), 1); + } + } + } + + @Test(groups = { "integration" }) + public void testSSLAuthentication_invalidConfig() throws Exception { + if (isCloud()) { + return; // Current test is working only with local server because of self-signed certificates. + } + ClickHouseNode server = getSecureServer(ClickHouseProtocol.HTTP); + try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), true) + .useSSLAuthentication(true) + .setUsername("some_user") + .setPassword("s3cret") + .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") + .setClientCertificate("some_user.crt") + .setClientKey("some_user.key") + .compressServerResponse(false) + .build()) { + fail("Expected exception"); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + Assert.assertTrue(e.getMessage().startsWith("Only one of password, access token or SSL authentication")); + } } }