Skip to content

Commit 13fcafa

Browse files
committed
Implemented authentication via http basic auth
1 parent ec9f249 commit 13fcafa

File tree

5 files changed

+122
-15
lines changed

5 files changed

+122
-15
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.net.URLEncoder;
1010
import java.nio.charset.Charset;
1111
import java.nio.charset.StandardCharsets;
12+
import java.util.Base64;
1213
import java.util.Collections;
1314
import java.util.HashSet;
1415
import java.util.LinkedHashMap;
@@ -38,6 +39,7 @@
3839
import com.clickhouse.data.ClickHouseUtils;
3940
import com.clickhouse.logging.Logger;
4041
import com.clickhouse.logging.LoggerFactory;
42+
import org.apache.hc.core5.http.HttpHeaders;
4143

4244
public abstract class ClickHouseHttpConnection implements AutoCloseable {
4345
private static final Logger log = LoggerFactory.getLogger(ClickHouseHttpConnection.class);
@@ -238,11 +240,12 @@ protected static Map<String, String> createDefaultHeaders(ClickHouseConfig confi
238240
// TODO check if auth-scheme is available and supported
239241
map.put("authorization", credentials.getAccessToken());
240242
} else if (!hasAuthorizationHeader) {
241-
map.put("x-clickhouse-user", credentials.getUserName());
242243
if (config.isSsl() && !ClickHouseChecker.isNullOrEmpty(config.getSslCert())) {
244+
map.put("x-clickhouse-user", credentials.getUserName());
243245
map.put("x-clickhouse-ssl-certificate-auth", "on");
244246
} else if (!ClickHouseChecker.isNullOrEmpty(credentials.getPassword())) {
245-
map.put("x-clickhouse-key", credentials.getPassword());
247+
map.put(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder()
248+
.encodeToString((credentials.getUserName() + ":" + credentials.getPassword()).getBytes(StandardCharsets.UTF_8)));
246249
}
247250
}
248251

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public class ClickHouseHttpProto {
4444
*/
4545
public static final String HEADER_DB_USER = "X-ClickHouse-User";
4646

47+
/**
48+
* Password of user to be used to authenticate. Note: header value should be unencoded, so using
49+
* special characters might cause issues. It is recommended to use the Basic Authentication instead.
50+
*/
4751
public static final String HEADER_DB_PASSWORD = "X-ClickHouse-Key";
4852

4953
public static final String HEADER_SSL_CERT_AUTH = "x-clickhouse-ssl-certificate-auth";

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,43 @@ public void testSetRolesAccessingTableRows() throws SQLException {
176176
Assert.fail("Failed to check roles", e);
177177
}
178178
}
179+
180+
@Test(groups = "integration", dataProvider = "passwordAuthMethods")
181+
public void testPasswordAuthentication(String identifyWith, String identifyBy) throws SQLException {
182+
if (isCloud()) return; // TODO: testPasswordAuthentication - Revisit, see:
183+
String url = String.format("jdbc:ch:%s", getEndpointString());
184+
Properties properties = new Properties();
185+
properties.setProperty(ClickHouseHttpOption.REMEMBER_LAST_SET_ROLES.getKey(), "true");
186+
ClickHouseDataSource dataSource = new ClickHouseDataSource(url, properties);
187+
188+
try (Connection connection = dataSource.getConnection("access_dba", "123")) {
189+
Statement st = connection.createStatement();
190+
st.execute("DROP USER IF EXISTS some_user");
191+
st.execute("CREATE USER some_user IDENTIFIED WITH " + identifyWith + " BY '" + identifyBy + "'");
192+
} catch (Exception e) {
193+
Assert.fail("Failed on setup", e);
194+
}
195+
196+
try (Connection connection = dataSource.getConnection("some_user", identifyBy)) {
197+
Statement st = connection.createStatement();
198+
ResultSet rs = st.executeQuery("SELECT 1");
199+
Assert.assertTrue(rs.next());
200+
Assert.assertEquals(rs.getInt(1), 1);
201+
} catch (Exception e) {
202+
Assert.fail("Failed to authenticate", e);
203+
}
204+
}
205+
206+
@DataProvider(name = "passwordAuthMethods")
207+
private static Object[][] passwordAuthMethods() {
208+
return new Object[][] {
209+
{ "plaintext_password", "password" },
210+
{ "plaintext_password", "S3Cr=?t"},
211+
{ "plaintext_password", "123§" },
212+
{ "sha256_password", "password" },
213+
{ "sha256_password", "123§" },
214+
{ "sha256_password", "S3Cr=?t"},
215+
{ "sha256_password", "S3Cr?=t"},
216+
};
217+
}
179218
}

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

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import java.net.NoRouteToHostException;
6464
import java.net.URI;
6565
import java.net.URISyntaxException;
66+
import java.net.URLEncoder;
6667
import java.net.UnknownHostException;
6768
import java.nio.charset.StandardCharsets;
6869
import java.security.NoSuchAlgorithmException;
@@ -338,13 +339,14 @@ public ClassicHttpResponse executeRequest(ClickHouseNode server, Map<String, Obj
338339
.build();
339340
req.setConfig(httpReqConfig);
340341
// setting entity. wrapping if compression is enabled
341-
req.setEntity(wrapEntity(new EntityTemplate(-1, CONTENT_TYPE, null, writeCallback), false));
342+
req.setEntity(wrapEntity(new EntityTemplate(-1, CONTENT_TYPE, null, writeCallback), HttpStatus.SC_OK, false));
342343

343344
HttpClientContext context = HttpClientContext.create();
344345

345346
try {
346347
ClassicHttpResponse httpResponse = httpClient.executeOpen(null, req, context);
347-
httpResponse.setEntity(wrapEntity(httpResponse.getEntity(), true));
348+
httpResponse.setEntity(wrapEntity(httpResponse.getEntity(), httpResponse.getCode(), true));
349+
348350
if (httpResponse.getCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
349351
throw new ClientMisconfigurationException("Proxy authentication required. Please check your proxy settings.");
350352
} else if (httpResponse.getCode() == HttpStatus.SC_BAD_GATEWAY) {
@@ -395,11 +397,12 @@ private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String,
395397
req.addHeader(ClickHouseHttpProto.HEADER_QUERY_ID, requestConfig.get(ClickHouseClientOption.QUERY_ID.getKey()).toString());
396398
}
397399
}
398-
req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, chConfig.get(ClickHouseDefaults.USER.getKey()));
399400
if (MapUtils.getFlag(chConfig, "ssl_authentication", false)) {
401+
req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, chConfig.get(ClickHouseDefaults.USER.getKey()));
400402
req.addHeader(ClickHouseHttpProto.HEADER_SSL_CERT_AUTH, "on");
401403
} else {
402-
req.addHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD, chConfig.get(ClickHouseDefaults.PASSWORD.getKey()));
404+
req.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString(
405+
(chConfig.get(ClickHouseDefaults.USER.getKey()) + ":" + chConfig.get(ClickHouseDefaults.PASSWORD.getKey())).getBytes(StandardCharsets.UTF_8)));
403406
}
404407
if (proxyAuthHeaderValue != null) {
405408
req.addHeader(HttpHeaders.PROXY_AUTHORIZATION, proxyAuthHeaderValue);
@@ -487,15 +490,26 @@ private void addQueryParams(URIBuilder req, Map<String, String> chConfig, Map<St
487490
}
488491
}
489492

490-
private HttpEntity wrapEntity(HttpEntity httpEntity, boolean isResponse) {
491-
boolean serverCompression = chConfiguration.getOrDefault(ClickHouseClientOption.COMPRESS.getKey(), "false").equalsIgnoreCase("true");
492-
boolean clientCompression = chConfiguration.getOrDefault(ClickHouseClientOption.DECOMPRESS.getKey(), "false").equalsIgnoreCase("true");
493-
boolean useHttpCompression = chConfiguration.getOrDefault("client.use_http_compression", "false").equalsIgnoreCase("true");
494-
if (serverCompression || clientCompression) {
495-
return new LZ4Entity(httpEntity, useHttpCompression, serverCompression, clientCompression,
496-
MapUtils.getInt(chConfiguration, "compression.lz4.uncompressed_buffer_size"), isResponse);
497-
} else {
498-
return httpEntity;
493+
private HttpEntity wrapEntity(HttpEntity httpEntity, int httpStatus, boolean isResponse) {
494+
495+
switch (httpStatus) {
496+
case HttpStatus.SC_OK:
497+
case HttpStatus.SC_CREATED:
498+
case HttpStatus.SC_ACCEPTED:
499+
case HttpStatus.SC_NO_CONTENT:
500+
case HttpStatus.SC_PARTIAL_CONTENT:
501+
case HttpStatus.SC_RESET_CONTENT:
502+
case HttpStatus.SC_NOT_MODIFIED:
503+
case HttpStatus.SC_BAD_REQUEST:
504+
boolean serverCompression = chConfiguration.getOrDefault(ClickHouseClientOption.COMPRESS.getKey(), "false").equalsIgnoreCase("true");
505+
boolean clientCompression = chConfiguration.getOrDefault(ClickHouseClientOption.DECOMPRESS.getKey(), "false").equalsIgnoreCase("true");
506+
boolean useHttpCompression = chConfiguration.getOrDefault("client.use_http_compression", "false").equalsIgnoreCase("true");
507+
if (serverCompression || clientCompression) {
508+
return new LZ4Entity(httpEntity, useHttpCompression, serverCompression, clientCompression,
509+
MapUtils.getInt(chConfiguration, "compression.lz4.uncompressed_buffer_size"), isResponse);
510+
}
511+
default:
512+
return httpEntity;
499513
}
500514
}
501515

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ public void testServerSettings() {
458458
static {
459459
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "DEBUG");
460460
}
461+
461462
@Test(groups = { "integration" })
462463
public void testSSLAuthentication() throws Exception {
463464
if (isCloud()) {
@@ -491,6 +492,52 @@ public void testSSLAuthentication() throws Exception {
491492
}
492493
}
493494

495+
@Test(groups = { "integration" }, dataProvider = "testPasswordAuthenticationProvider", dataProviderClass = HttpTransportTests.class)
496+
public void testPasswordAuthentication(String identifyWith, String identifyBy) throws Exception {
497+
if (isCloud()) {
498+
return; // Current test is working only with local server because of self-signed certificates.
499+
}
500+
ClickHouseNode server = getServer(ClickHouseProtocol.HTTP);
501+
502+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), false)
503+
.setUsername("default")
504+
.setPassword("")
505+
.build()) {
506+
507+
try (CommandResponse resp = client.execute("DROP USER IF EXISTS some_user").get()) {
508+
}
509+
try (CommandResponse resp = client.execute("CREATE USER some_user IDENTIFIED WITH " + identifyWith + " BY '" + identifyBy + "'").get()) {
510+
}
511+
} catch (Exception e) {
512+
Assert.fail("Failed on setup", e);
513+
}
514+
515+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), false)
516+
.setUsername("some_user")
517+
.setPassword(identifyBy)
518+
.build()) {
519+
520+
try (QueryResponse resp = client.query("SELECT 1").get()) {
521+
Assert.assertEquals(resp.getReadRows(), 1);
522+
}
523+
} catch (Exception e) {
524+
Assert.fail("Failed to authenticate", e);
525+
}
526+
}
527+
528+
@DataProvider(name = "testPasswordAuthenticationProvider")
529+
public static Object[][] testPasswordAuthenticationProvider() {
530+
return new Object[][] {
531+
{ "plaintext_password", "password" },
532+
{ "plaintext_password", "S3Cr=?t"},
533+
{ "plaintext_password", "123§" },
534+
{ "sha256_password", "password" },
535+
{ "sha256_password", "123§" },
536+
{ "sha256_password", "S3Cr=?t"},
537+
{ "sha256_password", "S3Cr?=t"},
538+
};
539+
}
540+
494541
@Test(groups = { "integration" })
495542
public void testSSLAuthentication_invalidConfig() throws Exception {
496543
if (isCloud()) {

0 commit comments

Comments
 (0)