From 6ed0d52f5a31b6342cca71e9b4d4765ed22fc857 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 30 Mar 2026 14:10:19 -0700 Subject: [PATCH 1/5] Added core API for changing credentials --- .../com/clickhouse/client/api/Client.java | 73 +- .../client/api/ClientConfigProperties.java | 8 + .../api/internal/CredentialsManager.java | 76 ++ .../com/clickhouse/client/ClientTests.java | 1206 +++++++++-------- .../clickhouse/client/HttpTransportTests.java | 61 +- .../examples/client_v2/Authentication.java | 119 ++ .../examples/jdbc/Authentication.java | 102 ++ .../com/clickhouse/jdbc/DataSourceImpl.java | 8 +- .../jdbc/internal/JdbcConfiguration.java | 9 +- .../com/clickhouse/jdbc/ConnectionTest.java | 5 +- 10 files changed, 1039 insertions(+), 628 deletions(-) create mode 100644 client-v2/src/main/java/com/clickhouse/client/api/internal/CredentialsManager.java create mode 100644 examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Authentication.java create mode 100644 examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/Authentication.java 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 73ec21155..e116f3573 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 @@ -16,6 +16,7 @@ import com.clickhouse.client.api.insert.InsertResponse; import com.clickhouse.client.api.insert.InsertSettings; import com.clickhouse.client.api.internal.ClientStatisticsHolder; +import com.clickhouse.client.api.internal.CredentialsManager; import com.clickhouse.client.api.internal.HttpAPIClientHelper; import com.clickhouse.client.api.internal.MapUtils; import com.clickhouse.client.api.internal.TableSchemaParser; @@ -141,11 +142,13 @@ public class Client implements AutoCloseable { private final int retries; private LZ4Factory lz4Factory = null; private final Supplier queryIdGenerator; + private final CredentialsManager credentialsManager; private Client(Collection endpoints, Map configuration, ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy, Object metricsRegistry, Supplier queryIdGenerator) { - this.configuration = ClientConfigProperties.parseConfigMap(configuration); + this.configuration = new ConcurrentHashMap<>(ClientConfigProperties.parseConfigMap(configuration)); + this.credentialsManager = new CredentialsManager(this.configuration); this.readOnlyConfig = Collections.unmodifiableMap(configuration); this.metricsRegistry = metricsRegistry; this.queryIdGenerator = queryIdGenerator; @@ -364,8 +367,11 @@ public Builder setOption(String key, String value) { if (key.equals(ClientConfigProperties.PRODUCT_NAME.getKey())) { setClientName(value); } + if (key.equals(ClientConfigProperties.ACCESS_TOKEN.getKey())) { + setAccessToken(value); + } if (key.equals(ClientConfigProperties.BEARERTOKEN_AUTH.getKey())) { - useBearerTokenAuth(value); + setAccessToken(value); } return this; } @@ -393,13 +399,17 @@ public Builder setPassword(String password) { } /** - * Access token for authentication with server. Required for all operations. + * Preferred way to configure token-based authentication. * Same access token will be used for all endpoints. + * Internally it is sent as an HTTP Bearer token. * * @param accessToken - plain text access token */ + @SuppressWarnings("deprecation") public Builder setAccessToken(String accessToken) { this.configuration.put(ClientConfigProperties.ACCESS_TOKEN.getKey(), accessToken); + this.configuration.remove(ClientConfigProperties.BEARERTOKEN_AUTH.getKey()); + this.httpHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); return this; } @@ -1007,16 +1017,16 @@ public Builder setOptions(Map options) { } /** - * Specifies whether to use Bearer Authentication and what token to use. - * The token will be sent as is, so it should be encoded before passing to this method. + * Legacy HTTP-specific alias for {@link Builder#setAccessToken(String)}. + * Prefer using {@link Builder#setAccessToken(String)}. * * @param bearerToken - token to use * @return same instance of the builder */ + @Deprecated public Builder useBearerTokenAuth(String bearerToken) { // Most JWT libraries (https://jwt.io/libraries?language=Java) compact tokens in proper way - this.httpHeader(HttpHeaders.AUTHORIZATION, "Bearer " + bearerToken); - return this; + return setAccessToken(bearerToken); } /** @@ -1099,28 +1109,10 @@ public Client build() { if (this.endpoints.isEmpty()) { throw new IllegalArgumentException("At least one endpoint is required"); } - // check if username and password are empty. so can not initiate client? - boolean useSslAuth = MapUtils.getFlag(this.configuration, ClientConfigProperties.SSL_AUTH.getKey()); - boolean hasAccessToken = this.configuration.containsKey(ClientConfigProperties.ACCESS_TOKEN.getKey()); - boolean hasUser = this.configuration.containsKey(ClientConfigProperties.USER.getKey()); - boolean hasPassword = this.configuration.containsKey(ClientConfigProperties.PASSWORD.getKey()); - boolean customHttpHeaders = this.configuration.containsKey(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION)); - - if (!(useSslAuth || hasAccessToken || hasUser || hasPassword || customHttpHeaders)) { - throw new IllegalArgumentException("Username and password (or access token or SSL authentication or pre-define Authorization header) are required"); - } - - if (useSslAuth && (hasAccessToken || hasPassword)) { - throw new IllegalArgumentException("Only one of password, access token or SSL authentication can be used per client."); - } - if (useSslAuth && !this.configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) { - throw new IllegalArgumentException("SSL authentication requires a client certificate"); - } - - if (this.configuration.containsKey(ClientConfigProperties.SSL_TRUST_STORE.getKey()) && - this.configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) { - throw new IllegalArgumentException("Trust store and certificates cannot be used together"); + ClientMisconfigurationException authConfigException = CredentialsManager.validateAuthConfig(configuration); + if (authConfigException != null) { + throw authConfigException; } // Check timezone settings @@ -2153,8 +2145,28 @@ public Collection getDBRoles() { return unmodifiableDbRolesView; } + public void setCredentials(String username, String password) { + this.credentialsManager.setCredentials(username, password); + } + + /** + * Preferred runtime API to update token-based authentication. + * Internally it refreshes the HTTP Bearer token used by requests. + * + * @param accessToken - plain text access token + */ + public void setAccessToken(String accessToken) { + this.credentialsManager.setAccessToken(accessToken); + } + + /** + * Legacy HTTP-specific alias for {@link #setAccessToken(String)}. + * Prefer using {@link #setAccessToken(String)}. + * + * @param bearer - token to use + */ public void updateBearerToken(String bearer) { - this.configuration.put(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION), "Bearer " + bearer); + setAccessToken(bearer); } private Endpoint getNextAliveNode() { @@ -2170,8 +2182,7 @@ private Endpoint getNextAliveNode() { * @return request settings - merged client and operation settings */ private Map buildRequestSettings(Map opSettings) { - Map requestSettings = new HashMap<>(); - requestSettings.putAll(configuration); + Map requestSettings = credentialsManager.snapshot(); requestSettings.putAll(opSettings); return requestSettings; } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java index e548a90f9..892ad1367 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java @@ -125,6 +125,10 @@ public enum ClientConfigProperties { CLIENT_NETWORK_BUFFER_SIZE("client_network_buffer_size", Integer.class, "300000"), + /** + * Preferred client setting for token-based authentication like JWT and Oauth. + * For Http it is translated to Authorization Bearer header. + */ ACCESS_TOKEN("access_token", String.class), SSL_AUTH("ssl_authentication", Boolean.class, "false"), @@ -157,6 +161,10 @@ public Object parseValue(String value) { @Deprecated PRODUCT_NAME("product_name", String.class), + /** + * HTTP-specific alias for {@link ClientConfigProperties#ACCESS_TOKEN}. + * Prefer using {@link ClientConfigProperties#ACCESS_TOKEN}. + */ BEARERTOKEN_AUTH ("bearer_token", String.class), /** * Indicates that data provided for write operation is compressed by application. diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/CredentialsManager.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/CredentialsManager.java new file mode 100644 index 000000000..5f53caf51 --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/CredentialsManager.java @@ -0,0 +1,76 @@ +package com.clickhouse.client.api.internal; + +import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.ClientMisconfigurationException; +import org.apache.hc.core5.http.HttpHeaders; + +import java.util.HashMap; +import java.util.Map; + +/** + * Manages mutable authentication-related client settings. + */ +public class CredentialsManager { + private final Map configuration; + private final Object lock = new Object(); + + public CredentialsManager(Map configuration) { + this.configuration = configuration; + } + + public Map snapshot() { + synchronized (lock) { + return new HashMap<>(configuration); + } + } + + public void setCredentials(String username, String password) { + synchronized (lock) { + configuration.put(ClientConfigProperties.USER.getKey(), username); + configuration.put(ClientConfigProperties.PASSWORD.getKey(), password); + configuration.put(ClientConfigProperties.SSL_AUTH.getKey(), Boolean.FALSE); + configuration.remove(ClientConfigProperties.ACCESS_TOKEN.getKey()); + configuration.remove(ClientConfigProperties.BEARERTOKEN_AUTH.getKey()); + configuration.remove(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION)); + } + } + + public void setAccessToken(String accessToken) { + synchronized (lock) { + configuration.put(ClientConfigProperties.ACCESS_TOKEN.getKey(), accessToken); + configuration.put(ClientConfigProperties.SSL_AUTH.getKey(), Boolean.FALSE); + configuration.remove(ClientConfigProperties.BEARERTOKEN_AUTH.getKey()); + configuration.remove(ClientConfigProperties.USER.getKey()); + configuration.remove(ClientConfigProperties.PASSWORD.getKey()); + configuration.put(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION), "Bearer " + accessToken); + } + } + + public static ClientMisconfigurationException validateAuthConfig(Map configuration) { + // check if username and password are empty. so can not initiate client? + boolean useSslAuth = MapUtils.getFlag(configuration, ClientConfigProperties.SSL_AUTH.getKey()); + boolean hasAccessToken = configuration.containsKey(ClientConfigProperties.ACCESS_TOKEN.getKey()); + boolean hasUser = configuration.containsKey(ClientConfigProperties.USER.getKey()); + boolean hasPassword = configuration.containsKey(ClientConfigProperties.PASSWORD.getKey()); + boolean customHttpHeaders = configuration.containsKey(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION)); + + if (!(useSslAuth || hasAccessToken || hasUser || hasPassword || customHttpHeaders)) { + return new ClientMisconfigurationException("Username and password (or access token or SSL authentication or pre-define Authorization header) are required"); + } + + if (useSslAuth && (hasAccessToken || hasPassword)) { + return new ClientMisconfigurationException("Only one of password, access token or SSL authentication can be used per client."); + } + + if (useSslAuth && !configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) { + return new ClientMisconfigurationException("SSL authentication requires a client certificate"); + } + + if (configuration.containsKey(ClientConfigProperties.SSL_TRUST_STORE.getKey()) && + configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) { + return new ClientMisconfigurationException("Trust store and certificates cannot be used together"); + } + + return null; + } +} diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index d63f9b2cb..22f1b6875 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -1,586 +1,620 @@ -package com.clickhouse.client; - -import com.clickhouse.client.api.Client; -import com.clickhouse.client.api.ClientConfigProperties; -import com.clickhouse.client.api.ClientException; -import com.clickhouse.client.api.ClientFaultCause; -import com.clickhouse.client.api.ClientMisconfigurationException; -import com.clickhouse.client.api.ConnectionReuseStrategy; -import com.clickhouse.client.api.ServerException; -import com.clickhouse.client.api.enums.Protocol; -import com.clickhouse.client.api.insert.InsertSettings; -import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream; -import com.clickhouse.client.api.internal.ServerSettings; -import com.clickhouse.client.api.internal.ValidationUtils; -import com.clickhouse.client.api.metadata.DefaultColumnToMethodMatchingStrategy; -import com.clickhouse.client.api.query.GenericRecord; -import com.clickhouse.client.api.query.QueryResponse; -import com.clickhouse.client.api.query.QuerySettings; -import com.clickhouse.client.api.query.Records; -import com.clickhouse.client.config.ClickHouseClientOption; -import com.clickhouse.data.ClickHouseFormat; -import com.clickhouse.data.ClickHouseVersion; -import org.apache.commons.lang3.RandomStringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; -import org.testng.util.Strings; - -import java.io.ByteArrayInputStream; -import java.net.ConnectException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Queue; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; - -import static java.time.temporal.ChronoUnit.MILLIS; -import static java.time.temporal.ChronoUnit.SECONDS; -import static org.testng.AssertJUnit.fail; - -public class ClientTests extends BaseIntegrationTest { - private static final Logger LOGGER = LoggerFactory.getLogger(ClientTests.class); - - @Test(groups = {"integration"}, dataProvider = "secureClientProvider") - public void testAddSecureEndpoint(Client client) { - if (isCloud()) { - return; // will fail in other tests - } - try { - Optional genericRecord = client - .queryAll("SELECT hostname()").stream().findFirst(); - Assert.assertTrue(genericRecord.isPresent()); - } catch (ClientException e) { - e.printStackTrace(); - if (e.getCause().getCause() instanceof ClickHouseException) { - Exception cause = (Exception) e.getCause().getCause().getCause(); - Assert.assertTrue(cause instanceof ConnectException); - // TODO: correct when SSL support is fully implemented. - Assert.assertTrue(cause.getMessage() - .startsWith("HTTP request failed: PKIX path building failed")); - return; - } - Assert.fail(e.getMessage()); - } finally { - client.close(); - } - } - - @DataProvider(name = "secureClientProvider") - public static Object[][] secureClientProvider() throws Exception { - ClickHouseNode node = ClickHouseServerForTest.getClickHouseNode(ClickHouseProtocol.HTTP, - true, ClickHouseNode.builder() - .addOption(ClickHouseClientOption.SSL_MODE.getKey(), "none") - .addOption(ClickHouseClientOption.SSL.getKey(), "true").build()); - - Client client1; - Client client2; - try { - client1 = new Client.Builder() - .addEndpoint("https://" + node.getHost() + ":" + node.getPort()) - .setUsername("default") - .setPassword(ClickHouseServerForTest.getPassword()) - .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") - .build(); - - client2 = new Client.Builder() - .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), true) - .setUsername("default") - .setPassword(ClickHouseServerForTest.getPassword()) - .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") - .setClientKey("some_user.key") - .setClientCertificate("some_user.crt") - .build(); - - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - - - return new Client[][]{ - { - client1 - }, - { - client2 - } - }; - } - - @Test(groups = {"integration"}) - public void testRawSettings() { - try (Client client = newClient().build()) { - - client.execute("SELECT 1"); - - QuerySettings querySettings = new QuerySettings(); - querySettings.serverSetting("session_timezone", "Europe/Zurich"); - - try (Records response = - client.queryRecords("SELECT timeZone(), serverTimeZone()", querySettings).get(10, TimeUnit.SECONDS)) { - - response.forEach(record -> { - Assert.assertEquals("Europe/Zurich", record.getString(1)); - Assert.assertEquals("UTC", record.getString(2)); - }); - } catch (Exception e) { - Assert.fail(e.getMessage()); - } - } - } - - @Test(groups = {"integration"}) - public void testCustomSettings() { - if (isCloud()) { - return; // no custom parameters on cloud instance - } - final String CLIENT_OPTION = "custom_client_option"; // prefix should be known from server config - try (Client client = newClient().serverSetting(CLIENT_OPTION, "opt1").build()) { - - final List clientOption = client.queryAll("SELECT getSetting({option_name:String})", - Collections.singletonMap("option_name", CLIENT_OPTION)); - - Assert.assertEquals(clientOption.get(0).getString(1), "opt1"); - - QuerySettings querySettings = new QuerySettings(); - querySettings.serverSetting(CLIENT_OPTION, "opt2"); - - final List requestOption = client.queryAll("SELECT getSetting({option_name:String})", - Collections.singletonMap("option_name", CLIENT_OPTION), querySettings); - - Assert.assertEquals(requestOption.get(0).getString(1), "opt2"); - } - } - - @Test(groups = {"integration"}) - public void testPing() { - try (Client client = newClient().build()) { - Assert.assertTrue(client.ping()); - } - } - - @Test(groups = {"integration"}) - public void testPingUnpooled() { - try (Client client = newClient().enableConnectionPool(false).build()) { - Assert.assertTrue(client.ping()); - } - } - - @Test(groups = {"integration"}) - public void testPingFailure() { - try (Client client = new Client.Builder() - .addEndpoint("http://localhost:12345") - .setUsername("default") - .setPassword("") - .build()) { - Assert.assertFalse(client.ping(TimeUnit.SECONDS.toMillis(20))); - } - } - - @Test(groups = {"integration"}) - public void testPingAsync() { - try (Client client = newClient().useAsyncRequests(true).build()) { - Assert.assertTrue(client.ping()); - } - } - - @Test(groups = {"integration"}) - public void testSetOptions() { - Map options = new HashMap<>(); - String productName = "my product_name (version 1.0)"; - options.put(ClickHouseClientOption.PRODUCT_NAME.getKey(), productName); - try (Client client = newClient() - .setOptions(options).build()) { - - Assert.assertEquals(client.getConfiguration().get(ClickHouseClientOption.PRODUCT_NAME.getKey()), productName); - } - } - - @Test(groups = {"integration"}) - public void testProvidedExecutor() throws Exception { - - ExecutorService executorService = Executors.newSingleThreadExecutor(); - try (Client client = newClient().useAsyncRequests(true).setSharedOperationExecutor(executorService).build()) { - QueryResponse response = client.query("SELECT 1").get(); - response.getMetrics(); - } catch (Exception e) { - Assert.fail("unexpected exception", e); - } - - AtomicBoolean flag = new AtomicBoolean(true); - executorService.submit(() -> flag.compareAndSet(true, false)); - executorService.shutdown(); - executorService.awaitTermination(10, TimeUnit.SECONDS); - - Assert.assertFalse(flag.get()); - } - - @Test(groups = {"integration"}) - public void testLoadingServerContext() throws Exception { - long start = System.nanoTime(); - try (Client client = newClient().build()) { - long initTime = (System.nanoTime() - start) / 1_000_000; - Assert.assertTrue(initTime < 100); - Assert.assertEquals(client.getServerVersion(), "unknown"); - client.loadServerInfo(); - Assert.assertNotNull(client.getServerVersion()); - } - } - - @Test(groups = {"integration"}) - public void testDisableNative() { - try (Client client = newClient().disableNativeCompression(true).build()) { - Assert.assertTrue(client.toString().contains("JavaSafe") || client.toString().contains("JavaUnsafe")); - } - } - - @Test(groups = {"integration"}) - public void testDefaultSettings() { - try (Client client = new Client.Builder().setUsername("default").setPassword("secret") - .addEndpoint("http://localhost:8123").build()) { - Map config = client.getConfiguration(); - for (ClientConfigProperties p : ClientConfigProperties.values()) { - if (p.getDefaultValue() != null) { - Assert.assertTrue(config.containsKey(p.getKey()), "Default value should be set for " + p.getKey()); - Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); - } - } - Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. - } - - try (Client client = new Client.Builder() - .setUsername("default") - .setPassword("secret") - .addEndpoint("http://localhost:8123") - .setDefaultDatabase("mydb") - .setExecutionTimeout(10, MILLIS) - .setLZ4UncompressedBufferSize(300_000) - .disableNativeCompression(true) - .useServerTimeZone(false) - .setServerTimeZone("America/Los_Angeles") - .useTimeZone("America/Los_Angeles") - .useAsyncRequests(true) - .setMaxConnections(330) - .setConnectionRequestTimeout(20, SECONDS) - .setConnectionReuseStrategy(ConnectionReuseStrategy.LIFO) - .enableConnectionPool(false) - .setConnectionTTL(30, SECONDS) - .retryOnFailures(ClientFaultCause.NoHttpResponse) - .setClientNetworkBufferSize(500_000) - .setMaxRetries(10) - .useHTTPBasicAuth(false) - .compressClientRequest(true) - .compressServerResponse(false) - .useHttpCompression(true) - .appCompressedData(true) - .setSocketTimeout(20, SECONDS) - .setSocketRcvbuf(100000) - .setSocketSndbuf(100000) - .build()) { - Map config = client.getConfiguration(); - Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added. - Assert.assertEquals(config.get(ClientConfigProperties.DATABASE.getKey()), "mydb"); - Assert.assertEquals(config.get(ClientConfigProperties.MAX_EXECUTION_TIME.getKey()), "10"); - Assert.assertEquals(config.get(ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getKey()), "300000"); - Assert.assertEquals(config.get(ClientConfigProperties.DISABLE_NATIVE_COMPRESSION.getKey()), "true"); - Assert.assertEquals(config.get(ClientConfigProperties.USE_SERVER_TIMEZONE.getKey()), "false"); - Assert.assertEquals(config.get(ClientConfigProperties.SERVER_TIMEZONE.getKey()), "America/Los_Angeles"); - Assert.assertEquals(config.get(ClientConfigProperties.ASYNC_OPERATIONS.getKey()), "true"); - Assert.assertEquals(config.get(ClientConfigProperties.HTTP_MAX_OPEN_CONNECTIONS.getKey()), "330"); - Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_REQUEST_TIMEOUT.getKey()), "20000"); - Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_REUSE_STRATEGY.getKey()), "LIFO"); - Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_POOL_ENABLED.getKey()), "false"); - Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_TTL.getKey()), "30000"); - Assert.assertEquals(config.get(ClientConfigProperties.CLIENT_RETRY_ON_FAILURE.getKey()), "NoHttpResponse"); - Assert.assertEquals(config.get(ClientConfigProperties.CLIENT_NETWORK_BUFFER_SIZE.getKey()), "500000"); - Assert.assertEquals(config.get(ClientConfigProperties.RETRY_ON_FAILURE.getKey()), "10"); - Assert.assertEquals(config.get(ClientConfigProperties.HTTP_USE_BASIC_AUTH.getKey()), "false"); - Assert.assertEquals(config.get(ClientConfigProperties.COMPRESS_CLIENT_REQUEST.getKey()), "true"); - Assert.assertEquals(config.get(ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getKey()), "false"); - Assert.assertEquals(config.get(ClientConfigProperties.USE_HTTP_COMPRESSION.getKey()), "true"); - Assert.assertEquals(config.get(ClientConfigProperties.APP_COMPRESSED_DATA.getKey()), "true"); - Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()), "20000"); - Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_RCVBUF_OPT.getKey()), "100000"); - Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_SNDBUF_OPT.getKey()), "100000"); - } - } - - @Test(groups = {"integration"}) - public void testWithOldDefaults() { - try (Client client = new Client.Builder() - .setUsername("default") - .setPassword("seceret") - .addEndpoint("http://localhost:8123") - .setDefaultDatabase("default") - .setExecutionTimeout(0, MILLIS) - .setLZ4UncompressedBufferSize(ClickHouseLZ4OutputStream.UNCOMPRESSED_BUFF_SIZE) - .disableNativeCompression(false) - .useServerTimeZone(true) - .setServerTimeZone("UTC") - .useAsyncRequests(false) - .setMaxConnections(10) - .setConnectionRequestTimeout(10, SECONDS) - .setConnectionReuseStrategy(ConnectionReuseStrategy.FIFO) - .enableConnectionPool(true) - .setConnectionTTL(-1, MILLIS) - .retryOnFailures(ClientFaultCause.NoHttpResponse, ClientFaultCause.ConnectTimeout, - ClientFaultCause.ConnectionRequestTimeout, ClientFaultCause.ServerRetryable) - .setClientNetworkBufferSize(300_000) - .setMaxRetries(3) - .allowBinaryReaderToReuseBuffers(false) - .columnToMethodMatchingStrategy(DefaultColumnToMethodMatchingStrategy.INSTANCE) - .useHTTPBasicAuth(true) - .compressClientRequest(false) - .compressServerResponse(true) - .useHttpCompression(false) - .appCompressedData(false) - .setSocketTimeout(0, SECONDS) - .setSocketRcvbuf(804800) - .setSocketSndbuf(804800) - .build()) { - Map config = client.getConfiguration(); - for (ClientConfigProperties p : ClientConfigProperties.values()) { - if (p.getDefaultValue() != null) { - Assert.assertTrue(config.containsKey(p.getKey()), "Default value should be set for " + p.getKey()); - Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); - } - } - Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. - } - } - - @DataProvider(name = "sessionRoles") - private static Object[][] sessionRoles() { - return new Object[][]{ - {new String[]{"ROL1", "ROL2"}}, - {new String[]{"ROL1", "ROL2"}}, - {new String[]{"ROL1", "ROL2"}}, - {new String[]{"ROL1", "ROL2,☺"}}, - {new String[]{"ROL1", "ROL2"}}, - }; - } - - @Test(groups = {"integration"}, dataProvider = "sessionRoles") - public void testOperationCustomRoles(String[] roles) throws Exception { - if (isVersionMatch("(,24.3]", newClient().build())) { - return; - } - - String password = "^1A" + RandomStringUtils.random(12, true, true) + "3b$"; - final String rolesList = "\"" + Strings.join("\",\"", roles) + "\""; - try (Client client = newClient().build()) { - client.execute("DROP ROLE IF EXISTS " + rolesList).get().close(); - client.execute("CREATE ROLE " + rolesList).get().close(); - client.execute("DROP USER IF EXISTS some_user").get().close(); - client.execute("CREATE USER some_user IDENTIFIED BY '" + password + "'").get().close(); - client.execute("GRANT " + rolesList + " TO some_user").get().close(); - } - - try (Client userClient = newClient().setUsername("some_user").setPassword(password).build()) { - QuerySettings settings = new QuerySettings().setDBRoles(Arrays.asList(roles)); - List resp = userClient.queryAll("SELECT currentRoles()", settings); - Set roleSet = new HashSet<>(Arrays.asList(roles)); - Set currentRoles = new HashSet (resp.get(0).getList(1)); - Assert.assertEquals(currentRoles, roleSet, "Roles " + roleSet + " not found in " + currentRoles); - } - } - - @DataProvider(name = "clientSessionRoles") - private static Object[][] clientSessionRoles() { - return new Object[][]{ - {new String[]{"ROL1", "ROL2"}}, - {new String[]{"ROL1", "ROL2,☺"}}, - }; - } - @Test(groups = {"integration"}, dataProvider = "clientSessionRoles") - public void testClientCustomRoles(String[] roles) throws Exception { - if (isVersionMatch("(,24.3]", newClient().build())) { - return; - } - - String password = "^1A" + RandomStringUtils.random(12, true, true) + "3B$"; - final String rolesList = "\"" + Strings.join("\",\"", roles) + "\""; - try (Client client = newClient().build()) { - client.execute("DROP ROLE IF EXISTS " + rolesList).get().close(); - client.execute("CREATE ROLE " + rolesList).get().close(); - client.execute("DROP USER IF EXISTS some_user").get().close(); - client.execute("CREATE USER some_user IDENTIFIED WITH sha256_password BY '" + password + "'").get().close(); - client.execute("GRANT " + rolesList + " TO some_user").get().close(); - } - - try (Client userClient = newClient().setUsername("some_user").setPassword(password).build()) { - userClient.setDBRoles(Arrays.asList(roles)); - List resp = userClient.queryAll("SELECT currentRoles()"); - Set roleSet = new HashSet<>(Arrays.asList(roles)); - Set currentRoles = new HashSet (resp.get(0).getList(1)); - Assert.assertEquals(currentRoles, roleSet, "Roles " + roleSet + " not found in " + currentRoles); - } - } - - - @Test(groups = {"integration"}) - public void testLogComment() throws Exception { - - String logComment = "Test log comment"; - QuerySettings settings = new QuerySettings() - .setQueryId(UUID.randomUUID().toString()) - .logComment(logComment); - - try (Client client = newClient().build()) { - - try (QueryResponse response = client.query("SELECT 1", settings).get()) { - Assert.assertNotNull(response.getQueryId()); - Assert.assertTrue(response.getQueryId().startsWith(settings.getQueryId())); - } - - client.execute("SYSTEM FLUSH LOGS").get().close(); - - List logRecords = client.queryAll("SELECT query_id, log_comment FROM clusterAllReplicas('default', system.query_log) WHERE query_id = '" + settings.getQueryId() + "'"); - Assert.assertEquals(logRecords.get(0).getString("query_id"), settings.getQueryId()); - Assert.assertEquals(logRecords.get(0).getString("log_comment"), logComment); - } - } - - @Test(groups = {"integration"}) - public void testServerSettings() throws Exception { - try (Client client = newClient().build()) { - client.execute("DROP TABLE IF EXISTS server_settings_test_table"); - client.execute("CREATE TABLE server_settings_test_table (v Float) Engine MergeTree ORDER BY ()"); - - final String queryId = UUID.randomUUID().toString(); - InsertSettings insertSettings = new InsertSettings() - .setQueryId(queryId) - .serverSetting(ServerSettings.ASYNC_INSERT, "1") - .serverSetting(ServerSettings.WAIT_ASYNC_INSERT, "1") - .serverSetting(ServerSettings.INPUT_FORMAT_BINARY_READ_JSON_AS_STRING, "1"); - - String csvData = "0.33\n0.44\n0.55\n"; - client.insert("server_settings_test_table", new ByteArrayInputStream(csvData.getBytes()), ClickHouseFormat.CSV, insertSettings).get().close(); - - client.execute("SYSTEM FLUSH LOGS").get().close(); - - List logRecords = client.queryAll("SELECT Settings, ProfileEvents['AsyncInsertQuery'] as was_async FROM clusterAllReplicas('default', system.query_log) WHERE query_id = '" + queryId + "' AND type = 'QueryFinish'"); - - GenericRecord record = logRecords.get(0); - Assert.assertTrue(record.getBoolean("was_async")); - String settings = record.getString(record.getSchema().nameToColumnIndex("Settings")); - Assert.assertTrue(settings.contains("input_format_binary_read_json_as_string=1")); -// Assert.assertTrue(settings.contains(ServerSettings.ASYNC_INSERT + "=1")); // async settings are not reflected in query log any more -// Assert.assertTrue(settings.contains(ServerSettings.WAIT_ASYNC_INSERT + "=1")); // uncomment after server fix - } - } - - @Test(groups = {"integration"}) - public void testInvalidEndpoint() { - - try { - new Client.Builder().addEndpoint("http://localhost/default"); - fail("Exception expected"); - } catch (ValidationUtils.SettingsValidationException e) { - Assert.assertTrue(e.getMessage().contains("port")); - } - } - - @Test(groups = {"integration"}) - public void testUnknownClientSettings() throws Exception { - try (Client client = newClient().setOption("unknown_setting", "value").build()) { - Assert.fail("Exception expected"); - } catch (Exception ex) { - Assert.assertTrue(ex instanceof ClientMisconfigurationException); - Assert.assertTrue(ex.getMessage().contains("unknown_setting")); - } - - try (Client client = newClient().setOption(ClientConfigProperties.IGNORE_UNKNOWN_CONFIG_KEY, "true").setOption("unknown_setting", "value").build()) { - Assert.assertTrue(client.ping()); - } - - try (Client client = newClient().setOption(ClientConfigProperties.SERVER_SETTING_PREFIX + "unknown_setting", "value").build()) { - try { - client.execute("SELECT 1"); - Assert.fail("Exception expected"); - } catch (ServerException e) { - Assert.assertEquals(e.getCode(), ServerException.UNKNOWN_SETTING); - } - } - - try (Client client = newClient().setOption(ClientConfigProperties.HTTP_HEADER_PREFIX + "unknown_setting", "value").build()) { - Assert.assertTrue(client.ping()); - } - } - - @Test(groups = {"integration"}) - public void testInvalidConfig() { - try { - newClient().setOption(ClientConfigProperties.CUSTOM_SETTINGS_PREFIX.getKey(), "").build(); - Assert.fail("exception expected"); - } catch (ClientException e) { - Assert.assertTrue(e.getMessage().contains(ClientConfigProperties.CUSTOM_SETTINGS_PREFIX.getKey())); - } - } - - @Test(groups = {"integration"}) - public void testQueryIdGenerator() throws Exception { - final String queryId = UUID.randomUUID().toString(); - Supplier constantQueryIdSupplier = () -> queryId; - - // check getting same UUID - for (int i = 0; i < 3; i++ ) { - try (Client client = newClient().setQueryIdGenerator(constantQueryIdSupplier).build()) { - client.execute("SELECT * FROM unknown_table").get().close(); - } catch (ServerException ex) { - Assert.assertEquals(ex.getCode(), ServerException.ErrorCodes.TABLE_NOT_FOUND.getCode()); - Assert.assertEquals(ex.getQueryId(), queryId); - } - } - - final Queue queryIds = new ConcurrentLinkedQueue<>(); // non-blocking - final Supplier queryIdGen = () -> { - String id = UUID.randomUUID().toString(); - queryIds.add(id); - return id; - }; - int requests = 3; - final Queue actualIds = new ConcurrentLinkedQueue<>(); - for (int i = 0; i < requests; i++ ) { - try (Client client = newClient().setQueryIdGenerator(queryIdGen).build()) { - client.execute("SELECT * FROM unknown_table").get().close(); - } catch (ServerException ex) { - Assert.assertEquals(ex.getCode(), ServerException.ErrorCodes.TABLE_NOT_FOUND.getCode()); - actualIds.add(ex.getQueryId()); - } - } - - Assert.assertEquals(queryIds.size(), requests); - Assert.assertEquals(actualIds, new ArrayList<>(queryIds)); - } - - public boolean isVersionMatch(String versionExpression, Client client) { - List serverVersion = client.queryAll("SELECT version()"); - return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression); - } - - protected Client.Builder newClient() { - ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); - boolean isSecure = isCloud(); - return new Client.Builder() - .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), isSecure) - .setUsername("default") - .setPassword(ClickHouseServerForTest.getPassword()) - .setDefaultDatabase(ClickHouseServerForTest.getDatabase()); - } -} +package com.clickhouse.client; + +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.ClientException; +import com.clickhouse.client.api.ClientFaultCause; +import com.clickhouse.client.api.ClientMisconfigurationException; +import com.clickhouse.client.api.ConnectionReuseStrategy; +import com.clickhouse.client.api.ServerException; +import com.clickhouse.client.api.enums.Protocol; +import com.clickhouse.client.api.insert.InsertSettings; +import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream; +import com.clickhouse.client.api.internal.ServerSettings; +import com.clickhouse.client.api.internal.ValidationUtils; +import com.clickhouse.client.api.metadata.DefaultColumnToMethodMatchingStrategy; +import com.clickhouse.client.api.query.GenericRecord; +import com.clickhouse.client.api.query.QueryResponse; +import com.clickhouse.client.api.query.QuerySettings; +import com.clickhouse.client.api.query.Records; +import com.clickhouse.client.config.ClickHouseClientOption; +import com.clickhouse.data.ClickHouseFormat; +import com.clickhouse.data.ClickHouseVersion; +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.testng.util.Strings; + +import java.io.ByteArrayInputStream; +import java.net.ConnectException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.SECONDS; +import static org.testng.AssertJUnit.fail; + +public class ClientTests extends BaseIntegrationTest { + private static final Logger LOGGER = LoggerFactory.getLogger(ClientTests.class); + + @Test(groups = {"integration"}, dataProvider = "secureClientProvider") + public void testAddSecureEndpoint(Client client) { + if (isCloud()) { + return; // will fail in other tests + } + try { + Optional genericRecord = client + .queryAll("SELECT hostname()").stream().findFirst(); + Assert.assertTrue(genericRecord.isPresent()); + } catch (ClientException e) { + e.printStackTrace(); + if (e.getCause().getCause() instanceof ClickHouseException) { + Exception cause = (Exception) e.getCause().getCause().getCause(); + Assert.assertTrue(cause instanceof ConnectException); + // TODO: correct when SSL support is fully implemented. + Assert.assertTrue(cause.getMessage() + .startsWith("HTTP request failed: PKIX path building failed")); + return; + } + Assert.fail(e.getMessage()); + } finally { + client.close(); + } + } + + @DataProvider(name = "secureClientProvider") + public static Object[][] secureClientProvider() throws Exception { + ClickHouseNode node = ClickHouseServerForTest.getClickHouseNode(ClickHouseProtocol.HTTP, + true, ClickHouseNode.builder() + .addOption(ClickHouseClientOption.SSL_MODE.getKey(), "none") + .addOption(ClickHouseClientOption.SSL.getKey(), "true").build()); + + Client client1; + Client client2; + try { + client1 = new Client.Builder() + .addEndpoint("https://" + node.getHost() + ":" + node.getPort()) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") + .build(); + + client2 = new Client.Builder() + .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), true) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") + .setClientKey("some_user.key") + .setClientCertificate("some_user.crt") + .build(); + + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + + return new Client[][]{ + { + client1 + }, + { + client2 + } + }; + } + + @Test(groups = {"integration"}) + public void testRawSettings() { + try (Client client = newClient().build()) { + + client.execute("SELECT 1"); + + QuerySettings querySettings = new QuerySettings(); + querySettings.serverSetting("session_timezone", "Europe/Zurich"); + + try (Records response = + client.queryRecords("SELECT timeZone(), serverTimeZone()", querySettings).get(10, TimeUnit.SECONDS)) { + + response.forEach(record -> { + Assert.assertEquals("Europe/Zurich", record.getString(1)); + Assert.assertEquals("UTC", record.getString(2)); + }); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } + } + + @Test(groups = {"integration"}) + public void testCustomSettings() { + if (isCloud()) { + return; // no custom parameters on cloud instance + } + final String CLIENT_OPTION = "custom_client_option"; // prefix should be known from server config + try (Client client = newClient().serverSetting(CLIENT_OPTION, "opt1").build()) { + + final List clientOption = client.queryAll("SELECT getSetting({option_name:String})", + Collections.singletonMap("option_name", CLIENT_OPTION)); + + Assert.assertEquals(clientOption.get(0).getString(1), "opt1"); + + QuerySettings querySettings = new QuerySettings(); + querySettings.serverSetting(CLIENT_OPTION, "opt2"); + + final List requestOption = client.queryAll("SELECT getSetting({option_name:String})", + Collections.singletonMap("option_name", CLIENT_OPTION), querySettings); + + Assert.assertEquals(requestOption.get(0).getString(1), "opt2"); + } + } + + @Test(groups = {"integration"}) + public void testPing() { + try (Client client = newClient().build()) { + Assert.assertTrue(client.ping()); + } + } + + @Test(groups = {"integration"}) + public void testPingUnpooled() { + try (Client client = newClient().enableConnectionPool(false).build()) { + Assert.assertTrue(client.ping()); + } + } + + @Test(groups = {"integration"}) + public void testPingFailure() { + try (Client client = new Client.Builder() + .addEndpoint("http://localhost:12345") + .setUsername("default") + .setPassword("") + .build()) { + Assert.assertFalse(client.ping(TimeUnit.SECONDS.toMillis(20))); + } + } + + @Test(groups = {"integration"}) + public void testPingAsync() { + try (Client client = newClient().useAsyncRequests(true).build()) { + Assert.assertTrue(client.ping()); + } + } + + @Test(groups = {"integration"}) + public void testSetOptions() { + Map options = new HashMap<>(); + String productName = "my product_name (version 1.0)"; + options.put(ClickHouseClientOption.PRODUCT_NAME.getKey(), productName); + try (Client client = newClient() + .setOptions(options).build()) { + + Assert.assertEquals(client.getConfiguration().get(ClickHouseClientOption.PRODUCT_NAME.getKey()), productName); + } + } + + @Test(groups = {"integration"}) + public void testProvidedExecutor() throws Exception { + + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try (Client client = newClient().useAsyncRequests(true).setSharedOperationExecutor(executorService).build()) { + QueryResponse response = client.query("SELECT 1").get(); + response.getMetrics(); + } catch (Exception e) { + Assert.fail("unexpected exception", e); + } + + AtomicBoolean flag = new AtomicBoolean(true); + executorService.submit(() -> flag.compareAndSet(true, false)); + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + + Assert.assertFalse(flag.get()); + } + + @Test(groups = {"integration"}) + public void testLoadingServerContext() throws Exception { + long start = System.nanoTime(); + try (Client client = newClient().build()) { + long initTime = (System.nanoTime() - start) / 1_000_000; + Assert.assertTrue(initTime < 100); + Assert.assertEquals(client.getServerVersion(), "unknown"); + client.loadServerInfo(); + Assert.assertNotNull(client.getServerVersion()); + } + } + + @Test(groups = {"integration"}) + public void testDisableNative() { + try (Client client = newClient().disableNativeCompression(true).build()) { + Assert.assertTrue(client.toString().contains("JavaSafe") || client.toString().contains("JavaUnsafe")); + } + } + + @Test(groups = {"integration"}) + public void testDefaultSettings() { + try (Client client = new Client.Builder().setUsername("default").setPassword("secret") + .addEndpoint("http://localhost:8123").build()) { + Map config = client.getConfiguration(); + for (ClientConfigProperties p : ClientConfigProperties.values()) { + if (p.getDefaultValue() != null) { + Assert.assertTrue(config.containsKey(p.getKey()), "Default value should be set for " + p.getKey()); + Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); + } + } + Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. + } + + try (Client client = new Client.Builder() + .setUsername("default") + .setPassword("secret") + .addEndpoint("http://localhost:8123") + .setDefaultDatabase("mydb") + .setExecutionTimeout(10, MILLIS) + .setLZ4UncompressedBufferSize(300_000) + .disableNativeCompression(true) + .useServerTimeZone(false) + .setServerTimeZone("America/Los_Angeles") + .useTimeZone("America/Los_Angeles") + .useAsyncRequests(true) + .setMaxConnections(330) + .setConnectionRequestTimeout(20, SECONDS) + .setConnectionReuseStrategy(ConnectionReuseStrategy.LIFO) + .enableConnectionPool(false) + .setConnectionTTL(30, SECONDS) + .retryOnFailures(ClientFaultCause.NoHttpResponse) + .setClientNetworkBufferSize(500_000) + .setMaxRetries(10) + .useHTTPBasicAuth(false) + .compressClientRequest(true) + .compressServerResponse(false) + .useHttpCompression(true) + .appCompressedData(true) + .setSocketTimeout(20, SECONDS) + .setSocketRcvbuf(100000) + .setSocketSndbuf(100000) + .build()) { + Map config = client.getConfiguration(); + Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added. + Assert.assertEquals(config.get(ClientConfigProperties.DATABASE.getKey()), "mydb"); + Assert.assertEquals(config.get(ClientConfigProperties.MAX_EXECUTION_TIME.getKey()), "10"); + Assert.assertEquals(config.get(ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getKey()), "300000"); + Assert.assertEquals(config.get(ClientConfigProperties.DISABLE_NATIVE_COMPRESSION.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.USE_SERVER_TIMEZONE.getKey()), "false"); + Assert.assertEquals(config.get(ClientConfigProperties.SERVER_TIMEZONE.getKey()), "America/Los_Angeles"); + Assert.assertEquals(config.get(ClientConfigProperties.ASYNC_OPERATIONS.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.HTTP_MAX_OPEN_CONNECTIONS.getKey()), "330"); + Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_REQUEST_TIMEOUT.getKey()), "20000"); + Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_REUSE_STRATEGY.getKey()), "LIFO"); + Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_POOL_ENABLED.getKey()), "false"); + Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_TTL.getKey()), "30000"); + Assert.assertEquals(config.get(ClientConfigProperties.CLIENT_RETRY_ON_FAILURE.getKey()), "NoHttpResponse"); + Assert.assertEquals(config.get(ClientConfigProperties.CLIENT_NETWORK_BUFFER_SIZE.getKey()), "500000"); + Assert.assertEquals(config.get(ClientConfigProperties.RETRY_ON_FAILURE.getKey()), "10"); + Assert.assertEquals(config.get(ClientConfigProperties.HTTP_USE_BASIC_AUTH.getKey()), "false"); + Assert.assertEquals(config.get(ClientConfigProperties.COMPRESS_CLIENT_REQUEST.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getKey()), "false"); + Assert.assertEquals(config.get(ClientConfigProperties.USE_HTTP_COMPRESSION.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.APP_COMPRESSED_DATA.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()), "20000"); + Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_RCVBUF_OPT.getKey()), "100000"); + Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_SNDBUF_OPT.getKey()), "100000"); + } + } + + @Test(groups = {"integration"}) + public void testWithOldDefaults() { + try (Client client = new Client.Builder() + .setUsername("default") + .setPassword("seceret") + .addEndpoint("http://localhost:8123") + .setDefaultDatabase("default") + .setExecutionTimeout(0, MILLIS) + .setLZ4UncompressedBufferSize(ClickHouseLZ4OutputStream.UNCOMPRESSED_BUFF_SIZE) + .disableNativeCompression(false) + .useServerTimeZone(true) + .setServerTimeZone("UTC") + .useAsyncRequests(false) + .setMaxConnections(10) + .setConnectionRequestTimeout(10, SECONDS) + .setConnectionReuseStrategy(ConnectionReuseStrategy.FIFO) + .enableConnectionPool(true) + .setConnectionTTL(-1, MILLIS) + .retryOnFailures(ClientFaultCause.NoHttpResponse, ClientFaultCause.ConnectTimeout, + ClientFaultCause.ConnectionRequestTimeout, ClientFaultCause.ServerRetryable) + .setClientNetworkBufferSize(300_000) + .setMaxRetries(3) + .allowBinaryReaderToReuseBuffers(false) + .columnToMethodMatchingStrategy(DefaultColumnToMethodMatchingStrategy.INSTANCE) + .useHTTPBasicAuth(true) + .compressClientRequest(false) + .compressServerResponse(true) + .useHttpCompression(false) + .appCompressedData(false) + .setSocketTimeout(0, SECONDS) + .setSocketRcvbuf(804800) + .setSocketSndbuf(804800) + .build()) { + Map config = client.getConfiguration(); + for (ClientConfigProperties p : ClientConfigProperties.values()) { + if (p.getDefaultValue() != null) { + Assert.assertTrue(config.containsKey(p.getKey()), "Default value should be set for " + p.getKey()); + Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); + } + } + Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. + } + } + + @DataProvider(name = "sessionRoles") + private static Object[][] sessionRoles() { + return new Object[][]{ + {new String[]{"ROL1", "ROL2"}}, + {new String[]{"ROL1", "ROL2"}}, + {new String[]{"ROL1", "ROL2"}}, + {new String[]{"ROL1", "ROL2,☺"}}, + {new String[]{"ROL1", "ROL2"}}, + }; + } + + @Test(groups = {"integration"}, dataProvider = "sessionRoles") + public void testOperationCustomRoles(String[] roles) throws Exception { + if (isVersionMatch("(,24.3]", newClient().build())) { + return; + } + + String password = "^1A" + RandomStringUtils.random(12, true, true) + "3b$"; + final String rolesList = "\"" + Strings.join("\",\"", roles) + "\""; + try (Client client = newClient().build()) { + client.execute("DROP ROLE IF EXISTS " + rolesList).get().close(); + client.execute("CREATE ROLE " + rolesList).get().close(); + client.execute("DROP USER IF EXISTS some_user").get().close(); + client.execute("CREATE USER some_user IDENTIFIED BY '" + password + "'").get().close(); + client.execute("GRANT " + rolesList + " TO some_user").get().close(); + } + + try (Client userClient = newClient().setUsername("some_user").setPassword(password).build()) { + QuerySettings settings = new QuerySettings().setDBRoles(Arrays.asList(roles)); + List resp = userClient.queryAll("SELECT currentRoles()", settings); + Set roleSet = new HashSet<>(Arrays.asList(roles)); + Set currentRoles = new HashSet (resp.get(0).getList(1)); + Assert.assertEquals(currentRoles, roleSet, "Roles " + roleSet + " not found in " + currentRoles); + } + } + + @DataProvider(name = "clientSessionRoles") + private static Object[][] clientSessionRoles() { + return new Object[][]{ + {new String[]{"ROL1", "ROL2"}}, + {new String[]{"ROL1", "ROL2,☺"}}, + }; + } + @Test(groups = {"integration"}, dataProvider = "clientSessionRoles") + public void testClientCustomRoles(String[] roles) throws Exception { + if (isVersionMatch("(,24.3]", newClient().build())) { + return; + } + + String password = "^1A" + RandomStringUtils.random(12, true, true) + "3B$"; + final String rolesList = "\"" + Strings.join("\",\"", roles) + "\""; + try (Client client = newClient().build()) { + client.execute("DROP ROLE IF EXISTS " + rolesList).get().close(); + client.execute("CREATE ROLE " + rolesList).get().close(); + client.execute("DROP USER IF EXISTS some_user").get().close(); + client.execute("CREATE USER some_user IDENTIFIED WITH sha256_password BY '" + password + "'").get().close(); + client.execute("GRANT " + rolesList + " TO some_user").get().close(); + } + + try (Client userClient = newClient().setUsername("some_user").setPassword(password).build()) { + userClient.setDBRoles(Arrays.asList(roles)); + List resp = userClient.queryAll("SELECT currentRoles()"); + Set roleSet = new HashSet<>(Arrays.asList(roles)); + Set currentRoles = new HashSet (resp.get(0).getList(1)); + Assert.assertEquals(currentRoles, roleSet, "Roles " + roleSet + " not found in " + currentRoles); + } + } + + @Test(groups = {"integration"}) + public void testRuntimeCredentialChange() throws Exception { + if (isCloud()) { + return; // creating users is not expected in cloud tests + } + + String user1 = "client_v2_user1_" + RandomStringUtils.random(8, true, true).toLowerCase(); + String user2 = "client_v2_user2_" + RandomStringUtils.random(8, true, true).toLowerCase(); + String password1 = "^1A" + RandomStringUtils.random(12, true, true) + "3b$"; + String password2 = "^1A" + RandomStringUtils.random(12, true, true) + "3B$"; + + try (Client adminClient = newClient().build()) { + try { + adminClient.execute("DROP USER IF EXISTS " + user1).get().close(); + adminClient.execute("DROP USER IF EXISTS " + user2).get().close(); + adminClient.execute("CREATE USER " + user1 + " IDENTIFIED BY '" + password1 + "'").get().close(); + adminClient.execute("CREATE USER " + user2 + " IDENTIFIED BY '" + password2 + "'").get().close(); + + try (Client userClient = newClient().setUsername(user1).setPassword(password1).build()) { + List firstResponse = userClient.queryAll("SELECT currentUser() AS user"); + Assert.assertEquals(firstResponse.get(0).getString("user"), user1); + + userClient.setCredentials(user2, password2); + + List secondResponse = userClient.queryAll("SELECT currentUser() AS user"); + Assert.assertEquals(secondResponse.get(0).getString("user"), user2); + } + } finally { + adminClient.execute("DROP USER IF EXISTS " + user1).get().close(); + adminClient.execute("DROP USER IF EXISTS " + user2).get().close(); + } + } + } + + + @Test(groups = {"integration"}) + public void testLogComment() throws Exception { + + String logComment = "Test log comment"; + QuerySettings settings = new QuerySettings() + .setQueryId(UUID.randomUUID().toString()) + .logComment(logComment); + + try (Client client = newClient().build()) { + + try (QueryResponse response = client.query("SELECT 1", settings).get()) { + Assert.assertNotNull(response.getQueryId()); + Assert.assertTrue(response.getQueryId().startsWith(settings.getQueryId())); + } + + client.execute("SYSTEM FLUSH LOGS").get().close(); + + List logRecords = client.queryAll("SELECT query_id, log_comment FROM clusterAllReplicas('default', system.query_log) WHERE query_id = '" + settings.getQueryId() + "'"); + Assert.assertEquals(logRecords.get(0).getString("query_id"), settings.getQueryId()); + Assert.assertEquals(logRecords.get(0).getString("log_comment"), logComment); + } + } + + @Test(groups = {"integration"}) + public void testServerSettings() throws Exception { + try (Client client = newClient().build()) { + client.execute("DROP TABLE IF EXISTS server_settings_test_table"); + client.execute("CREATE TABLE server_settings_test_table (v Float) Engine MergeTree ORDER BY ()"); + + final String queryId = UUID.randomUUID().toString(); + InsertSettings insertSettings = new InsertSettings() + .setQueryId(queryId) + .serverSetting(ServerSettings.ASYNC_INSERT, "1") + .serverSetting(ServerSettings.WAIT_ASYNC_INSERT, "1") + .serverSetting(ServerSettings.INPUT_FORMAT_BINARY_READ_JSON_AS_STRING, "1"); + + String csvData = "0.33\n0.44\n0.55\n"; + client.insert("server_settings_test_table", new ByteArrayInputStream(csvData.getBytes()), ClickHouseFormat.CSV, insertSettings).get().close(); + + client.execute("SYSTEM FLUSH LOGS").get().close(); + + List logRecords = client.queryAll("SELECT Settings, ProfileEvents['AsyncInsertQuery'] as was_async FROM clusterAllReplicas('default', system.query_log) WHERE query_id = '" + queryId + "' AND type = 'QueryFinish'"); + + GenericRecord record = logRecords.get(0); + Assert.assertTrue(record.getBoolean("was_async")); + String settings = record.getString(record.getSchema().nameToColumnIndex("Settings")); + Assert.assertTrue(settings.contains("input_format_binary_read_json_as_string=1")); +// Assert.assertTrue(settings.contains(ServerSettings.ASYNC_INSERT + "=1")); // async settings are not reflected in query log any more +// Assert.assertTrue(settings.contains(ServerSettings.WAIT_ASYNC_INSERT + "=1")); // uncomment after server fix + } + } + + @Test(groups = {"integration"}) + public void testInvalidEndpoint() { + + try { + new Client.Builder().addEndpoint("http://localhost/default"); + fail("Exception expected"); + } catch (ValidationUtils.SettingsValidationException e) { + Assert.assertTrue(e.getMessage().contains("port")); + } + } + + @Test(groups = {"integration"}) + public void testUnknownClientSettings() throws Exception { + try (Client client = newClient().setOption("unknown_setting", "value").build()) { + Assert.fail("Exception expected"); + } catch (Exception ex) { + Assert.assertTrue(ex instanceof ClientMisconfigurationException); + Assert.assertTrue(ex.getMessage().contains("unknown_setting")); + } + + try (Client client = newClient().setOption(ClientConfigProperties.IGNORE_UNKNOWN_CONFIG_KEY, "true").setOption("unknown_setting", "value").build()) { + Assert.assertTrue(client.ping()); + } + + try (Client client = newClient().setOption(ClientConfigProperties.SERVER_SETTING_PREFIX + "unknown_setting", "value").build()) { + try { + client.execute("SELECT 1"); + Assert.fail("Exception expected"); + } catch (ServerException e) { + Assert.assertEquals(e.getCode(), ServerException.UNKNOWN_SETTING); + } + } + + try (Client client = newClient().setOption(ClientConfigProperties.HTTP_HEADER_PREFIX + "unknown_setting", "value").build()) { + Assert.assertTrue(client.ping()); + } + } + + @Test(groups = {"integration"}) + public void testInvalidConfig() { + try { + newClient().setOption(ClientConfigProperties.CUSTOM_SETTINGS_PREFIX.getKey(), "").build(); + Assert.fail("exception expected"); + } catch (ClientException e) { + Assert.assertTrue(e.getMessage().contains(ClientConfigProperties.CUSTOM_SETTINGS_PREFIX.getKey())); + } + } + + @Test(groups = {"integration"}) + public void testQueryIdGenerator() throws Exception { + final String queryId = UUID.randomUUID().toString(); + Supplier constantQueryIdSupplier = () -> queryId; + + // check getting same UUID + for (int i = 0; i < 3; i++ ) { + try (Client client = newClient().setQueryIdGenerator(constantQueryIdSupplier).build()) { + client.execute("SELECT * FROM unknown_table").get().close(); + } catch (ServerException ex) { + Assert.assertEquals(ex.getCode(), ServerException.ErrorCodes.TABLE_NOT_FOUND.getCode()); + Assert.assertEquals(ex.getQueryId(), queryId); + } + } + + final Queue queryIds = new ConcurrentLinkedQueue<>(); // non-blocking + final Supplier queryIdGen = () -> { + String id = UUID.randomUUID().toString(); + queryIds.add(id); + return id; + }; + int requests = 3; + final Queue actualIds = new ConcurrentLinkedQueue<>(); + for (int i = 0; i < requests; i++ ) { + try (Client client = newClient().setQueryIdGenerator(queryIdGen).build()) { + client.execute("SELECT * FROM unknown_table").get().close(); + } catch (ServerException ex) { + Assert.assertEquals(ex.getCode(), ServerException.ErrorCodes.TABLE_NOT_FOUND.getCode()); + actualIds.add(ex.getQueryId()); + } + } + + Assert.assertEquals(queryIds.size(), requests); + Assert.assertEquals(actualIds, new ArrayList<>(queryIds)); + } + + public boolean isVersionMatch(String versionExpression, Client client) { + List serverVersion = client.queryAll("SELECT version()"); + return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression); + } + + protected Client.Builder newClient() { + ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); + boolean isSecure = isCloud(); + return new Client.Builder() + .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), isSecure) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setDefaultDatabase(ClickHouseServerForTest.getDatabase()); + } +} 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 fc2d13a86..93bb78394 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -944,7 +944,7 @@ public void testBearerTokenAuth() throws Exception { .map(s -> Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8))) .reduce((s1, s2) -> s1 + "." + s2).get(); try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false) - .useBearerTokenAuth(jwtToken1) + .setAccessToken(jwtToken1) .compressServerResponse(false) .build()) { @@ -974,7 +974,7 @@ public void testBearerTokenAuth() throws Exception { .build()); try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false) - .useBearerTokenAuth(jwtToken1) + .setAccessToken(jwtToken1) .compressServerResponse(false) .build()) { @@ -994,7 +994,7 @@ public void testBearerTokenAuth() throws Exception { .build()); - client.updateBearerToken(jwtToken2); + client.setAccessToken(jwtToken2); client.execute("SELECT 1").get(); } @@ -1003,6 +1003,59 @@ public void testBearerTokenAuth() throws Exception { } } + @Test(groups = { "integration" }) + public void testSetCredentialsAfterClientCreation() throws Exception { + if (isCloud()) { + return; // mocked server + } + + WireMockServer mockServer = new WireMockServer(WireMockConfiguration + .options().port(9090).notifier(new ConsoleNotifier(false))); + mockServer.start(); + + try { + String user1 = "default"; + String password1 = "wrong-password"; + String user2 = "runtime-user"; + String password2 = "runtime-password"; + String basicAuth2 = "Basic " + Base64.getEncoder().encodeToString( + (user2 + ":" + password2).getBytes(StandardCharsets.UTF_8)); + + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withHeader(HttpHeaders.AUTHORIZATION, WireMock.equalTo("Basic " + Base64.getEncoder().encodeToString( + (user1 + ":" + password1).getBytes(StandardCharsets.UTF_8)))) + .willReturn(WireMock.aResponse() + .withStatus(HttpStatus.SC_UNAUTHORIZED)) + .build()); + + try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false) + .setUsername(user1) + .setPassword(password1) + .compressServerResponse(false) + .build()) { + try { + client.execute("SELECT 1").get(); + fail("Exception expected"); + } catch (ServerException e) { + Assert.assertEquals(e.getTransportProtocolCode(), HttpStatus.SC_UNAUTHORIZED); + } + + mockServer.resetAll(); + mockServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .withHeader(HttpHeaders.AUTHORIZATION, WireMock.equalTo(basicAuth2)) + .willReturn(WireMock.aResponse() + .withHeader("X-ClickHouse-Summary", + "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")) + .build()); + + client.setCredentials(user2, password2); + client.execute("SELECT 1").get(); + } + } finally { + mockServer.stop(); + } + } + @Test(groups = { "integration" }) public void testJWTWithCloud() throws Exception { if (!isCloud()) { @@ -1011,7 +1064,7 @@ public void testJWTWithCloud() throws Exception { String jwt = System.getenv("CLIENT_JWT"); Assert.assertTrue(jwt != null && !jwt.trim().isEmpty(), "JWT is missing"); Assert.assertFalse(jwt.contains("\n") || jwt.contains("-----"), "JWT should be single string ready for HTTP header"); - try (Client client = newClient().useBearerTokenAuth(jwt).build()) { + try (Client client = newClient().setAccessToken(jwt).build()) { try { List response = client.queryAll("SELECT user(), now()"); System.out.println("response: " + response.get(0).getString(1) + " time: " + response.get(0).getString(2)); diff --git a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Authentication.java b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Authentication.java new file mode 100644 index 000000000..71cf2c62b --- /dev/null +++ b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/Authentication.java @@ -0,0 +1,119 @@ +package com.clickhouse.examples.client_v2; + +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.query.GenericRecord; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * Example showing how to update authentication settings on an existing client. + * + *

Supported startup properties:

+ *
    + *
  • {@code chEndpoint} - ClickHouse endpoint, default {@code http://localhost:8123}
  • + *
  • {@code chDatabase} - database name, default {@code default}
  • + *
  • {@code chUser} and {@code chPassword} - initial username/password credentials
  • + *
  • {@code chAccessToken} - initial access token, preferred over username/password when set
  • + *
  • {@code chNextUser} and {@code chNextPassword} - replacement username/password credentials
  • + *
  • {@code chNextAccessToken} - replacement access token
  • + *
+ */ +@Slf4j +public class Authentication { + + public static void main(String[] args) { + final String endpoint = System.getProperty("chEndpoint", "http://localhost:8123"); + final String database = System.getProperty("chDatabase", "default"); + + final String initialUser = System.getProperty("chUser", "default"); + final String initialPassword = System.getProperty("chPassword", ""); + final String initialAccessToken = trimToNull(System.getProperty("chAccessToken")); + + final String nextUser = trimToNull(System.getProperty("chNextUser")); + final String nextPassword = trimToNull(System.getProperty("chNextPassword")); + final String nextAccessToken = trimToNull(System.getProperty("chNextAccessToken")); + + Client.Builder builder = new Client.Builder() + .addEndpoint(endpoint) + .setDefaultDatabase(database) + .compressServerResponse(true); + + configureInitialAuthentication(builder, initialUser, initialPassword, initialAccessToken); + + try (Client client = builder.build()) { + printCurrentUser(client, "Before authentication update"); + + if (!updateAuthentication(client, initialUser, initialPassword, nextUser, nextPassword, nextAccessToken)) { + log.info("No replacement credentials were provided. Set chNextAccessToken or chNextUser/chNextPassword to try runtime authentication update."); + return; + } + + printCurrentUser(client, "After authentication update"); + } catch (Exception e) { + log.error("Authentication example failed", e); + } + } + + private static void configureInitialAuthentication(Client.Builder builder, String user, String password, String accessToken) { + if (accessToken != null) { + authenticateWithAccessToken(builder, accessToken); + } else { + authenticateWithCredentials(builder, user, password); + } + } + + private static boolean updateAuthentication(Client client, String initialUser, String initialPassword, + String nextUser, String nextPassword, String nextAccessToken) { + if (nextAccessToken != null) { + authenticateWithAccessToken(client, nextAccessToken); + return true; + } + + if (nextUser != null || nextPassword != null) { + authenticateWithCredentials( + client, + nextUser != null ? nextUser : initialUser, + nextPassword != null ? nextPassword : initialPassword); + return true; + } + + return false; + } + + private static void authenticateWithCredentials(Client.Builder builder, String user, String password) { + builder.setOption(ClientConfigProperties.USER.getKey(), user); // user + builder.setOption(ClientConfigProperties.PASSWORD.getKey(), password); // password + log.info("Client created with username/password authentication"); + } + + private static void authenticateWithAccessToken(Client.Builder builder, String accessToken) { + builder.setOption(ClientConfigProperties.ACCESS_TOKEN.getKey(), accessToken); // access_token + log.info("Client created with access token authentication"); + } + + private static void authenticateWithCredentials(Client client, String user, String password) { + client.setCredentials(user, password); + log.info("Updated client authentication using username/password"); + } + + private static void authenticateWithAccessToken(Client client, String accessToken) { + client.setAccessToken(accessToken); + log.info("Updated client authentication using access token"); + } + + private static void printCurrentUser(Client client, String stage) { + List rows = client.queryAll("SELECT currentUser() AS user"); + log.info("{}: {}", stage, rows.get(0).getString("user")); + } + + private static String trimToNull(String value) { + if (value == null) { + return null; + } + + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } +} diff --git a/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/Authentication.java b/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/Authentication.java new file mode 100644 index 000000000..cb63886d6 --- /dev/null +++ b/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/Authentication.java @@ -0,0 +1,102 @@ +package com.clickhouse.examples.jdbc; + +import com.clickhouse.client.api.ClientConfigProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +/** + * Example showing how to use different authentication settings with JDBC. + * + *

Unlike the client-v2 example, JDBC normally works by creating a new connection with + * updated properties instead of mutating an existing connection.

+ * + *

Supported startup properties:

+ *
    + *
  • {@code chUrl} - ClickHouse JDBC URL, default {@code jdbc:clickhouse://localhost:8123/default}
  • + *
  • {@code chUser} and {@code chPassword} - initial username/password credentials
  • + *
  • {@code chAccessToken} - initial access token
  • + *
  • {@code chNextUser} and {@code chNextPassword} - replacement username/password credentials
  • + *
  • {@code chNextAccessToken} - replacement access token
  • + *
+ */ +public class Authentication { + private static final Logger log = LoggerFactory.getLogger(Authentication.class); + + public static void main(String[] args) { + final String url = System.getProperty("chUrl", "jdbc:clickhouse://localhost:8123/default"); + + final String initialUser = System.getProperty("chUser", "default"); + final String initialPassword = System.getProperty("chPassword", ""); + final String initialAccessToken = trimToNull(System.getProperty("chAccessToken")); + + final String nextUser = trimToNull(System.getProperty("chNextUser")); + final String nextPassword = trimToNull(System.getProperty("chNextPassword")); + final String nextAccessToken = trimToNull(System.getProperty("chNextAccessToken")); + + try { + if (initialAccessToken != null) { + authenticateWithAccessToken(url, initialAccessToken, "Initial connection"); + } else { + authenticateWithCredentials(url, initialUser, initialPassword, "Initial connection"); + } + + if (nextAccessToken != null) { + authenticateWithAccessToken(url, nextAccessToken, "Connection after auth update"); + } else if (nextUser != null || nextPassword != null) { + authenticateWithCredentials( + url, + nextUser != null ? nextUser : initialUser, + nextPassword != null ? nextPassword : initialPassword, + "Connection after auth update"); + } else { + log.info("No replacement credentials were provided. Set chNextAccessToken or chNextUser/chNextPassword to try authentication update with a new JDBC connection."); + } + } catch (SQLException e) { + log.error("JDBC authentication example failed", e); + } + } + + private static void authenticateWithCredentials(String url, String user, String password, String stage) throws SQLException { + Properties properties = new Properties(); + properties.setProperty(ClientConfigProperties.USER.getKey(), user); // user + properties.setProperty(ClientConfigProperties.PASSWORD.getKey(), password); // password + + try (Connection connection = DriverManager.getConnection(url, properties)) { + printCurrentUser(connection, stage + " using username/password"); + } + } + + private static void authenticateWithAccessToken(String url, String accessToken, String stage) throws SQLException { + Properties properties = new Properties(); + properties.setProperty(ClientConfigProperties.ACCESS_TOKEN.getKey(), accessToken); // access_token + + try (Connection connection = DriverManager.getConnection(url, properties)) { + printCurrentUser(connection, stage + " using access token"); + } + } + + private static void printCurrentUser(Connection connection, String stage) throws SQLException { + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT currentUser()")) { + if (rs.next()) { + log.info("{}: {}", stage, rs.getString(1)); + } + } + } + + private static String trimToNull(String value) { + if (value == null) { + return null; + } + + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } +} diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/DataSourceImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/DataSourceImpl.java index f42c82982..25d388dae 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/DataSourceImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/DataSourceImpl.java @@ -1,5 +1,6 @@ package com.clickhouse.jdbc; +import com.clickhouse.client.api.ClientConfigProperties; import com.clickhouse.jdbc.internal.ExceptionUtils; import javax.sql.DataSource; @@ -50,8 +51,11 @@ public Connection getConnection() throws SQLException { @Override public Connection getConnection(String username, String password) throws SQLException { Properties info = getProperties(); - info.setProperty("user", username); - info.setProperty("password", password); + info.setProperty(ClientConfigProperties.USER.getKey(), username); + info.setProperty(ClientConfigProperties.PASSWORD.getKey(), password); + info.setProperty(ClientConfigProperties.SSL_AUTH.getKey(), Boolean.FALSE.toString()); + info.remove(ClientConfigProperties.ACCESS_TOKEN.getKey()); + info.remove(ClientConfigProperties.BEARERTOKEN_AUTH.getKey()); return driver.connect(this.url, info); } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java index 9aa5ce61a..cf98a601d 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java @@ -95,9 +95,12 @@ public JdbcConfiguration(String url, Properties info) throws SQLException { boolean useSSLInfo = Boolean.parseBoolean(props.getProperty(DriverProperties.SECURE_CONNECTION.getKey(), "false")); boolean useSSLUrlProperties = Boolean.parseBoolean(urlProperties.getOrDefault(DriverProperties.SECURE_CONNECTION.getKey(), "false")); boolean useSSL = useSSLInfo || useSSLUrlProperties; - String bearerToken = props.getProperty(ClientConfigProperties.BEARERTOKEN_AUTH.getKey(), null); - if (bearerToken != null) { - clientProperties.put(ClientConfigProperties.BEARERTOKEN_AUTH.getKey(), bearerToken); + String accessToken = props.getProperty(ClientConfigProperties.ACCESS_TOKEN.getKey(), null); + if (accessToken == null) { + accessToken = props.getProperty(ClientConfigProperties.BEARERTOKEN_AUTH.getKey(), null); + } + if (accessToken != null) { + clientProperties.put(ClientConfigProperties.ACCESS_TOKEN.getKey(), accessToken); } this.connectionUrl = createConnectionURL(tmpConnectionUrl, useSSL); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java index 51001b461..7a23088b5 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java @@ -850,7 +850,7 @@ public void testUnwrapping() throws Exception { } @Test(groups = { "integration" }) - public void testBearerTokenAuth() throws Exception { + public void testAccessTokenAuth() throws Exception { if (isCloud()) { return; // mocked server } @@ -898,7 +898,7 @@ public void testBearerTokenAuth() throws Exception { "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")).build()); Properties properties = new Properties(); - properties.put(ClientConfigProperties.BEARERTOKEN_AUTH.getKey(), jwtToken1); + properties.put(ClientConfigProperties.ACCESS_TOKEN.getKey(), jwtToken1); properties.put("compress", "false"); String jdbcUrl = "jdbc:clickhouse://" + "localhost" + ":" + mockServer.port(); try (Connection conn = new ConnectionImpl(jdbcUrl, properties); @@ -911,6 +911,7 @@ public void testBearerTokenAuth() throws Exception { mockServer.stop(); } } + @Test(groups = { "integration" }) public void testJWTWithCloud() throws Exception { if (!isCloud()) { From ffbdd849fc0be2f6a9d53f622449254e75525d33 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 30 Mar 2026 14:21:30 -0700 Subject: [PATCH 2/5] fix encoding --- .../com/clickhouse/client/ClientTests.java | 1240 ++++++++--------- 1 file changed, 620 insertions(+), 620 deletions(-) diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index 22f1b6875..d2e4454c9 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -1,620 +1,620 @@ -package com.clickhouse.client; - -import com.clickhouse.client.api.Client; -import com.clickhouse.client.api.ClientConfigProperties; -import com.clickhouse.client.api.ClientException; -import com.clickhouse.client.api.ClientFaultCause; -import com.clickhouse.client.api.ClientMisconfigurationException; -import com.clickhouse.client.api.ConnectionReuseStrategy; -import com.clickhouse.client.api.ServerException; -import com.clickhouse.client.api.enums.Protocol; -import com.clickhouse.client.api.insert.InsertSettings; -import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream; -import com.clickhouse.client.api.internal.ServerSettings; -import com.clickhouse.client.api.internal.ValidationUtils; -import com.clickhouse.client.api.metadata.DefaultColumnToMethodMatchingStrategy; -import com.clickhouse.client.api.query.GenericRecord; -import com.clickhouse.client.api.query.QueryResponse; -import com.clickhouse.client.api.query.QuerySettings; -import com.clickhouse.client.api.query.Records; -import com.clickhouse.client.config.ClickHouseClientOption; -import com.clickhouse.data.ClickHouseFormat; -import com.clickhouse.data.ClickHouseVersion; -import org.apache.commons.lang3.RandomStringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; -import org.testng.util.Strings; - -import java.io.ByteArrayInputStream; -import java.net.ConnectException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Queue; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; - -import static java.time.temporal.ChronoUnit.MILLIS; -import static java.time.temporal.ChronoUnit.SECONDS; -import static org.testng.AssertJUnit.fail; - -public class ClientTests extends BaseIntegrationTest { - private static final Logger LOGGER = LoggerFactory.getLogger(ClientTests.class); - - @Test(groups = {"integration"}, dataProvider = "secureClientProvider") - public void testAddSecureEndpoint(Client client) { - if (isCloud()) { - return; // will fail in other tests - } - try { - Optional genericRecord = client - .queryAll("SELECT hostname()").stream().findFirst(); - Assert.assertTrue(genericRecord.isPresent()); - } catch (ClientException e) { - e.printStackTrace(); - if (e.getCause().getCause() instanceof ClickHouseException) { - Exception cause = (Exception) e.getCause().getCause().getCause(); - Assert.assertTrue(cause instanceof ConnectException); - // TODO: correct when SSL support is fully implemented. - Assert.assertTrue(cause.getMessage() - .startsWith("HTTP request failed: PKIX path building failed")); - return; - } - Assert.fail(e.getMessage()); - } finally { - client.close(); - } - } - - @DataProvider(name = "secureClientProvider") - public static Object[][] secureClientProvider() throws Exception { - ClickHouseNode node = ClickHouseServerForTest.getClickHouseNode(ClickHouseProtocol.HTTP, - true, ClickHouseNode.builder() - .addOption(ClickHouseClientOption.SSL_MODE.getKey(), "none") - .addOption(ClickHouseClientOption.SSL.getKey(), "true").build()); - - Client client1; - Client client2; - try { - client1 = new Client.Builder() - .addEndpoint("https://" + node.getHost() + ":" + node.getPort()) - .setUsername("default") - .setPassword(ClickHouseServerForTest.getPassword()) - .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") - .build(); - - client2 = new Client.Builder() - .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), true) - .setUsername("default") - .setPassword(ClickHouseServerForTest.getPassword()) - .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") - .setClientKey("some_user.key") - .setClientCertificate("some_user.crt") - .build(); - - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - - - return new Client[][]{ - { - client1 - }, - { - client2 - } - }; - } - - @Test(groups = {"integration"}) - public void testRawSettings() { - try (Client client = newClient().build()) { - - client.execute("SELECT 1"); - - QuerySettings querySettings = new QuerySettings(); - querySettings.serverSetting("session_timezone", "Europe/Zurich"); - - try (Records response = - client.queryRecords("SELECT timeZone(), serverTimeZone()", querySettings).get(10, TimeUnit.SECONDS)) { - - response.forEach(record -> { - Assert.assertEquals("Europe/Zurich", record.getString(1)); - Assert.assertEquals("UTC", record.getString(2)); - }); - } catch (Exception e) { - Assert.fail(e.getMessage()); - } - } - } - - @Test(groups = {"integration"}) - public void testCustomSettings() { - if (isCloud()) { - return; // no custom parameters on cloud instance - } - final String CLIENT_OPTION = "custom_client_option"; // prefix should be known from server config - try (Client client = newClient().serverSetting(CLIENT_OPTION, "opt1").build()) { - - final List clientOption = client.queryAll("SELECT getSetting({option_name:String})", - Collections.singletonMap("option_name", CLIENT_OPTION)); - - Assert.assertEquals(clientOption.get(0).getString(1), "opt1"); - - QuerySettings querySettings = new QuerySettings(); - querySettings.serverSetting(CLIENT_OPTION, "opt2"); - - final List requestOption = client.queryAll("SELECT getSetting({option_name:String})", - Collections.singletonMap("option_name", CLIENT_OPTION), querySettings); - - Assert.assertEquals(requestOption.get(0).getString(1), "opt2"); - } - } - - @Test(groups = {"integration"}) - public void testPing() { - try (Client client = newClient().build()) { - Assert.assertTrue(client.ping()); - } - } - - @Test(groups = {"integration"}) - public void testPingUnpooled() { - try (Client client = newClient().enableConnectionPool(false).build()) { - Assert.assertTrue(client.ping()); - } - } - - @Test(groups = {"integration"}) - public void testPingFailure() { - try (Client client = new Client.Builder() - .addEndpoint("http://localhost:12345") - .setUsername("default") - .setPassword("") - .build()) { - Assert.assertFalse(client.ping(TimeUnit.SECONDS.toMillis(20))); - } - } - - @Test(groups = {"integration"}) - public void testPingAsync() { - try (Client client = newClient().useAsyncRequests(true).build()) { - Assert.assertTrue(client.ping()); - } - } - - @Test(groups = {"integration"}) - public void testSetOptions() { - Map options = new HashMap<>(); - String productName = "my product_name (version 1.0)"; - options.put(ClickHouseClientOption.PRODUCT_NAME.getKey(), productName); - try (Client client = newClient() - .setOptions(options).build()) { - - Assert.assertEquals(client.getConfiguration().get(ClickHouseClientOption.PRODUCT_NAME.getKey()), productName); - } - } - - @Test(groups = {"integration"}) - public void testProvidedExecutor() throws Exception { - - ExecutorService executorService = Executors.newSingleThreadExecutor(); - try (Client client = newClient().useAsyncRequests(true).setSharedOperationExecutor(executorService).build()) { - QueryResponse response = client.query("SELECT 1").get(); - response.getMetrics(); - } catch (Exception e) { - Assert.fail("unexpected exception", e); - } - - AtomicBoolean flag = new AtomicBoolean(true); - executorService.submit(() -> flag.compareAndSet(true, false)); - executorService.shutdown(); - executorService.awaitTermination(10, TimeUnit.SECONDS); - - Assert.assertFalse(flag.get()); - } - - @Test(groups = {"integration"}) - public void testLoadingServerContext() throws Exception { - long start = System.nanoTime(); - try (Client client = newClient().build()) { - long initTime = (System.nanoTime() - start) / 1_000_000; - Assert.assertTrue(initTime < 100); - Assert.assertEquals(client.getServerVersion(), "unknown"); - client.loadServerInfo(); - Assert.assertNotNull(client.getServerVersion()); - } - } - - @Test(groups = {"integration"}) - public void testDisableNative() { - try (Client client = newClient().disableNativeCompression(true).build()) { - Assert.assertTrue(client.toString().contains("JavaSafe") || client.toString().contains("JavaUnsafe")); - } - } - - @Test(groups = {"integration"}) - public void testDefaultSettings() { - try (Client client = new Client.Builder().setUsername("default").setPassword("secret") - .addEndpoint("http://localhost:8123").build()) { - Map config = client.getConfiguration(); - for (ClientConfigProperties p : ClientConfigProperties.values()) { - if (p.getDefaultValue() != null) { - Assert.assertTrue(config.containsKey(p.getKey()), "Default value should be set for " + p.getKey()); - Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); - } - } - Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. - } - - try (Client client = new Client.Builder() - .setUsername("default") - .setPassword("secret") - .addEndpoint("http://localhost:8123") - .setDefaultDatabase("mydb") - .setExecutionTimeout(10, MILLIS) - .setLZ4UncompressedBufferSize(300_000) - .disableNativeCompression(true) - .useServerTimeZone(false) - .setServerTimeZone("America/Los_Angeles") - .useTimeZone("America/Los_Angeles") - .useAsyncRequests(true) - .setMaxConnections(330) - .setConnectionRequestTimeout(20, SECONDS) - .setConnectionReuseStrategy(ConnectionReuseStrategy.LIFO) - .enableConnectionPool(false) - .setConnectionTTL(30, SECONDS) - .retryOnFailures(ClientFaultCause.NoHttpResponse) - .setClientNetworkBufferSize(500_000) - .setMaxRetries(10) - .useHTTPBasicAuth(false) - .compressClientRequest(true) - .compressServerResponse(false) - .useHttpCompression(true) - .appCompressedData(true) - .setSocketTimeout(20, SECONDS) - .setSocketRcvbuf(100000) - .setSocketSndbuf(100000) - .build()) { - Map config = client.getConfiguration(); - Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added. - Assert.assertEquals(config.get(ClientConfigProperties.DATABASE.getKey()), "mydb"); - Assert.assertEquals(config.get(ClientConfigProperties.MAX_EXECUTION_TIME.getKey()), "10"); - Assert.assertEquals(config.get(ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getKey()), "300000"); - Assert.assertEquals(config.get(ClientConfigProperties.DISABLE_NATIVE_COMPRESSION.getKey()), "true"); - Assert.assertEquals(config.get(ClientConfigProperties.USE_SERVER_TIMEZONE.getKey()), "false"); - Assert.assertEquals(config.get(ClientConfigProperties.SERVER_TIMEZONE.getKey()), "America/Los_Angeles"); - Assert.assertEquals(config.get(ClientConfigProperties.ASYNC_OPERATIONS.getKey()), "true"); - Assert.assertEquals(config.get(ClientConfigProperties.HTTP_MAX_OPEN_CONNECTIONS.getKey()), "330"); - Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_REQUEST_TIMEOUT.getKey()), "20000"); - Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_REUSE_STRATEGY.getKey()), "LIFO"); - Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_POOL_ENABLED.getKey()), "false"); - Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_TTL.getKey()), "30000"); - Assert.assertEquals(config.get(ClientConfigProperties.CLIENT_RETRY_ON_FAILURE.getKey()), "NoHttpResponse"); - Assert.assertEquals(config.get(ClientConfigProperties.CLIENT_NETWORK_BUFFER_SIZE.getKey()), "500000"); - Assert.assertEquals(config.get(ClientConfigProperties.RETRY_ON_FAILURE.getKey()), "10"); - Assert.assertEquals(config.get(ClientConfigProperties.HTTP_USE_BASIC_AUTH.getKey()), "false"); - Assert.assertEquals(config.get(ClientConfigProperties.COMPRESS_CLIENT_REQUEST.getKey()), "true"); - Assert.assertEquals(config.get(ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getKey()), "false"); - Assert.assertEquals(config.get(ClientConfigProperties.USE_HTTP_COMPRESSION.getKey()), "true"); - Assert.assertEquals(config.get(ClientConfigProperties.APP_COMPRESSED_DATA.getKey()), "true"); - Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()), "20000"); - Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_RCVBUF_OPT.getKey()), "100000"); - Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_SNDBUF_OPT.getKey()), "100000"); - } - } - - @Test(groups = {"integration"}) - public void testWithOldDefaults() { - try (Client client = new Client.Builder() - .setUsername("default") - .setPassword("seceret") - .addEndpoint("http://localhost:8123") - .setDefaultDatabase("default") - .setExecutionTimeout(0, MILLIS) - .setLZ4UncompressedBufferSize(ClickHouseLZ4OutputStream.UNCOMPRESSED_BUFF_SIZE) - .disableNativeCompression(false) - .useServerTimeZone(true) - .setServerTimeZone("UTC") - .useAsyncRequests(false) - .setMaxConnections(10) - .setConnectionRequestTimeout(10, SECONDS) - .setConnectionReuseStrategy(ConnectionReuseStrategy.FIFO) - .enableConnectionPool(true) - .setConnectionTTL(-1, MILLIS) - .retryOnFailures(ClientFaultCause.NoHttpResponse, ClientFaultCause.ConnectTimeout, - ClientFaultCause.ConnectionRequestTimeout, ClientFaultCause.ServerRetryable) - .setClientNetworkBufferSize(300_000) - .setMaxRetries(3) - .allowBinaryReaderToReuseBuffers(false) - .columnToMethodMatchingStrategy(DefaultColumnToMethodMatchingStrategy.INSTANCE) - .useHTTPBasicAuth(true) - .compressClientRequest(false) - .compressServerResponse(true) - .useHttpCompression(false) - .appCompressedData(false) - .setSocketTimeout(0, SECONDS) - .setSocketRcvbuf(804800) - .setSocketSndbuf(804800) - .build()) { - Map config = client.getConfiguration(); - for (ClientConfigProperties p : ClientConfigProperties.values()) { - if (p.getDefaultValue() != null) { - Assert.assertTrue(config.containsKey(p.getKey()), "Default value should be set for " + p.getKey()); - Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); - } - } - Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. - } - } - - @DataProvider(name = "sessionRoles") - private static Object[][] sessionRoles() { - return new Object[][]{ - {new String[]{"ROL1", "ROL2"}}, - {new String[]{"ROL1", "ROL2"}}, - {new String[]{"ROL1", "ROL2"}}, - {new String[]{"ROL1", "ROL2,☺"}}, - {new String[]{"ROL1", "ROL2"}}, - }; - } - - @Test(groups = {"integration"}, dataProvider = "sessionRoles") - public void testOperationCustomRoles(String[] roles) throws Exception { - if (isVersionMatch("(,24.3]", newClient().build())) { - return; - } - - String password = "^1A" + RandomStringUtils.random(12, true, true) + "3b$"; - final String rolesList = "\"" + Strings.join("\",\"", roles) + "\""; - try (Client client = newClient().build()) { - client.execute("DROP ROLE IF EXISTS " + rolesList).get().close(); - client.execute("CREATE ROLE " + rolesList).get().close(); - client.execute("DROP USER IF EXISTS some_user").get().close(); - client.execute("CREATE USER some_user IDENTIFIED BY '" + password + "'").get().close(); - client.execute("GRANT " + rolesList + " TO some_user").get().close(); - } - - try (Client userClient = newClient().setUsername("some_user").setPassword(password).build()) { - QuerySettings settings = new QuerySettings().setDBRoles(Arrays.asList(roles)); - List resp = userClient.queryAll("SELECT currentRoles()", settings); - Set roleSet = new HashSet<>(Arrays.asList(roles)); - Set currentRoles = new HashSet (resp.get(0).getList(1)); - Assert.assertEquals(currentRoles, roleSet, "Roles " + roleSet + " not found in " + currentRoles); - } - } - - @DataProvider(name = "clientSessionRoles") - private static Object[][] clientSessionRoles() { - return new Object[][]{ - {new String[]{"ROL1", "ROL2"}}, - {new String[]{"ROL1", "ROL2,☺"}}, - }; - } - @Test(groups = {"integration"}, dataProvider = "clientSessionRoles") - public void testClientCustomRoles(String[] roles) throws Exception { - if (isVersionMatch("(,24.3]", newClient().build())) { - return; - } - - String password = "^1A" + RandomStringUtils.random(12, true, true) + "3B$"; - final String rolesList = "\"" + Strings.join("\",\"", roles) + "\""; - try (Client client = newClient().build()) { - client.execute("DROP ROLE IF EXISTS " + rolesList).get().close(); - client.execute("CREATE ROLE " + rolesList).get().close(); - client.execute("DROP USER IF EXISTS some_user").get().close(); - client.execute("CREATE USER some_user IDENTIFIED WITH sha256_password BY '" + password + "'").get().close(); - client.execute("GRANT " + rolesList + " TO some_user").get().close(); - } - - try (Client userClient = newClient().setUsername("some_user").setPassword(password).build()) { - userClient.setDBRoles(Arrays.asList(roles)); - List resp = userClient.queryAll("SELECT currentRoles()"); - Set roleSet = new HashSet<>(Arrays.asList(roles)); - Set currentRoles = new HashSet (resp.get(0).getList(1)); - Assert.assertEquals(currentRoles, roleSet, "Roles " + roleSet + " not found in " + currentRoles); - } - } - - @Test(groups = {"integration"}) - public void testRuntimeCredentialChange() throws Exception { - if (isCloud()) { - return; // creating users is not expected in cloud tests - } - - String user1 = "client_v2_user1_" + RandomStringUtils.random(8, true, true).toLowerCase(); - String user2 = "client_v2_user2_" + RandomStringUtils.random(8, true, true).toLowerCase(); - String password1 = "^1A" + RandomStringUtils.random(12, true, true) + "3b$"; - String password2 = "^1A" + RandomStringUtils.random(12, true, true) + "3B$"; - - try (Client adminClient = newClient().build()) { - try { - adminClient.execute("DROP USER IF EXISTS " + user1).get().close(); - adminClient.execute("DROP USER IF EXISTS " + user2).get().close(); - adminClient.execute("CREATE USER " + user1 + " IDENTIFIED BY '" + password1 + "'").get().close(); - adminClient.execute("CREATE USER " + user2 + " IDENTIFIED BY '" + password2 + "'").get().close(); - - try (Client userClient = newClient().setUsername(user1).setPassword(password1).build()) { - List firstResponse = userClient.queryAll("SELECT currentUser() AS user"); - Assert.assertEquals(firstResponse.get(0).getString("user"), user1); - - userClient.setCredentials(user2, password2); - - List secondResponse = userClient.queryAll("SELECT currentUser() AS user"); - Assert.assertEquals(secondResponse.get(0).getString("user"), user2); - } - } finally { - adminClient.execute("DROP USER IF EXISTS " + user1).get().close(); - adminClient.execute("DROP USER IF EXISTS " + user2).get().close(); - } - } - } - - - @Test(groups = {"integration"}) - public void testLogComment() throws Exception { - - String logComment = "Test log comment"; - QuerySettings settings = new QuerySettings() - .setQueryId(UUID.randomUUID().toString()) - .logComment(logComment); - - try (Client client = newClient().build()) { - - try (QueryResponse response = client.query("SELECT 1", settings).get()) { - Assert.assertNotNull(response.getQueryId()); - Assert.assertTrue(response.getQueryId().startsWith(settings.getQueryId())); - } - - client.execute("SYSTEM FLUSH LOGS").get().close(); - - List logRecords = client.queryAll("SELECT query_id, log_comment FROM clusterAllReplicas('default', system.query_log) WHERE query_id = '" + settings.getQueryId() + "'"); - Assert.assertEquals(logRecords.get(0).getString("query_id"), settings.getQueryId()); - Assert.assertEquals(logRecords.get(0).getString("log_comment"), logComment); - } - } - - @Test(groups = {"integration"}) - public void testServerSettings() throws Exception { - try (Client client = newClient().build()) { - client.execute("DROP TABLE IF EXISTS server_settings_test_table"); - client.execute("CREATE TABLE server_settings_test_table (v Float) Engine MergeTree ORDER BY ()"); - - final String queryId = UUID.randomUUID().toString(); - InsertSettings insertSettings = new InsertSettings() - .setQueryId(queryId) - .serverSetting(ServerSettings.ASYNC_INSERT, "1") - .serverSetting(ServerSettings.WAIT_ASYNC_INSERT, "1") - .serverSetting(ServerSettings.INPUT_FORMAT_BINARY_READ_JSON_AS_STRING, "1"); - - String csvData = "0.33\n0.44\n0.55\n"; - client.insert("server_settings_test_table", new ByteArrayInputStream(csvData.getBytes()), ClickHouseFormat.CSV, insertSettings).get().close(); - - client.execute("SYSTEM FLUSH LOGS").get().close(); - - List logRecords = client.queryAll("SELECT Settings, ProfileEvents['AsyncInsertQuery'] as was_async FROM clusterAllReplicas('default', system.query_log) WHERE query_id = '" + queryId + "' AND type = 'QueryFinish'"); - - GenericRecord record = logRecords.get(0); - Assert.assertTrue(record.getBoolean("was_async")); - String settings = record.getString(record.getSchema().nameToColumnIndex("Settings")); - Assert.assertTrue(settings.contains("input_format_binary_read_json_as_string=1")); -// Assert.assertTrue(settings.contains(ServerSettings.ASYNC_INSERT + "=1")); // async settings are not reflected in query log any more -// Assert.assertTrue(settings.contains(ServerSettings.WAIT_ASYNC_INSERT + "=1")); // uncomment after server fix - } - } - - @Test(groups = {"integration"}) - public void testInvalidEndpoint() { - - try { - new Client.Builder().addEndpoint("http://localhost/default"); - fail("Exception expected"); - } catch (ValidationUtils.SettingsValidationException e) { - Assert.assertTrue(e.getMessage().contains("port")); - } - } - - @Test(groups = {"integration"}) - public void testUnknownClientSettings() throws Exception { - try (Client client = newClient().setOption("unknown_setting", "value").build()) { - Assert.fail("Exception expected"); - } catch (Exception ex) { - Assert.assertTrue(ex instanceof ClientMisconfigurationException); - Assert.assertTrue(ex.getMessage().contains("unknown_setting")); - } - - try (Client client = newClient().setOption(ClientConfigProperties.IGNORE_UNKNOWN_CONFIG_KEY, "true").setOption("unknown_setting", "value").build()) { - Assert.assertTrue(client.ping()); - } - - try (Client client = newClient().setOption(ClientConfigProperties.SERVER_SETTING_PREFIX + "unknown_setting", "value").build()) { - try { - client.execute("SELECT 1"); - Assert.fail("Exception expected"); - } catch (ServerException e) { - Assert.assertEquals(e.getCode(), ServerException.UNKNOWN_SETTING); - } - } - - try (Client client = newClient().setOption(ClientConfigProperties.HTTP_HEADER_PREFIX + "unknown_setting", "value").build()) { - Assert.assertTrue(client.ping()); - } - } - - @Test(groups = {"integration"}) - public void testInvalidConfig() { - try { - newClient().setOption(ClientConfigProperties.CUSTOM_SETTINGS_PREFIX.getKey(), "").build(); - Assert.fail("exception expected"); - } catch (ClientException e) { - Assert.assertTrue(e.getMessage().contains(ClientConfigProperties.CUSTOM_SETTINGS_PREFIX.getKey())); - } - } - - @Test(groups = {"integration"}) - public void testQueryIdGenerator() throws Exception { - final String queryId = UUID.randomUUID().toString(); - Supplier constantQueryIdSupplier = () -> queryId; - - // check getting same UUID - for (int i = 0; i < 3; i++ ) { - try (Client client = newClient().setQueryIdGenerator(constantQueryIdSupplier).build()) { - client.execute("SELECT * FROM unknown_table").get().close(); - } catch (ServerException ex) { - Assert.assertEquals(ex.getCode(), ServerException.ErrorCodes.TABLE_NOT_FOUND.getCode()); - Assert.assertEquals(ex.getQueryId(), queryId); - } - } - - final Queue queryIds = new ConcurrentLinkedQueue<>(); // non-blocking - final Supplier queryIdGen = () -> { - String id = UUID.randomUUID().toString(); - queryIds.add(id); - return id; - }; - int requests = 3; - final Queue actualIds = new ConcurrentLinkedQueue<>(); - for (int i = 0; i < requests; i++ ) { - try (Client client = newClient().setQueryIdGenerator(queryIdGen).build()) { - client.execute("SELECT * FROM unknown_table").get().close(); - } catch (ServerException ex) { - Assert.assertEquals(ex.getCode(), ServerException.ErrorCodes.TABLE_NOT_FOUND.getCode()); - actualIds.add(ex.getQueryId()); - } - } - - Assert.assertEquals(queryIds.size(), requests); - Assert.assertEquals(actualIds, new ArrayList<>(queryIds)); - } - - public boolean isVersionMatch(String versionExpression, Client client) { - List serverVersion = client.queryAll("SELECT version()"); - return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression); - } - - protected Client.Builder newClient() { - ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); - boolean isSecure = isCloud(); - return new Client.Builder() - .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), isSecure) - .setUsername("default") - .setPassword(ClickHouseServerForTest.getPassword()) - .setDefaultDatabase(ClickHouseServerForTest.getDatabase()); - } -} +package com.clickhouse.client; + +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.ClientException; +import com.clickhouse.client.api.ClientFaultCause; +import com.clickhouse.client.api.ClientMisconfigurationException; +import com.clickhouse.client.api.ConnectionReuseStrategy; +import com.clickhouse.client.api.ServerException; +import com.clickhouse.client.api.enums.Protocol; +import com.clickhouse.client.api.insert.InsertSettings; +import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream; +import com.clickhouse.client.api.internal.ServerSettings; +import com.clickhouse.client.api.internal.ValidationUtils; +import com.clickhouse.client.api.metadata.DefaultColumnToMethodMatchingStrategy; +import com.clickhouse.client.api.query.GenericRecord; +import com.clickhouse.client.api.query.QueryResponse; +import com.clickhouse.client.api.query.QuerySettings; +import com.clickhouse.client.api.query.Records; +import com.clickhouse.client.config.ClickHouseClientOption; +import com.clickhouse.data.ClickHouseFormat; +import com.clickhouse.data.ClickHouseVersion; +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.testng.util.Strings; + +import java.io.ByteArrayInputStream; +import java.net.ConnectException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.SECONDS; +import static org.testng.AssertJUnit.fail; + +public class ClientTests extends BaseIntegrationTest { + private static final Logger LOGGER = LoggerFactory.getLogger(ClientTests.class); + + @Test(groups = {"integration"}, dataProvider = "secureClientProvider") + public void testAddSecureEndpoint(Client client) { + if (isCloud()) { + return; // will fail in other tests + } + try { + Optional genericRecord = client + .queryAll("SELECT hostname()").stream().findFirst(); + Assert.assertTrue(genericRecord.isPresent()); + } catch (ClientException e) { + e.printStackTrace(); + if (e.getCause().getCause() instanceof ClickHouseException) { + Exception cause = (Exception) e.getCause().getCause().getCause(); + Assert.assertTrue(cause instanceof ConnectException); + // TODO: correct when SSL support is fully implemented. + Assert.assertTrue(cause.getMessage() + .startsWith("HTTP request failed: PKIX path building failed")); + return; + } + Assert.fail(e.getMessage()); + } finally { + client.close(); + } + } + + @DataProvider(name = "secureClientProvider") + public static Object[][] secureClientProvider() throws Exception { + ClickHouseNode node = ClickHouseServerForTest.getClickHouseNode(ClickHouseProtocol.HTTP, + true, ClickHouseNode.builder() + .addOption(ClickHouseClientOption.SSL_MODE.getKey(), "none") + .addOption(ClickHouseClientOption.SSL.getKey(), "true").build()); + + Client client1; + Client client2; + try { + client1 = new Client.Builder() + .addEndpoint("https://" + node.getHost() + ":" + node.getPort()) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") + .build(); + + client2 = new Client.Builder() + .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), true) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setRootCertificate("containers/clickhouse-server/certs/localhost.crt") + .setClientKey("some_user.key") + .setClientCertificate("some_user.crt") + .build(); + + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + + return new Client[][]{ + { + client1 + }, + { + client2 + } + }; + } + + @Test(groups = {"integration"}) + public void testRawSettings() { + try (Client client = newClient().build()) { + + client.execute("SELECT 1"); + + QuerySettings querySettings = new QuerySettings(); + querySettings.serverSetting("session_timezone", "Europe/Zurich"); + + try (Records response = + client.queryRecords("SELECT timeZone(), serverTimeZone()", querySettings).get(10, TimeUnit.SECONDS)) { + + response.forEach(record -> { + Assert.assertEquals("Europe/Zurich", record.getString(1)); + Assert.assertEquals("UTC", record.getString(2)); + }); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } + } + + @Test(groups = {"integration"}) + public void testCustomSettings() { + if (isCloud()) { + return; // no custom parameters on cloud instance + } + final String CLIENT_OPTION = "custom_client_option"; // prefix should be known from server config + try (Client client = newClient().serverSetting(CLIENT_OPTION, "opt1").build()) { + + final List clientOption = client.queryAll("SELECT getSetting({option_name:String})", + Collections.singletonMap("option_name", CLIENT_OPTION)); + + Assert.assertEquals(clientOption.get(0).getString(1), "opt1"); + + QuerySettings querySettings = new QuerySettings(); + querySettings.serverSetting(CLIENT_OPTION, "opt2"); + + final List requestOption = client.queryAll("SELECT getSetting({option_name:String})", + Collections.singletonMap("option_name", CLIENT_OPTION), querySettings); + + Assert.assertEquals(requestOption.get(0).getString(1), "opt2"); + } + } + + @Test(groups = {"integration"}) + public void testPing() { + try (Client client = newClient().build()) { + Assert.assertTrue(client.ping()); + } + } + + @Test(groups = {"integration"}) + public void testPingUnpooled() { + try (Client client = newClient().enableConnectionPool(false).build()) { + Assert.assertTrue(client.ping()); + } + } + + @Test(groups = {"integration"}) + public void testPingFailure() { + try (Client client = new Client.Builder() + .addEndpoint("http://localhost:12345") + .setUsername("default") + .setPassword("") + .build()) { + Assert.assertFalse(client.ping(TimeUnit.SECONDS.toMillis(20))); + } + } + + @Test(groups = {"integration"}) + public void testPingAsync() { + try (Client client = newClient().useAsyncRequests(true).build()) { + Assert.assertTrue(client.ping()); + } + } + + @Test(groups = {"integration"}) + public void testSetOptions() { + Map options = new HashMap<>(); + String productName = "my product_name (version 1.0)"; + options.put(ClickHouseClientOption.PRODUCT_NAME.getKey(), productName); + try (Client client = newClient() + .setOptions(options).build()) { + + Assert.assertEquals(client.getConfiguration().get(ClickHouseClientOption.PRODUCT_NAME.getKey()), productName); + } + } + + @Test(groups = {"integration"}) + public void testProvidedExecutor() throws Exception { + + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try (Client client = newClient().useAsyncRequests(true).setSharedOperationExecutor(executorService).build()) { + QueryResponse response = client.query("SELECT 1").get(); + response.getMetrics(); + } catch (Exception e) { + Assert.fail("unexpected exception", e); + } + + AtomicBoolean flag = new AtomicBoolean(true); + executorService.submit(() -> flag.compareAndSet(true, false)); + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + + Assert.assertFalse(flag.get()); + } + + @Test(groups = {"integration"}) + public void testLoadingServerContext() throws Exception { + long start = System.nanoTime(); + try (Client client = newClient().build()) { + long initTime = (System.nanoTime() - start) / 1_000_000; + Assert.assertTrue(initTime < 100); + Assert.assertEquals(client.getServerVersion(), "unknown"); + client.loadServerInfo(); + Assert.assertNotNull(client.getServerVersion()); + } + } + + @Test(groups = {"integration"}) + public void testDisableNative() { + try (Client client = newClient().disableNativeCompression(true).build()) { + Assert.assertTrue(client.toString().contains("JavaSafe") || client.toString().contains("JavaUnsafe")); + } + } + + @Test(groups = {"integration"}) + public void testDefaultSettings() { + try (Client client = new Client.Builder().setUsername("default").setPassword("secret") + .addEndpoint("http://localhost:8123").build()) { + Map config = client.getConfiguration(); + for (ClientConfigProperties p : ClientConfigProperties.values()) { + if (p.getDefaultValue() != null) { + Assert.assertTrue(config.containsKey(p.getKey()), "Default value should be set for " + p.getKey()); + Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); + } + } + Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. + } + + try (Client client = new Client.Builder() + .setUsername("default") + .setPassword("secret") + .addEndpoint("http://localhost:8123") + .setDefaultDatabase("mydb") + .setExecutionTimeout(10, MILLIS) + .setLZ4UncompressedBufferSize(300_000) + .disableNativeCompression(true) + .useServerTimeZone(false) + .setServerTimeZone("America/Los_Angeles") + .useTimeZone("America/Los_Angeles") + .useAsyncRequests(true) + .setMaxConnections(330) + .setConnectionRequestTimeout(20, SECONDS) + .setConnectionReuseStrategy(ConnectionReuseStrategy.LIFO) + .enableConnectionPool(false) + .setConnectionTTL(30, SECONDS) + .retryOnFailures(ClientFaultCause.NoHttpResponse) + .setClientNetworkBufferSize(500_000) + .setMaxRetries(10) + .useHTTPBasicAuth(false) + .compressClientRequest(true) + .compressServerResponse(false) + .useHttpCompression(true) + .appCompressedData(true) + .setSocketTimeout(20, SECONDS) + .setSocketRcvbuf(100000) + .setSocketSndbuf(100000) + .build()) { + Map config = client.getConfiguration(); + Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added. + Assert.assertEquals(config.get(ClientConfigProperties.DATABASE.getKey()), "mydb"); + Assert.assertEquals(config.get(ClientConfigProperties.MAX_EXECUTION_TIME.getKey()), "10"); + Assert.assertEquals(config.get(ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getKey()), "300000"); + Assert.assertEquals(config.get(ClientConfigProperties.DISABLE_NATIVE_COMPRESSION.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.USE_SERVER_TIMEZONE.getKey()), "false"); + Assert.assertEquals(config.get(ClientConfigProperties.SERVER_TIMEZONE.getKey()), "America/Los_Angeles"); + Assert.assertEquals(config.get(ClientConfigProperties.ASYNC_OPERATIONS.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.HTTP_MAX_OPEN_CONNECTIONS.getKey()), "330"); + Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_REQUEST_TIMEOUT.getKey()), "20000"); + Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_REUSE_STRATEGY.getKey()), "LIFO"); + Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_POOL_ENABLED.getKey()), "false"); + Assert.assertEquals(config.get(ClientConfigProperties.CONNECTION_TTL.getKey()), "30000"); + Assert.assertEquals(config.get(ClientConfigProperties.CLIENT_RETRY_ON_FAILURE.getKey()), "NoHttpResponse"); + Assert.assertEquals(config.get(ClientConfigProperties.CLIENT_NETWORK_BUFFER_SIZE.getKey()), "500000"); + Assert.assertEquals(config.get(ClientConfigProperties.RETRY_ON_FAILURE.getKey()), "10"); + Assert.assertEquals(config.get(ClientConfigProperties.HTTP_USE_BASIC_AUTH.getKey()), "false"); + Assert.assertEquals(config.get(ClientConfigProperties.COMPRESS_CLIENT_REQUEST.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getKey()), "false"); + Assert.assertEquals(config.get(ClientConfigProperties.USE_HTTP_COMPRESSION.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.APP_COMPRESSED_DATA.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()), "20000"); + Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_RCVBUF_OPT.getKey()), "100000"); + Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_SNDBUF_OPT.getKey()), "100000"); + } + } + + @Test(groups = {"integration"}) + public void testWithOldDefaults() { + try (Client client = new Client.Builder() + .setUsername("default") + .setPassword("seceret") + .addEndpoint("http://localhost:8123") + .setDefaultDatabase("default") + .setExecutionTimeout(0, MILLIS) + .setLZ4UncompressedBufferSize(ClickHouseLZ4OutputStream.UNCOMPRESSED_BUFF_SIZE) + .disableNativeCompression(false) + .useServerTimeZone(true) + .setServerTimeZone("UTC") + .useAsyncRequests(false) + .setMaxConnections(10) + .setConnectionRequestTimeout(10, SECONDS) + .setConnectionReuseStrategy(ConnectionReuseStrategy.FIFO) + .enableConnectionPool(true) + .setConnectionTTL(-1, MILLIS) + .retryOnFailures(ClientFaultCause.NoHttpResponse, ClientFaultCause.ConnectTimeout, + ClientFaultCause.ConnectionRequestTimeout, ClientFaultCause.ServerRetryable) + .setClientNetworkBufferSize(300_000) + .setMaxRetries(3) + .allowBinaryReaderToReuseBuffers(false) + .columnToMethodMatchingStrategy(DefaultColumnToMethodMatchingStrategy.INSTANCE) + .useHTTPBasicAuth(true) + .compressClientRequest(false) + .compressServerResponse(true) + .useHttpCompression(false) + .appCompressedData(false) + .setSocketTimeout(0, SECONDS) + .setSocketRcvbuf(804800) + .setSocketSndbuf(804800) + .build()) { + Map config = client.getConfiguration(); + for (ClientConfigProperties p : ClientConfigProperties.values()) { + if (p.getDefaultValue() != null) { + Assert.assertTrue(config.containsKey(p.getKey()), "Default value should be set for " + p.getKey()); + Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); + } + } + Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. + } + } + + @DataProvider(name = "sessionRoles") + private static Object[][] sessionRoles() { + return new Object[][]{ + {new String[]{"ROL1", "ROL2"}}, + {new String[]{"ROL1", "ROL2"}}, + {new String[]{"ROL1", "ROL2"}}, + {new String[]{"ROL1", "ROL2,☺"}}, + {new String[]{"ROL1", "ROL2"}}, + }; + } + + @Test(groups = {"integration"}, dataProvider = "sessionRoles") + public void testOperationCustomRoles(String[] roles) throws Exception { + if (isVersionMatch("(,24.3]", newClient().build())) { + return; + } + + String password = "^1A" + RandomStringUtils.random(12, true, true) + "3b$"; + final String rolesList = "\"" + Strings.join("\",\"", roles) + "\""; + try (Client client = newClient().build()) { + client.execute("DROP ROLE IF EXISTS " + rolesList).get().close(); + client.execute("CREATE ROLE " + rolesList).get().close(); + client.execute("DROP USER IF EXISTS some_user").get().close(); + client.execute("CREATE USER some_user IDENTIFIED BY '" + password + "'").get().close(); + client.execute("GRANT " + rolesList + " TO some_user").get().close(); + } + + try (Client userClient = newClient().setUsername("some_user").setPassword(password).build()) { + QuerySettings settings = new QuerySettings().setDBRoles(Arrays.asList(roles)); + List resp = userClient.queryAll("SELECT currentRoles()", settings); + Set roleSet = new HashSet<>(Arrays.asList(roles)); + Set currentRoles = new HashSet (resp.get(0).getList(1)); + Assert.assertEquals(currentRoles, roleSet, "Roles " + roleSet + " not found in " + currentRoles); + } + } + + @DataProvider(name = "clientSessionRoles") + private static Object[][] clientSessionRoles() { + return new Object[][]{ + {new String[]{"ROL1", "ROL2"}}, + {new String[]{"ROL1", "ROL2,☺"}}, + }; + } + @Test(groups = {"integration"}, dataProvider = "clientSessionRoles") + public void testClientCustomRoles(String[] roles) throws Exception { + if (isVersionMatch("(,24.3]", newClient().build())) { + return; + } + + String password = "^1A" + RandomStringUtils.random(12, true, true) + "3B$"; + final String rolesList = "\"" + Strings.join("\",\"", roles) + "\""; + try (Client client = newClient().build()) { + client.execute("DROP ROLE IF EXISTS " + rolesList).get().close(); + client.execute("CREATE ROLE " + rolesList).get().close(); + client.execute("DROP USER IF EXISTS some_user").get().close(); + client.execute("CREATE USER some_user IDENTIFIED WITH sha256_password BY '" + password + "'").get().close(); + client.execute("GRANT " + rolesList + " TO some_user").get().close(); + } + + try (Client userClient = newClient().setUsername("some_user").setPassword(password).build()) { + userClient.setDBRoles(Arrays.asList(roles)); + List resp = userClient.queryAll("SELECT currentRoles()"); + Set roleSet = new HashSet<>(Arrays.asList(roles)); + Set currentRoles = new HashSet (resp.get(0).getList(1)); + Assert.assertEquals(currentRoles, roleSet, "Roles " + roleSet + " not found in " + currentRoles); + } + } + + @Test(groups = {"integration"}) + public void testRuntimeCredentialChange() throws Exception { + if (isCloud()) { + return; // creating users is not expected in cloud tests + } + + String user1 = "client_v2_user1_" + RandomStringUtils.random(8, true, true).toLowerCase(); + String user2 = "client_v2_user2_" + RandomStringUtils.random(8, true, true).toLowerCase(); + String password1 = "^1A" + RandomStringUtils.random(12, true, true) + "3b$"; + String password2 = "^1A" + RandomStringUtils.random(12, true, true) + "3B$"; + + try (Client adminClient = newClient().build()) { + try { + adminClient.execute("DROP USER IF EXISTS " + user1).get().close(); + adminClient.execute("DROP USER IF EXISTS " + user2).get().close(); + adminClient.execute("CREATE USER " + user1 + " IDENTIFIED BY '" + password1 + "'").get().close(); + adminClient.execute("CREATE USER " + user2 + " IDENTIFIED BY '" + password2 + "'").get().close(); + + try (Client userClient = newClient().setUsername(user1).setPassword(password1).build()) { + List firstResponse = userClient.queryAll("SELECT currentUser() AS user"); + Assert.assertEquals(firstResponse.get(0).getString("user"), user1); + + userClient.setCredentials(user2, password2); + + List secondResponse = userClient.queryAll("SELECT currentUser() AS user"); + Assert.assertEquals(secondResponse.get(0).getString("user"), user2); + } + } finally { + adminClient.execute("DROP USER IF EXISTS " + user1).get().close(); + adminClient.execute("DROP USER IF EXISTS " + user2).get().close(); + } + } + } + + + @Test(groups = {"integration"}) + public void testLogComment() throws Exception { + + String logComment = "Test log comment"; + QuerySettings settings = new QuerySettings() + .setQueryId(UUID.randomUUID().toString()) + .logComment(logComment); + + try (Client client = newClient().build()) { + + try (QueryResponse response = client.query("SELECT 1", settings).get()) { + Assert.assertNotNull(response.getQueryId()); + Assert.assertTrue(response.getQueryId().startsWith(settings.getQueryId())); + } + + client.execute("SYSTEM FLUSH LOGS").get().close(); + + List logRecords = client.queryAll("SELECT query_id, log_comment FROM clusterAllReplicas('default', system.query_log) WHERE query_id = '" + settings.getQueryId() + "'"); + Assert.assertEquals(logRecords.get(0).getString("query_id"), settings.getQueryId()); + Assert.assertEquals(logRecords.get(0).getString("log_comment"), logComment); + } + } + + @Test(groups = {"integration"}) + public void testServerSettings() throws Exception { + try (Client client = newClient().build()) { + client.execute("DROP TABLE IF EXISTS server_settings_test_table"); + client.execute("CREATE TABLE server_settings_test_table (v Float) Engine MergeTree ORDER BY ()"); + + final String queryId = UUID.randomUUID().toString(); + InsertSettings insertSettings = new InsertSettings() + .setQueryId(queryId) + .serverSetting(ServerSettings.ASYNC_INSERT, "1") + .serverSetting(ServerSettings.WAIT_ASYNC_INSERT, "1") + .serverSetting(ServerSettings.INPUT_FORMAT_BINARY_READ_JSON_AS_STRING, "1"); + + String csvData = "0.33\n0.44\n0.55\n"; + client.insert("server_settings_test_table", new ByteArrayInputStream(csvData.getBytes()), ClickHouseFormat.CSV, insertSettings).get().close(); + + client.execute("SYSTEM FLUSH LOGS").get().close(); + + List logRecords = client.queryAll("SELECT Settings, ProfileEvents['AsyncInsertQuery'] as was_async FROM clusterAllReplicas('default', system.query_log) WHERE query_id = '" + queryId + "' AND type = 'QueryFinish'"); + + GenericRecord record = logRecords.get(0); + Assert.assertTrue(record.getBoolean("was_async")); + String settings = record.getString(record.getSchema().nameToColumnIndex("Settings")); + Assert.assertTrue(settings.contains("input_format_binary_read_json_as_string=1")); +// Assert.assertTrue(settings.contains(ServerSettings.ASYNC_INSERT + "=1")); // async settings are not reflected in query log any more +// Assert.assertTrue(settings.contains(ServerSettings.WAIT_ASYNC_INSERT + "=1")); // uncomment after server fix + } + } + + @Test(groups = {"integration"}) + public void testInvalidEndpoint() { + + try { + new Client.Builder().addEndpoint("http://localhost/default"); + fail("Exception expected"); + } catch (ValidationUtils.SettingsValidationException e) { + Assert.assertTrue(e.getMessage().contains("port")); + } + } + + @Test(groups = {"integration"}) + public void testUnknownClientSettings() throws Exception { + try (Client client = newClient().setOption("unknown_setting", "value").build()) { + Assert.fail("Exception expected"); + } catch (Exception ex) { + Assert.assertTrue(ex instanceof ClientMisconfigurationException); + Assert.assertTrue(ex.getMessage().contains("unknown_setting")); + } + + try (Client client = newClient().setOption(ClientConfigProperties.IGNORE_UNKNOWN_CONFIG_KEY, "true").setOption("unknown_setting", "value").build()) { + Assert.assertTrue(client.ping()); + } + + try (Client client = newClient().setOption(ClientConfigProperties.SERVER_SETTING_PREFIX + "unknown_setting", "value").build()) { + try { + client.execute("SELECT 1"); + Assert.fail("Exception expected"); + } catch (ServerException e) { + Assert.assertEquals(e.getCode(), ServerException.UNKNOWN_SETTING); + } + } + + try (Client client = newClient().setOption(ClientConfigProperties.HTTP_HEADER_PREFIX + "unknown_setting", "value").build()) { + Assert.assertTrue(client.ping()); + } + } + + @Test(groups = {"integration"}) + public void testInvalidConfig() { + try { + newClient().setOption(ClientConfigProperties.CUSTOM_SETTINGS_PREFIX.getKey(), "").build(); + Assert.fail("exception expected"); + } catch (ClientException e) { + Assert.assertTrue(e.getMessage().contains(ClientConfigProperties.CUSTOM_SETTINGS_PREFIX.getKey())); + } + } + + @Test(groups = {"integration"}) + public void testQueryIdGenerator() throws Exception { + final String queryId = UUID.randomUUID().toString(); + Supplier constantQueryIdSupplier = () -> queryId; + + // check getting same UUID + for (int i = 0; i < 3; i++ ) { + try (Client client = newClient().setQueryIdGenerator(constantQueryIdSupplier).build()) { + client.execute("SELECT * FROM unknown_table").get().close(); + } catch (ServerException ex) { + Assert.assertEquals(ex.getCode(), ServerException.ErrorCodes.TABLE_NOT_FOUND.getCode()); + Assert.assertEquals(ex.getQueryId(), queryId); + } + } + + final Queue queryIds = new ConcurrentLinkedQueue<>(); // non-blocking + final Supplier queryIdGen = () -> { + String id = UUID.randomUUID().toString(); + queryIds.add(id); + return id; + }; + int requests = 3; + final Queue actualIds = new ConcurrentLinkedQueue<>(); + for (int i = 0; i < requests; i++ ) { + try (Client client = newClient().setQueryIdGenerator(queryIdGen).build()) { + client.execute("SELECT * FROM unknown_table").get().close(); + } catch (ServerException ex) { + Assert.assertEquals(ex.getCode(), ServerException.ErrorCodes.TABLE_NOT_FOUND.getCode()); + actualIds.add(ex.getQueryId()); + } + } + + Assert.assertEquals(queryIds.size(), requests); + Assert.assertEquals(actualIds, new ArrayList<>(queryIds)); + } + + public boolean isVersionMatch(String versionExpression, Client client) { + List serverVersion = client.queryAll("SELECT version()"); + return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression); + } + + protected Client.Builder newClient() { + ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); + boolean isSecure = isCloud(); + return new Client.Builder() + .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), isSecure) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setDefaultDatabase(ClickHouseServerForTest.getDatabase()); + } +} From 32706eb6db90c396abc1a6b944000352168975a5 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 7 Apr 2026 14:56:58 -0700 Subject: [PATCH 3/5] fixed test --- .../test/java/com/clickhouse/client/HttpTransportTests.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 f0ef2a958..703a5695b 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -4,6 +4,7 @@ import com.clickhouse.client.api.ClientConfigProperties; import com.clickhouse.client.api.ClientException; import com.clickhouse.client.api.ClientFaultCause; +import com.clickhouse.client.api.ClientMisconfigurationException; import com.clickhouse.client.api.ConnectionInitiationException; import com.clickhouse.client.api.ConnectionReuseStrategy; import com.clickhouse.client.api.ServerException; @@ -777,9 +778,9 @@ public void testSSLAuthentication_invalidConfig() throws Exception { .compressServerResponse(false) .build()) { fail("Expected exception"); - } catch (IllegalArgumentException e) { + } catch (ClientMisconfigurationException e) { e.printStackTrace(); - Assert.assertTrue(e.getMessage().startsWith("Only one of password, access token or SSL authentication")); + Assert.assertTrue(e.getMessage().startsWith("Only one of password, access token or SSL authentication")); } } From 73aa2a5e4d33585d2e03a4a12f4cd67e801f36b8 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 7 Apr 2026 18:27:42 -0700 Subject: [PATCH 4/5] Implemented credentials manager as self entity controlling credentials --- .../com/clickhouse/client/api/Client.java | 34 ++-- .../api/internal/CredentialsManager.java | 115 ++++++++++---- .../api/internal/CredentialsManagerTest.java | 149 ++++++++++++++++++ docs/features.md | 2 + 4 files changed, 261 insertions(+), 39 deletions(-) create mode 100644 client-v2/src/test/java/com/clickhouse/client/api/internal/CredentialsManagerTest.java 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 830572eff..de2196858 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 @@ -144,9 +144,9 @@ public class Client implements AutoCloseable { private Client(Collection endpoints, Map configuration, ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy, - Object metricsRegistry, Supplier queryIdGenerator) { + Object metricsRegistry, Supplier queryIdGenerator, CredentialsManager cManager) { this.configuration = new ConcurrentHashMap<>(ClientConfigProperties.parseConfigMap(configuration)); - this.credentialsManager = new CredentialsManager(this.configuration); + this.credentialsManager = cManager; this.readOnlyConfig = Collections.unmodifiableMap(configuration); this.metricsRegistry = metricsRegistry; this.queryIdGenerator = queryIdGenerator; @@ -194,7 +194,7 @@ private Client(Collection endpoints, Map configuration, this.httpClientHelper = new HttpAPIClientHelper(this.configuration, metricsRegistry, initSslContext, lz4Factory); this.serverVersion = configuration.getOrDefault(ClientConfigProperties.SERVER_VERSION.getKey(), "unknown"); - this.dbUser = configuration.getOrDefault(ClientConfigProperties.USER.getKey(), ClientConfigProperties.USER.getDefObjVal()); + this.dbUser = credentialsManager.getUsername(); this.typeHintMapping = (Map>) this.configuration.get(ClientConfigProperties.TYPE_HINT_MAPPING.getKey()); } @@ -1086,10 +1086,7 @@ public Client build() { throw new IllegalArgumentException("At least one endpoint is required"); } - ClientMisconfigurationException authConfigException = CredentialsManager.validateAuthConfig(configuration); - if (authConfigException != null) { - throw authConfigException; - } + CredentialsManager cManager = new CredentialsManager(this.configuration); // Check timezone settings String useTimeZoneValue = this.configuration.get(ClientConfigProperties.USE_TIMEZONE.getKey()); @@ -1120,7 +1117,7 @@ public Client build() { } return new Client(this.endpoints, this.configuration, this.sharedOperationExecutor, - this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator); + this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, cManager); } } @@ -2121,6 +2118,16 @@ public Collection getDBRoles() { return unmodifiableDbRolesView; } + /** + * Updates the credentials used for subsequent requests. + * + *

This method is not thread-safe with respect to other credential updates + * or concurrent request execution. Applications must coordinate access if + * they require stronger consistency. + * + * @param username username to use for subsequent requests + * @param password password to use for subsequent requests + */ public void setCredentials(String username, String password) { this.credentialsManager.setCredentials(username, password); } @@ -2129,6 +2136,10 @@ public void setCredentials(String username, String password) { * Preferred runtime API to update token-based authentication. * Internally it refreshes the HTTP Bearer token used by requests. * + *

This method is not thread-safe with respect to other credential updates + * or concurrent request execution. Applications must coordinate access if + * they require stronger consistency. + * * @param accessToken - plain text access token */ public void setAccessToken(String accessToken) { @@ -2139,6 +2150,10 @@ public void setAccessToken(String accessToken) { * Legacy HTTP-specific alias for {@link #setAccessToken(String)}. * Prefer using {@link #setAccessToken(String)}. * + *

This method is not thread-safe with respect to other credential updates + * or concurrent request execution. Applications must coordinate access if + * they require stronger consistency. + * * @param bearer - token to use */ public void updateBearerToken(String bearer) { @@ -2158,7 +2173,8 @@ private Endpoint getNextAliveNode() { * @return request settings - merged client and operation settings */ private Map buildRequestSettings(Map opSettings) { - Map requestSettings = credentialsManager.snapshot(); + Map requestSettings = new HashMap<>(configuration); + credentialsManager.applyCredentials(requestSettings); requestSettings.putAll(opSettings); return requestSettings; } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/CredentialsManager.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/CredentialsManager.java index 5f53caf51..7d975e480 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/CredentialsManager.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/CredentialsManager.java @@ -9,68 +9,123 @@ /** * Manages mutable authentication-related client settings. + * + *

This class is not thread-safe. Callers are responsible for coordinating + * credential updates with request execution if they need stronger consistency. */ public class CredentialsManager { - private final Map configuration; - private final Object lock = new Object(); + private static final String AUTHORIZATION_HEADER_KEY = + ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION); - public CredentialsManager(Map configuration) { - this.configuration = configuration; + private String username; + private String password; + private String accessToken; + private String authorizationHeader; + private boolean useSslAuth; + + public CredentialsManager(Map configuration) { + validateAuthConfig(configuration); + + this.username = configuration.get(ClientConfigProperties.USER.getKey()); + this.password = configuration.get(ClientConfigProperties.PASSWORD.getKey()); + this.accessToken = readAccessToken(configuration); + this.authorizationHeader = readAuthorizationHeader(configuration, accessToken); + this.useSslAuth = MapUtils.getFlag(configuration, ClientConfigProperties.SSL_AUTH.getKey(), false); } public Map snapshot() { - synchronized (lock) { - return new HashMap<>(configuration); + Map snapshot = new HashMap<>(); + applyCredentials(snapshot); + return snapshot; + } + + public void applyCredentials(Map target) { + putIfNotNull(target, ClientConfigProperties.USER.getKey(), username); + putIfNotNull(target, ClientConfigProperties.PASSWORD.getKey(), password); + putIfNotNull(target, ClientConfigProperties.ACCESS_TOKEN.getKey(), accessToken); + putIfNotNull(target, AUTHORIZATION_HEADER_KEY, authorizationHeader); + if (useSslAuth) { + target.put(ClientConfigProperties.SSL_AUTH.getKey(), Boolean.TRUE); } } + /** + * Replaces the current username/password credentials. + * + *

This class does not synchronize credential updates. Callers must + * serialize updates and request execution if they require thread safety. + */ public void setCredentials(String username, String password) { - synchronized (lock) { - configuration.put(ClientConfigProperties.USER.getKey(), username); - configuration.put(ClientConfigProperties.PASSWORD.getKey(), password); - configuration.put(ClientConfigProperties.SSL_AUTH.getKey(), Boolean.FALSE); - configuration.remove(ClientConfigProperties.ACCESS_TOKEN.getKey()); - configuration.remove(ClientConfigProperties.BEARERTOKEN_AUTH.getKey()); - configuration.remove(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION)); - } + this.username = username; + this.password = password; + this.useSslAuth = false; + this.accessToken = null; + this.authorizationHeader = null; } + /** + * Replaces the current credentials with a bearer token. + * + *

This class does not synchronize credential updates. Callers must + * serialize updates and request execution if they require thread safety. + */ public void setAccessToken(String accessToken) { - synchronized (lock) { - configuration.put(ClientConfigProperties.ACCESS_TOKEN.getKey(), accessToken); - configuration.put(ClientConfigProperties.SSL_AUTH.getKey(), Boolean.FALSE); - configuration.remove(ClientConfigProperties.BEARERTOKEN_AUTH.getKey()); - configuration.remove(ClientConfigProperties.USER.getKey()); - configuration.remove(ClientConfigProperties.PASSWORD.getKey()); - configuration.put(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION), "Bearer " + accessToken); - } + this.accessToken = accessToken; + this.authorizationHeader = accessToken == null ? null : "Bearer " + accessToken; + this.useSslAuth = false; + this.username = null; + this.password = null; + } + + public String getUsername() { + return username == null ? ClientConfigProperties.USER.getDefObjVal() : username; } - public static ClientMisconfigurationException validateAuthConfig(Map configuration) { + public static void validateAuthConfig(Map configuration) throws ClientMisconfigurationException { // check if username and password are empty. so can not initiate client? - boolean useSslAuth = MapUtils.getFlag(configuration, ClientConfigProperties.SSL_AUTH.getKey()); + boolean useSslAuth = MapUtils.getFlag(configuration, ClientConfigProperties.SSL_AUTH.getKey(), false); boolean hasAccessToken = configuration.containsKey(ClientConfigProperties.ACCESS_TOKEN.getKey()); boolean hasUser = configuration.containsKey(ClientConfigProperties.USER.getKey()); boolean hasPassword = configuration.containsKey(ClientConfigProperties.PASSWORD.getKey()); - boolean customHttpHeaders = configuration.containsKey(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION)); + boolean customHttpHeaders = configuration.containsKey(AUTHORIZATION_HEADER_KEY); if (!(useSslAuth || hasAccessToken || hasUser || hasPassword || customHttpHeaders)) { - return new ClientMisconfigurationException("Username and password (or access token or SSL authentication or pre-define Authorization header) are required"); + throw new ClientMisconfigurationException("Username and password (or access token or SSL authentication or pre-define Authorization header) are required"); } if (useSslAuth && (hasAccessToken || hasPassword)) { - return new ClientMisconfigurationException("Only one of password, access token or SSL authentication can be used per client."); + throw new ClientMisconfigurationException("Only one of password, access token or SSL authentication can be used per client."); } if (useSslAuth && !configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) { - return new ClientMisconfigurationException("SSL authentication requires a client certificate"); + throw new ClientMisconfigurationException("SSL authentication requires a client certificate"); } if (configuration.containsKey(ClientConfigProperties.SSL_TRUST_STORE.getKey()) && configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) { - return new ClientMisconfigurationException("Trust store and certificates cannot be used together"); + throw new ClientMisconfigurationException("Trust store and certificates cannot be used together"); } + } + + private static String readAccessToken(Map configuration) { + Object accessToken = configuration.get(ClientConfigProperties.ACCESS_TOKEN.getKey()); + if (accessToken == null) { + accessToken = configuration.get(ClientConfigProperties.BEARERTOKEN_AUTH.getKey()); + } + return accessToken == null ? null : String.valueOf(accessToken); + } - return null; + private static String readAuthorizationHeader(Map configuration, String accessToken) { + Object configuredHeader = configuration.get(AUTHORIZATION_HEADER_KEY); + if (configuredHeader != null) { + return String.valueOf(configuredHeader); + } + return accessToken == null ? null : "Bearer " + accessToken; + } + + private static void putIfNotNull(Map configuration, String key, Object value) { + if (value != null) { + configuration.put(key, value); + } } } diff --git a/client-v2/src/test/java/com/clickhouse/client/api/internal/CredentialsManagerTest.java b/client-v2/src/test/java/com/clickhouse/client/api/internal/CredentialsManagerTest.java new file mode 100644 index 000000000..a10457483 --- /dev/null +++ b/client-v2/src/test/java/com/clickhouse/client/api/internal/CredentialsManagerTest.java @@ -0,0 +1,149 @@ +package com.clickhouse.client.api.internal; + +import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.ClientMisconfigurationException; +import org.apache.hc.core5.http.HttpHeaders; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.HashMap; +import java.util.Map; + +public class CredentialsManagerTest { + private static final String AUTHORIZATION_HEADER_KEY = + ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION); + + @DataProvider(name = "conflictingAuthConfig") + public Object[][] conflictingAuthConfig() { + return new Object[][]{ + {ClientConfigProperties.ACCESS_TOKEN.getKey(), "token"}, + {ClientConfigProperties.PASSWORD.getKey(), "password"} + }; + } + + @Test(groups = {"unit"}) + public void testValidateAuthConfigRejectsMissingAuthenticationConfiguration() { + ClientMisconfigurationException exception = Assert.expectThrows(ClientMisconfigurationException.class, + () -> CredentialsManager.validateAuthConfig(new HashMap<>())); + + Assert.assertTrue(exception.getMessage().contains("required")); + } + + @Test(groups = {"unit"}, dataProvider = "conflictingAuthConfig") + public void testValidateAuthConfigRejectsSslAuthCombinedWithAnotherCredential(String conflictingKey, + String conflictingValue) { + Map configuration = new HashMap<>(); + configuration.put(ClientConfigProperties.SSL_AUTH.getKey(), Boolean.TRUE.toString()); + configuration.put(conflictingKey, conflictingValue); + + ClientMisconfigurationException exception = Assert.expectThrows(ClientMisconfigurationException.class, + () -> CredentialsManager.validateAuthConfig(configuration)); + + Assert.assertEquals(exception.getMessage(), + "Only one of password, access token or SSL authentication can be used per client."); + } + + @Test(groups = {"unit"}) + public void testValidateAuthConfigRejectsSslAuthWithoutCertificate() { + Map configuration = new HashMap<>(); + configuration.put(ClientConfigProperties.SSL_AUTH.getKey(), Boolean.TRUE.toString()); + + ClientMisconfigurationException exception = Assert.expectThrows(ClientMisconfigurationException.class, + () -> CredentialsManager.validateAuthConfig(configuration)); + + Assert.assertEquals(exception.getMessage(), "SSL authentication requires a client certificate"); + } + + @Test(groups = {"unit"}) + public void testValidateAuthConfigRejectsTrustStoreAndCertificateTogether() { + Map configuration = new HashMap<>(); + configuration.put(ClientConfigProperties.USER.getKey(), "user"); + configuration.put(ClientConfigProperties.SSL_TRUST_STORE.getKey(), "trust-store.jks"); + configuration.put(ClientConfigProperties.SSL_CERTIFICATE.getKey(), "client-cert.pem"); + + ClientMisconfigurationException exception = Assert.expectThrows(ClientMisconfigurationException.class, + () -> CredentialsManager.validateAuthConfig(configuration)); + + Assert.assertEquals(exception.getMessage(), "Trust store and certificates cannot be used together"); + } + + @Test(groups = {"unit"}) + public void testConstructorReadsInitialCredentialsWithoutChangingSourceConfiguration() { + Map configuration = new HashMap<>(); + configuration.put(ClientConfigProperties.USER.getKey(), "user"); + configuration.put(ClientConfigProperties.PASSWORD.getKey(), "password"); + + CredentialsManager credentialsManager = new CredentialsManager(configuration); + + Map snapshot = credentialsManager.snapshot(); + Assert.assertEquals(snapshot.get(ClientConfigProperties.USER.getKey()), "user"); + Assert.assertEquals(snapshot.get(ClientConfigProperties.PASSWORD.getKey()), "password"); + Assert.assertEquals(configuration.get(ClientConfigProperties.USER.getKey()), "user"); + Assert.assertEquals(configuration.get(ClientConfigProperties.PASSWORD.getKey()), "password"); + } + + @Test(groups = {"unit"}) + public void testConstructorMaterializesAccessTokenAsAuthorizationHeader() { + Map configuration = new HashMap<>(); + configuration.put(ClientConfigProperties.ACCESS_TOKEN.getKey(), "token"); + + CredentialsManager credentialsManager = new CredentialsManager(configuration); + + Map snapshot = credentialsManager.snapshot(); + Assert.assertEquals(snapshot.get(ClientConfigProperties.ACCESS_TOKEN.getKey()), "token"); + Assert.assertEquals(snapshot.get(AUTHORIZATION_HEADER_KEY), "Bearer token"); + Assert.assertEquals(configuration.get(ClientConfigProperties.ACCESS_TOKEN.getKey()), "token"); + } + + @Test(groups = {"unit"}) + public void testConstructorReadsSslAuthFlagFromStringConfiguration() { + Map configuration = new HashMap<>(); + configuration.put(ClientConfigProperties.USER.getKey(), "user"); + configuration.put(ClientConfigProperties.SSL_AUTH.getKey(), Boolean.TRUE.toString()); + configuration.put(ClientConfigProperties.SSL_CERTIFICATE.getKey(), "--- certificate goes here --"); + + CredentialsManager credentialsManager = new CredentialsManager(configuration); + + Map snapshot = credentialsManager.snapshot(); + Assert.assertEquals(snapshot.get(ClientConfigProperties.USER.getKey()), "user"); + Assert.assertEquals(snapshot.get(ClientConfigProperties.SSL_AUTH.getKey()), Boolean.TRUE); + } + + @Test(groups = {"unit"}) + public void testApplyCredentialsDoesNotResetCallerProvidedTargetEntries() { + Map configuration = new HashMap<>(); + configuration.put(ClientConfigProperties.USER.getKey(), "user"); + configuration.put(ClientConfigProperties.PASSWORD.getKey(), "password"); + + CredentialsManager credentialsManager = new CredentialsManager(configuration); + Map requestSettings = new HashMap<>(); + requestSettings.put(ClientConfigProperties.ACCESS_TOKEN.getKey(), "old-token"); + requestSettings.put(AUTHORIZATION_HEADER_KEY, "Bearer old-token"); + requestSettings.put(ClientConfigProperties.SSL_AUTH.getKey(), Boolean.TRUE); + + credentialsManager.applyCredentials(requestSettings); + + Assert.assertEquals(requestSettings.get(ClientConfigProperties.USER.getKey()), "user"); + Assert.assertEquals(requestSettings.get(ClientConfigProperties.PASSWORD.getKey()), "password"); + Assert.assertEquals(requestSettings.get(ClientConfigProperties.ACCESS_TOKEN.getKey()), "old-token"); + Assert.assertEquals(requestSettings.get(AUTHORIZATION_HEADER_KEY), "Bearer old-token"); + Assert.assertEquals(requestSettings.get(ClientConfigProperties.SSL_AUTH.getKey()), Boolean.TRUE); + } + + @Test(groups = {"unit"}) + public void testSetAccessTokenClearsUsernameAndPasswordCredentials() { + Map configuration = new HashMap<>(); + configuration.put(ClientConfigProperties.USER.getKey(), "user"); + configuration.put(ClientConfigProperties.PASSWORD.getKey(), "password"); + + CredentialsManager credentialsManager = new CredentialsManager(configuration); + credentialsManager.setAccessToken("token"); + + Map snapshot = credentialsManager.snapshot(); + Assert.assertEquals(snapshot.get(ClientConfigProperties.ACCESS_TOKEN.getKey()), "token"); + Assert.assertEquals(snapshot.get(AUTHORIZATION_HEADER_KEY), "Bearer token"); + Assert.assertFalse(snapshot.containsKey(ClientConfigProperties.USER.getKey())); + Assert.assertFalse(snapshot.containsKey(ClientConfigProperties.PASSWORD.getKey())); + } +} diff --git a/docs/features.md b/docs/features.md index 858ff603d..e154bb6b4 100644 --- a/docs/features.md +++ b/docs/features.md @@ -7,6 +7,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t - HTTP and HTTPS connectivity: Connects to ClickHouse over HTTP(S), supports endpoint paths, and exposes a basic `ping` health check. - TLS configuration: Supports trust stores, client certificates/keys, SSL certificate authentication, and SNI for HTTPS connections. - Authentication modes: Supports username/password credentials, ClickHouse auth headers, bearer tokens, and optional HTTP Basic authentication. +- Runtime credential updates: Existing `Client` instances can update username/password or bearer-token credentials for subsequent requests without rebuilding the client. - Proxy support: Can send requests through configured HTTP proxies, including proxy credentials. - Connection and socket tuning: Exposes pool sizing, keep-alive, reuse strategy, connect/request/socket timeouts, and low-level socket options. - Query execution: Executes SQL asynchronously and returns streaming query responses with response metadata and metrics. @@ -30,6 +31,7 @@ Compatibility-sensitive traits: - Named parameter typing is part of the contract: placeholders are written as `{name:Type}` and the supplied value must match the expected ClickHouse textual representation for that type. - String query parameters are expected to round-trip correctly for ordinary text, Unicode, slashes, dashes, and leading or trailing spaces. +- Runtime authentication changes are compatibility-sensitive: after `setCredentials()` or `setAccessToken()`, subsequent requests from the same `Client` are expected to use the updated credentials. - String escaping behavior in `SQLUtils` is compatibility-sensitive: `enquoteLiteral()` uses SQL-style doubled single quotes, while `escapeSingleQuotes()` escapes both backslashes and single quotes with backslashes. - Identifier quoting behavior is stable API for helper callers: identifiers are double-quoted, embedded double quotes are doubled, and optional quoting keeps simple identifiers unchanged. - Instant formatting is type-sensitive and should not drift: `Date` formatting depends on an explicit timezone, `DateTime` is serialized as epoch seconds, and higher-precision timestamps preserve up to 9 fractional digits. From bd7579167f7d1323e4eb77fb3a076a5c4d83ab06 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 7 Apr 2026 18:38:18 -0700 Subject: [PATCH 5/5] added usage example --- examples/client-v2/README.md | 24 +++- .../client_v2/RuntimeCredentialsTwoUsers.java | 105 ++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/RuntimeCredentialsTwoUsers.java diff --git a/examples/client-v2/README.md b/examples/client-v2/README.md index 8c6918228..5a56f2050 100644 --- a/examples/client-v2/README.md +++ b/examples/client-v2/README.md @@ -22,4 +22,26 @@ Addition options can be passed to the application: - `-DchEndpoint` - Endpoint to connect in the format of URL (default: http://localhost:8123/) - `-DchUser` - ClickHouse user name (default: default) - `-DchPassword` - ClickHouse user password (default: empty) -- `-DchDatabase` - ClickHouse database name (default: default) \ No newline at end of file +- `-DchDatabase` - ClickHouse database name (default: default) + +## Runtime Credentials Switch Demo (Two Users) + +This standalone example creates two users and demonstrates switching credentials +on the same `Client` instance at runtime via `setCredentials()`. + +Run it with endpoint only (admin user defaults to `default`): + +```shell +mvn exec:java -Dexec.mainClass="com.clickhouse.examples.client_v2.RuntimeCredentialsTwoUsers" -Dexec.args="http://localhost:8123" +``` + +Run it with explicit admin credentials: + +```shell +mvn exec:java -Dexec.mainClass="com.clickhouse.examples.client_v2.RuntimeCredentialsTwoUsers" -Dexec.args="http://localhost:8123 admin_user admin_password" +``` + +Notes: +- First argument is server location (endpoint). +- Example uses admin credentials to `CREATE USER` and `DROP USER`. +- Optional database can be overridden with `-DchDatabase=`. \ No newline at end of file diff --git a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/RuntimeCredentialsTwoUsers.java b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/RuntimeCredentialsTwoUsers.java new file mode 100644 index 000000000..bf5aad349 --- /dev/null +++ b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/RuntimeCredentialsTwoUsers.java @@ -0,0 +1,105 @@ +package com.clickhouse.examples.client_v2; + +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.query.GenericRecord; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Standalone demo for runtime credential updates using two created users. + * + *

Usage:

+ *
+ *   mvn exec:java \
+ *     -Dexec.mainClass="com.clickhouse.examples.client_v2.RuntimeCredentialsTwoUsers" \
+ *     -Dexec.args="http://localhost:8123 [adminUser] [adminPassword]"
+ * 
+ */ +@Slf4j +public class RuntimeCredentialsTwoUsers { + + private static final String DEFAULT_ENDPOINT = "http://localhost:8123"; + private static final String DEFAULT_DATABASE = "default"; + private static final String DEFAULT_ADMIN_USER = "default"; + private static final String DEFAULT_ADMIN_PASSWORD = ""; + + public static void main(String[] args) { + String endpoint = args.length > 0 ? args[0] : DEFAULT_ENDPOINT; + String adminUser = args.length > 1 ? args[1] : DEFAULT_ADMIN_USER; + String adminPassword = args.length > 2 ? args[2] : DEFAULT_ADMIN_PASSWORD; + String database = System.getProperty("chDatabase", DEFAULT_DATABASE); + + String suffix = String.valueOf(System.currentTimeMillis()); + String firstUser = "runtime_user_a_" + suffix; + String secondUser = "runtime_user_b_" + suffix; + String firstPassword = "pwdA_" + suffix; + String secondPassword = "pwdB_" + suffix; + + log.info("Endpoint: {}", endpoint); + log.info("Creating two demo users: {} and {}", firstUser, secondUser); + + try (Client adminClient = createClient(endpoint, database, adminUser, adminPassword)) { + + // Pre-create the two users + createUser(adminClient, firstUser, firstPassword); + createUser(adminClient, secondUser, secondPassword); + + // Create a client with the first user. (It is recommended to use non-existing users for security reasons) + try (Client client = createClient(endpoint, database, firstUser, firstPassword)) { + + // Print the current user by executing a query `SELECT currentUser()` + printCurrentUser(client, "Initial user"); + + // Switch to the second user + client.setCredentials(secondUser, secondPassword); + // Print the current user by executing a query `SELECT currentUser()` + printCurrentUser(client, "After switch to second user"); + + // Switch back to the first user + client.setCredentials(firstUser, firstPassword); + // Print the current user by executing a query `SELECT currentUser()` + printCurrentUser(client, "After switch back to first user"); + } finally { + dropUser(adminClient, firstUser); + dropUser(adminClient, secondUser); + } + } catch (Exception e) { + log.error("Runtime credentials example failed. Ensure admin user can CREATE/DROP USER.", e); + Runtime.getRuntime().exit(1); + } + } + + private static void createUser(Client adminClient, String user, String password) throws Exception { + runCommand(adminClient, "CREATE USER IF NOT EXISTS " + user + " IDENTIFIED BY '" + password + "'"); + runCommand(adminClient, "GRANT SELECT ON system.one TO " + user); + } + + private static void dropUser(Client adminClient, String user) { + try { + runCommand(adminClient, "DROP USER IF EXISTS " + user); + } catch (Exception e) { + log.warn("Failed to drop user {}", user, e); + } + } + + private static void runCommand(Client client, String sql) throws Exception { + client.execute(sql).get(10, TimeUnit.SECONDS); + } + + private static Client createClient(String endpoint, String database, String user, String password) { + return new Client.Builder() + .addEndpoint(endpoint) + .setUsername(user) + .setPassword(password) + .setDefaultDatabase(database) + .compressServerResponse(true) + .build(); + } + + private static void printCurrentUser(Client client, String stage) { + List rows = client.queryAll("SELECT currentUser() AS user FROM system.one"); + log.info("{}: {}", stage, rows.get(0).getString("user")); + } +}