Skip to content

Commit 8ffa30a

Browse files
committed
added configuration flags to switch between basic auth and CH auth headers
1 parent 8533f46 commit 8ffa30a

File tree

7 files changed

+157
-17
lines changed

7 files changed

+157
-17
lines changed

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,18 @@ protected static Map<String, String> createDefaultHeaders(ClickHouseConfig confi
234234
map.put("authorization", credentials.getAccessToken());
235235
} else if (!hasAuthorizationHeader) {
236236
if (config.isSsl() && !ClickHouseChecker.isNullOrEmpty(config.getSslCert())) {
237-
map.put("x-clickhouse-user", credentials.getUserName());
238-
map.put("x-clickhouse-ssl-certificate-auth", "on");
237+
map.put(ClickHouseHttpProto.HEADER_DB_USER, credentials.getUserName());
238+
map.put(ClickHouseHttpProto.HEADER_SSL_CERT_AUTH, "on");
239239
} else {
240-
String password = credentials.getPassword() == null ? "" : credentials.getPassword();
241-
map.put(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder()
242-
.encodeToString((credentials.getUserName() + ":" + password).getBytes(StandardCharsets.UTF_8)));
240+
boolean useBasicAuthentication = config.getBoolOption(ClickHouseHttpOption.USE_BASIC_AUTHENTICATION);
241+
if (useBasicAuthentication) {
242+
String password = credentials.getPassword() == null ? "" : credentials.getPassword();
243+
map.put(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder()
244+
.encodeToString((credentials.getUserName() + ":" + password).getBytes(StandardCharsets.UTF_8)));
245+
} else {
246+
map.put(ClickHouseHttpProto.HEADER_DB_USER, credentials.getUserName());
247+
map.put(ClickHouseHttpProto.HEADER_DB_PASSWORD, credentials.getPassword());
248+
}
243249
}
244250
}
245251

clickhouse-http-client/src/main/java/com/clickhouse/client/http/config/ClickHouseHttpOption.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,13 @@ public enum ClickHouseHttpOption implements ClickHouseOption {
107107
*/
108108
KEEP_ALIVE_TIMEOUT("alive_timeout", -1L,
109109
"Default keep-alive timeout in milliseconds."),
110-
;
110+
111+
/**
112+
* Whether to use HTTP basic authentication. Default value is true.
113+
* Password that contain UTF8 characters may not be passed through http headers and BASIC authentication
114+
* is the only option here.
115+
*/
116+
USE_BASIC_AUTHENTICATION("http_use_basic_auth", true, "Whether to use basic authentication.");
111117

112118
private final String key;
113119
private final Serializable defaultValue;

clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/AccessManagementTest.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ public void testSetRolesAccessingTableRows() throws SQLException {
179179

180180
@Test(groups = "integration", dataProvider = "passwordAuthMethods")
181181
public void testPasswordAuthentication(String identifyWith, String identifyBy) throws SQLException {
182-
if (isCloud()) return; // TODO: testPasswordAuthentication - Revisit, see:
182+
// if (isCloud()) return; // TODO: testPasswordAuthentication - Revisit, see:
183183
String url = String.format("jdbc:ch:%s", getEndpointString());
184184
Properties properties = new Properties();
185185
properties.setProperty(ClickHouseHttpOption.REMEMBER_LAST_SET_ROLES.getKey(), "true");
@@ -216,4 +216,49 @@ private static Object[][] passwordAuthMethods() {
216216
{ "sha256_password", "S3Cr?=t"},
217217
};
218218
}
219+
220+
@Test(groups = "integration", dataProvider = "headerAuthDataProvider")
221+
public void testSwitchingBasicAuthToClickHouseHeaders(String identifyWith, String identifyBy, boolean shouldFail) throws SQLException {
222+
// if (isCloud()) return; // TODO: testPasswordAuthentication - Revisit, see:
223+
String url = String.format("jdbc:ch:%s", getEndpointString());
224+
Properties properties = new Properties();
225+
properties.put(ClickHouseHttpOption.USE_BASIC_AUTHENTICATION.getKey(), false);
226+
ClickHouseDataSource dataSource = new ClickHouseDataSource(url, properties);
227+
228+
try (Connection connection = dataSource.getConnection("access_dba", "123")) {
229+
Statement st = connection.createStatement();
230+
st.execute("DROP USER IF EXISTS some_user");
231+
st.execute("CREATE USER some_user IDENTIFIED WITH " + identifyWith + " BY '" + identifyBy + "'");
232+
} catch (Exception e) {
233+
Assert.fail("Failed on setup", e);
234+
}
235+
236+
try (Connection connection = dataSource.getConnection("some_user", identifyBy)) {
237+
Statement st = connection.createStatement();
238+
ResultSet rs = st.executeQuery("SELECT user() AS user_name");
239+
Assert.assertTrue(rs.next());
240+
Assert.assertEquals(rs.getString(1), "some_user");
241+
if (shouldFail) {
242+
Assert.fail("Expected authentication to fail");
243+
}
244+
} catch (Exception e) {
245+
if (!shouldFail) {
246+
Assert.fail("Failed to authenticate", e);
247+
}
248+
}
249+
}
250+
251+
@DataProvider(name = "headerAuthDataProvider")
252+
private static Object[][] headerAuthDataProvider() {
253+
return new Object[][] {
254+
{ "plaintext_password", "password", false },
255+
{ "plaintext_password", "", false },
256+
{ "plaintext_password", "S3Cr=?t", true},
257+
{ "plaintext_password", "123§", true },
258+
{ "sha256_password", "password", false},
259+
{ "sha256_password", "123§", true },
260+
{ "sha256_password", "S3Cr=?t", true},
261+
{ "sha256_password", "S3Cr?=t", false},
262+
};
263+
}
219264
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,16 @@ public Builder columnToMethodMatchingStrategy(ColumnToMethodMatchingStrategy str
876876
return this;
877877
}
878878

879+
/**
880+
* Whether to use HTTP basic authentication. Default value is true.
881+
* Password that contain UTF8 characters may not be passed through http headers and BASIC authentication
882+
* is the only option here.
883+
*/
884+
public Builder useHTTPBasicAuth(boolean useBasicAuth) {
885+
this.configuration.put(ClientSettings.HTTP_USE_BASIC_AUTH, String.valueOf(useBasicAuth));
886+
return this;
887+
}
888+
879889
public Client build() {
880890
setDefaults();
881891

@@ -1009,6 +1019,10 @@ private void setDefaults() {
10091019
if (columnToMethodMatchingStrategy == null) {
10101020
columnToMethodMatchingStrategy = DefaultColumnToMethodMatchingStrategy.INSTANCE;
10111021
}
1022+
1023+
if (!configuration.containsKey(ClientSettings.HTTP_USE_BASIC_AUTH)) {
1024+
useHTTPBasicAuth(true);
1025+
}
10121026
}
10131027
}
10141028

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,6 @@ public static List<String> valuesFromCommaSeparated(String value) {
3737
public static final String SESSION_DB_ROLES = "session_db_roles";
3838

3939
public static final String SETTING_LOG_COMMENT = SERVER_SETTING_PREFIX + "log_comment";
40+
41+
public static final String HTTP_USE_BASIC_AUTH = "http_use_basic_auth";
4042
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,12 +397,17 @@ private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String,
397397
req.addHeader(ClickHouseHttpProto.HEADER_QUERY_ID, requestConfig.get(ClickHouseClientOption.QUERY_ID.getKey()).toString());
398398
}
399399
}
400+
400401
if (MapUtils.getFlag(chConfig, "ssl_authentication", false)) {
401402
req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, chConfig.get(ClickHouseDefaults.USER.getKey()));
402403
req.addHeader(ClickHouseHttpProto.HEADER_SSL_CERT_AUTH, "on");
403-
} else {
404+
} else if (chConfig.getOrDefault(ClientSettings.HTTP_USE_BASIC_AUTH, "true").equalsIgnoreCase("true")) {
404405
req.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString(
405406
(chConfig.get(ClickHouseDefaults.USER.getKey()) + ":" + chConfig.get(ClickHouseDefaults.PASSWORD.getKey())).getBytes(StandardCharsets.UTF_8)));
407+
} else {
408+
req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, chConfig.get(ClickHouseDefaults.USER.getKey()));
409+
req.addHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD, chConfig.get(ClickHouseDefaults.PASSWORD.getKey()));
410+
406411
}
407412
if (proxyAuthHeaderValue != null) {
408413
req.addHeader(HttpHeaders.PROXY_AUTHORIZATION, proxyAuthHeaderValue);
@@ -431,6 +436,14 @@ private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String,
431436
req.addHeader(entry.getKey().substring(ClientSettings.HTTP_HEADER_PREFIX.length()), entry.getValue().toString());
432437
}
433438
}
439+
440+
// Special cases
441+
if (req.containsHeader(HttpHeaders.AUTHORIZATION) && (req.containsHeader(ClickHouseHttpProto.HEADER_DB_USER) ||
442+
req.containsHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD))) {
443+
// user has set auth header for purpose, lets remove ours
444+
req.removeHeaders(ClickHouseHttpProto.HEADER_DB_USER);
445+
req.removeHeaders(ClickHouseHttpProto.HEADER_DB_PASSWORD);
446+
}
434447
}
435448
private void addQueryParams(URIBuilder req, Map<String, String> chConfig, Map<String, Object> requestConfig) {
436449
if (requestConfig == null) {

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

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.github.tomakehurst.wiremock.http.Fault;
2424
import com.github.tomakehurst.wiremock.http.trafficlistener.WiremockNetworkTrafficListener;
2525
import org.apache.hc.core5.http.ConnectionRequestTimeoutException;
26+
import org.apache.hc.core5.http.HttpHeaders;
2627
import org.apache.hc.core5.http.HttpStatus;
2728
import org.apache.hc.core5.net.URIBuilder;
2829
import org.eclipse.jetty.server.Server;
@@ -36,6 +37,7 @@
3637
import java.nio.ByteBuffer;
3738
import java.time.temporal.ChronoUnit;
3839
import java.util.Arrays;
40+
import java.util.Base64;
3941
import java.util.List;
4042
import java.util.Random;
4143
import java.util.concurrent.CompletableFuture;
@@ -493,7 +495,7 @@ public void testSSLAuthentication() throws Exception {
493495
}
494496

495497
@Test(groups = { "integration" }, dataProvider = "testPasswordAuthenticationProvider", dataProviderClass = HttpTransportTests.class)
496-
public void testPasswordAuthentication(String identifyWith, String identifyBy) throws Exception {
498+
public void testPasswordAuthentication(String identifyWith, String identifyBy, boolean failsWithHeaders) throws Exception {
497499
if (isCloud()) {
498500
return; // Current test is working only with local server because of self-signed certificates.
499501
}
@@ -512,6 +514,7 @@ public void testPasswordAuthentication(String identifyWith, String identifyBy) t
512514
Assert.fail("Failed on setup", e);
513515
}
514516

517+
515518
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), false)
516519
.setUsername("some_user")
517520
.setPassword(identifyBy)
@@ -521,22 +524,73 @@ public void testPasswordAuthentication(String identifyWith, String identifyBy) t
521524
} catch (Exception e) {
522525
Assert.fail("Failed to authenticate", e);
523526
}
527+
528+
if (failsWithHeaders) {
529+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), false)
530+
.setUsername("some_user")
531+
.setPassword(identifyBy)
532+
.useHTTPBasicAuth(false)
533+
.build()) {
534+
535+
Assert.expectThrows(ClientException.class, () ->
536+
client.queryAll("SELECT user()").get(0).getString(1));
537+
538+
} catch (Exception e) {
539+
Assert.fail("Unexpected exception", e);
540+
}
541+
}
524542
}
525543

526544
@DataProvider(name = "testPasswordAuthenticationProvider")
527545
public static Object[][] testPasswordAuthenticationProvider() {
528546
return new Object[][] {
529-
{ "plaintext_password", "password" },
530-
{ "plaintext_password", "" },
531-
{ "plaintext_password", "S3Cr=?t"},
532-
{ "plaintext_password", "123§" },
533-
{ "sha256_password", "password" },
534-
{ "sha256_password", "123§" },
535-
{ "sha256_password", "S3Cr=?t"},
536-
{ "sha256_password", "S3Cr?=t"},
547+
{ "plaintext_password", "password", false},
548+
{ "plaintext_password", "", false },
549+
{ "plaintext_password", "S3Cr=?t", true},
550+
{ "plaintext_password", "123§", true },
551+
{ "sha256_password", "password", false },
552+
{ "sha256_password", "123§", true },
553+
{ "sha256_password", "S3Cr=?t", true},
554+
{ "sha256_password", "S3Cr?=t", false},
537555
};
538556
}
539557

558+
@Test(groups = { "integration" })
559+
public void testAuthHeaderIsKeptFromUser() throws Exception {
560+
if (isCloud()) {
561+
return; // Current test is working only with local server because of self-signed certificates.
562+
}
563+
ClickHouseNode server = getServer(ClickHouseProtocol.HTTP);
564+
565+
String identifyWith = "sha256_password";
566+
String identifyBy = "123§";
567+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), false)
568+
.setUsername("default")
569+
.setPassword("")
570+
.build()) {
571+
572+
try (CommandResponse resp = client.execute("DROP USER IF EXISTS some_user").get()) {
573+
}
574+
try (CommandResponse resp = client.execute("CREATE USER some_user IDENTIFIED WITH " + identifyWith + " BY '" + identifyBy + "'").get()) {
575+
}
576+
} catch (Exception e) {
577+
Assert.fail("Failed on setup", e);
578+
}
579+
580+
581+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), false)
582+
.setUsername("some_user")
583+
.setPassword(identifyBy)
584+
.useHTTPBasicAuth(false) // disable basic auth to produce CH headers
585+
.httpHeader(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString(("some_user:" +identifyBy).getBytes()))
586+
.build()) {
587+
588+
Assert.assertEquals(client.queryAll("SELECT user()").get(0).getString(1), "some_user");
589+
} catch (Exception e) {
590+
Assert.fail("Failed to authenticate", e);
591+
}
592+
}
593+
540594
@Test(groups = { "integration" })
541595
public void testSSLAuthentication_invalidConfig() throws Exception {
542596
if (isCloud()) {

0 commit comments

Comments
 (0)