-
Notifications
You must be signed in to change notification settings - Fork 623
[client-v2,jdbc-v2] Change credentials in realtime #2812
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6ed0d52
ffbdd84
bb1cfa8
32706eb
73aa2a5
bd75791
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 @@ | |||||||||||||||||
| private final int retries; | ||||||||||||||||||
| private LZ4Factory lz4Factory = null; | ||||||||||||||||||
| private final Supplier<String> queryIdGenerator; | ||||||||||||||||||
| private final CredentialsManager credentialsManager; | ||||||||||||||||||
|
|
||||||||||||||||||
| private Client(Collection<Endpoint> endpoints, Map<String,String> configuration, | ||||||||||||||||||
| ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy, | ||||||||||||||||||
| Object metricsRegistry, Supplier<String> queryIdGenerator) { | ||||||||||||||||||
| this.configuration = ClientConfigProperties.parseConfigMap(configuration); | ||||||||||||||||||
| Object metricsRegistry, Supplier<String> 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 @@ | |||||||||||||||||
|
|
||||||||||||||||||
| 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<ClickHouseDataType, Class<?>>) this.configuration.get(ClientConfigProperties.TYPE_HINT_MAPPING.getKey()); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -340,8 +343,11 @@ | |||||||||||||||||
| 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 @@ | |||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * 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 @@ | |||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * 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) { | ||||||||||||||||||
|
Check warning on line 1003 in client-v2/src/main/java/com/clickhouse/client/api/Client.java
|
||||||||||||||||||
| // 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 @@ | |||||||||||||||||
| 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 @@ | |||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| 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 @@ | |||||||||||||||||
| return unmodifiableDbRolesView; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Updates the credentials used for subsequent requests. | ||||||||||||||||||
| * | ||||||||||||||||||
| * <p>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. | ||||||||||||||||||
| * | ||||||||||||||||||
|
Comment on lines
+2121
to
+2127
|
||||||||||||||||||
| * @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. | ||||||||||||||||||
| * | ||||||||||||||||||
| * <p>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)}. | ||||||||||||||||||
| * | ||||||||||||||||||
| * <p>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 | ||||||||||||||||||
| */ | ||||||||||||||||||
|
Comment on lines
+2156
to
+2158
|
||||||||||||||||||
| * | |
| * @param bearer - token to use | |
| */ | |
| * | |
| * @deprecated Use {@link #setAccessToken(String)} instead. | |
| * @param bearer - token to use | |
| */ | |
| @Deprecated |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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. | ||||||
|
||||||
| * Preferred client setting for token-based authentication like JWT and Oauth. | |
| * Preferred client setting for token-based authentication like JWT and OAuth. |
Copilot
AI
Apr 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc comment casing: "For Http" should be "For HTTP".
| * For Http it is translated to Authorization Bearer header. | |
| * For HTTP it is translated to Authorization Bearer header. |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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. | ||||||||||||
| * | ||||||||||||
| * <p>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<String, String> 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<String, Object> snapshot() { | ||||||||||||
| Map<String, Object> snapshot = new HashMap<>(); | ||||||||||||
| applyCredentials(snapshot); | ||||||||||||
| return snapshot; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| public void applyCredentials(Map<String, Object> 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); | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+42
to
+49
|
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Replaces the current username/password credentials. | ||||||||||||
| * | ||||||||||||
| * <p>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. | ||||||||||||
| * | ||||||||||||
| * <p>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<String, ?> 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"); | ||||||||||||
|
||||||||||||
| throw 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-defined Authorization header) are required"); |
Copilot
AI
Apr 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thrown message says "Username and password ... are required", but the validation allows config with only one of USER or PASSWORD (and also allows only Authorization header). Consider rewording to avoid implying both are mandatory, and to reflect the accepted auth modes more clearly.
| throw new ClientMisconfigurationException("Username and password (or access token or SSL authentication or pre-define Authorization header) are required"); | |
| throw new ClientMisconfigurationException( | |
| "Authentication configuration is required: provide username or password, access token, SSL authentication, or a pre-defined Authorization header"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setAccessToken()always writes anAuthorization: Bearer ...header even whenaccessTokenis null/blank, which can result in sendingBearer null(orBearer) on requests. It would be safer to validate the token (non-null/non-blank) and otherwise remove/clear both theaccess_tokenconfig entry and the Authorization header entry.