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 10004f15c..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 @@ -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; @@ -139,11 +140,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); + Object metricsRegistry, Supplier queryIdGenerator, CredentialsManager cManager) { + this.configuration = new ConcurrentHashMap<>(ClientConfigProperties.parseConfigMap(configuration)); + this.credentialsManager = cManager; this.readOnlyConfig = Collections.unmodifiableMap(configuration); this.metricsRegistry = metricsRegistry; this.queryIdGenerator = queryIdGenerator; @@ -191,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()); } @@ -340,8 +343,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; } @@ -369,13 +375,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; } @@ -983,16 +993,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); } /** @@ -1075,29 +1085,8 @@ 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"); - } + CredentialsManager cManager = new CredentialsManager(this.configuration); // Check timezone settings String useTimeZoneValue = this.configuration.get(ClientConfigProperties.USE_TIMEZONE.getKey()); @@ -1128,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); } } @@ -2129,8 +2118,46 @@ 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); + } + + /** + * 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) { + this.credentialsManager.setAccessToken(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) { - this.configuration.put(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION), "Bearer " + bearer); + setAccessToken(bearer); } private Endpoint getNextAliveNode() { @@ -2146,8 +2173,8 @@ 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 = new HashMap<>(configuration); + credentialsManager.applyCredentials(requestSettings); 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..7d975e480 --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/CredentialsManager.java @@ -0,0 +1,131 @@ +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. + * + *

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 static final String AUTHORIZATION_HEADER_KEY = + ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION); + + 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() { + 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) { + 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) { + 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 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(), 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(AUTHORIZATION_HEADER_KEY); + + if (!(useSslAuth || hasAccessToken || hasUser || hasPassword || customHttpHeaders)) { + throw new ClientMisconfigurationException("Username and password (or access token or SSL authentication or pre-define Authorization header) are required"); + } + + if (useSslAuth && (hasAccessToken || hasPassword)) { + 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())) { + throw new ClientMisconfigurationException("SSL authentication requires a client certificate"); + } + + if (configuration.containsKey(ClientConfigProperties.SSL_TRUST_STORE.getKey()) && + configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) { + 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); + } + + 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/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index c8904708b..9b584d2c4 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -434,6 +434,40 @@ public void testClientCustomRoles(String[] roles) throws Exception { } } + @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 { 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 ed571bb7b..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")); } } @@ -968,7 +969,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()) { @@ -998,7 +999,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()) { @@ -1018,7 +1019,7 @@ public void testBearerTokenAuth() throws Exception { .build()); - client.updateBearerToken(jwtToken2); + client.setAccessToken(jwtToken2); client.execute("SELECT 1").get(); } @@ -1061,6 +1062,59 @@ public void testBasicAuthWithNoPassword() 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()) { @@ -1069,7 +1123,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/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. 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/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/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")); + } +} 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()) {