Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.*;
import java.util.Map.Entry;

Expand All @@ -31,6 +32,7 @@
import com.clickhouse.data.ClickHouseUtils;
import com.clickhouse.logging.Logger;
import com.clickhouse.logging.LoggerFactory;
import org.apache.hc.core5.http.HttpHeaders;

public abstract class ClickHouseHttpConnection implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(ClickHouseHttpConnection.class);
Expand Down Expand Up @@ -231,11 +233,19 @@ protected static Map<String, String> createDefaultHeaders(ClickHouseConfig confi
// TODO check if auth-scheme is available and supported
map.put("authorization", credentials.getAccessToken());
} else if (!hasAuthorizationHeader) {
map.put("x-clickhouse-user", credentials.getUserName());
if (config.isSsl() && !ClickHouseChecker.isNullOrEmpty(config.getSslCert())) {
map.put("x-clickhouse-ssl-certificate-auth", "on");
} else if (!ClickHouseChecker.isNullOrEmpty(credentials.getPassword())) {
map.put("x-clickhouse-key", credentials.getPassword());
map.put(ClickHouseHttpProto.HEADER_DB_USER, credentials.getUserName());
map.put(ClickHouseHttpProto.HEADER_SSL_CERT_AUTH, "on");
} else {
boolean useBasicAuthentication = config.getBoolOption(ClickHouseHttpOption.USE_BASIC_AUTHENTICATION);
if (useBasicAuthentication) {
String password = credentials.getPassword() == null ? "" : credentials.getPassword();
map.put(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder()
.encodeToString((credentials.getUserName() + ":" + password).getBytes(StandardCharsets.UTF_8)));
} else {
map.put(ClickHouseHttpProto.HEADER_DB_USER, credentials.getUserName());
map.put(ClickHouseHttpProto.HEADER_DB_PASSWORD, credentials.getPassword());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public class ClickHouseHttpProto {
*/
public static final String HEADER_DB_USER = "X-ClickHouse-User";

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

public static final String HEADER_SSL_CERT_AUTH = "x-clickhouse-ssl-certificate-auth";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,13 @@ public enum ClickHouseHttpOption implements ClickHouseOption {
*/
KEEP_ALIVE_TIMEOUT("alive_timeout", -1L,
"Default keep-alive timeout in milliseconds."),
;

/**
* Whether to use HTTP basic authentication. Default value is true.
* Password that contain UTF8 characters may not be passed through http headers and BASIC authentication
* is the only option here.
*/
USE_BASIC_AUTHENTICATION("http_use_basic_auth", true, "Whether to use basic authentication.");

private final String key;
private final Serializable defaultValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,89 @@ public void testSetRolesAccessingTableRows() throws SQLException {
Assert.fail("Failed to check roles", e);
}
}

@Test(groups = "integration", dataProvider = "passwordAuthMethods")
public void testPasswordAuthentication(String identifyWith, String identifyBy) throws SQLException {
if (isCloud()) return; // Doesn’t allow to create users with specific passwords
String url = String.format("jdbc:ch:%s", getEndpointString());
Properties properties = new Properties();
properties.setProperty(ClickHouseHttpOption.REMEMBER_LAST_SET_ROLES.getKey(), "true");
ClickHouseDataSource dataSource = new ClickHouseDataSource(url, properties);

try (Connection connection = dataSource.getConnection("access_dba", "123")) {
Statement st = connection.createStatement();
st.execute("DROP USER IF EXISTS some_user");
st.execute("CREATE USER some_user IDENTIFIED WITH " + identifyWith + " BY '" + identifyBy + "'");
} catch (Exception e) {
Assert.fail("Failed on setup", e);
}

try (Connection connection = dataSource.getConnection("some_user", identifyBy)) {
Statement st = connection.createStatement();
ResultSet rs = st.executeQuery("SELECT user() AS user_name");
Assert.assertTrue(rs.next());
Assert.assertEquals(rs.getString(1), "some_user");
} catch (Exception e) {
Assert.fail("Failed to authenticate", e);
}
}

@DataProvider(name = "passwordAuthMethods")
private static Object[][] passwordAuthMethods() {
return new Object[][] {
{ "plaintext_password", "password" },
{ "plaintext_password", "" },
{ "plaintext_password", "S3Cr=?t"},
{ "plaintext_password", "123§" },
{ "sha256_password", "password" },
{ "sha256_password", "123§" },
{ "sha256_password", "S3Cr=?t"},
{ "sha256_password", "S3Cr?=t"},
};
}

@Test(groups = "integration", dataProvider = "headerAuthDataProvider")
public void testSwitchingBasicAuthToClickHouseHeaders(String identifyWith, String identifyBy, boolean shouldFail) throws SQLException {
if (isCloud()) return; // Doesn't allow to create users with specific passwords
String url = String.format("jdbc:ch:%s", getEndpointString());
Properties properties = new Properties();
properties.put(ClickHouseHttpOption.USE_BASIC_AUTHENTICATION.getKey(), false);
ClickHouseDataSource dataSource = new ClickHouseDataSource(url, properties);

try (Connection connection = dataSource.getConnection("access_dba", "123")) {
Statement st = connection.createStatement();
st.execute("DROP USER IF EXISTS some_user");
st.execute("CREATE USER some_user IDENTIFIED WITH " + identifyWith + " BY '" + identifyBy + "'");
} catch (Exception e) {
Assert.fail("Failed on setup", e);
}

try (Connection connection = dataSource.getConnection("some_user", identifyBy)) {
Statement st = connection.createStatement();
ResultSet rs = st.executeQuery("SELECT user() AS user_name");
Assert.assertTrue(rs.next());
Assert.assertEquals(rs.getString(1), "some_user");
if (shouldFail) {
Assert.fail("Expected authentication to fail");
}
} catch (Exception e) {
if (!shouldFail) {
Assert.fail("Failed to authenticate", e);
}
}
}

@DataProvider(name = "headerAuthDataProvider")
private static Object[][] headerAuthDataProvider() {
return new Object[][] {
{ "plaintext_password", "password", false },
{ "plaintext_password", "", false },
{ "plaintext_password", "S3Cr=?t", true},
{ "plaintext_password", "123§", true },
{ "sha256_password", "password", false},
{ "sha256_password", "123§", true },
{ "sha256_password", "S3Cr=?t", true},
{ "sha256_password", "S3Cr?=t", false},
};
}
}
14 changes: 14 additions & 0 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,16 @@ public Builder columnToMethodMatchingStrategy(ColumnToMethodMatchingStrategy str
return this;
}

/**
* Whether to use HTTP basic authentication. Default value is true.
* Password that contain UTF8 characters may not be passed through http headers and BASIC authentication
* is the only option here.
*/
public Builder useHTTPBasicAuth(boolean useBasicAuth) {
this.configuration.put(ClientSettings.HTTP_USE_BASIC_AUTH, String.valueOf(useBasicAuth));
return this;
}

public Client build() {
setDefaults();

Expand Down Expand Up @@ -1009,6 +1019,10 @@ private void setDefaults() {
if (columnToMethodMatchingStrategy == null) {
columnToMethodMatchingStrategy = DefaultColumnToMethodMatchingStrategy.INSTANCE;
}

if (!configuration.containsKey(ClientSettings.HTTP_USE_BASIC_AUTH)) {
useHTTPBasicAuth(true);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ public static List<String> valuesFromCommaSeparated(String value) {
public static final String SESSION_DB_ROLES = "session_db_roles";

public static final String SETTING_LOG_COMMENT = SERVER_SETTING_PREFIX + "log_comment";

public static final String HTTP_USE_BASIC_AUTH = "http_use_basic_auth";
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import java.net.NoRouteToHostException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
Expand Down Expand Up @@ -338,13 +339,14 @@ public ClassicHttpResponse executeRequest(ClickHouseNode server, Map<String, Obj
.build();
req.setConfig(httpReqConfig);
// setting entity. wrapping if compression is enabled
req.setEntity(wrapEntity(new EntityTemplate(-1, CONTENT_TYPE, null, writeCallback), false));
req.setEntity(wrapEntity(new EntityTemplate(-1, CONTENT_TYPE, null, writeCallback), HttpStatus.SC_OK, false));

HttpClientContext context = HttpClientContext.create();

try {
ClassicHttpResponse httpResponse = httpClient.executeOpen(null, req, context);
httpResponse.setEntity(wrapEntity(httpResponse.getEntity(), true));
httpResponse.setEntity(wrapEntity(httpResponse.getEntity(), httpResponse.getCode(), true));

if (httpResponse.getCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
throw new ClientMisconfigurationException("Proxy authentication required. Please check your proxy settings.");
} else if (httpResponse.getCode() == HttpStatus.SC_BAD_GATEWAY) {
Expand Down Expand Up @@ -395,11 +397,17 @@ private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String,
req.addHeader(ClickHouseHttpProto.HEADER_QUERY_ID, requestConfig.get(ClickHouseClientOption.QUERY_ID.getKey()).toString());
}
}
req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, chConfig.get(ClickHouseDefaults.USER.getKey()));

if (MapUtils.getFlag(chConfig, "ssl_authentication", false)) {
req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, chConfig.get(ClickHouseDefaults.USER.getKey()));
req.addHeader(ClickHouseHttpProto.HEADER_SSL_CERT_AUTH, "on");
} else if (chConfig.getOrDefault(ClientSettings.HTTP_USE_BASIC_AUTH, "true").equalsIgnoreCase("true")) {
req.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString(
(chConfig.get(ClickHouseDefaults.USER.getKey()) + ":" + chConfig.get(ClickHouseDefaults.PASSWORD.getKey())).getBytes(StandardCharsets.UTF_8)));
} else {
req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, chConfig.get(ClickHouseDefaults.USER.getKey()));
req.addHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD, chConfig.get(ClickHouseDefaults.PASSWORD.getKey()));

}
if (proxyAuthHeaderValue != null) {
req.addHeader(HttpHeaders.PROXY_AUTHORIZATION, proxyAuthHeaderValue);
Expand Down Expand Up @@ -428,6 +436,14 @@ private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String,
req.addHeader(entry.getKey().substring(ClientSettings.HTTP_HEADER_PREFIX.length()), entry.getValue().toString());
}
}

// Special cases
if (req.containsHeader(HttpHeaders.AUTHORIZATION) && (req.containsHeader(ClickHouseHttpProto.HEADER_DB_USER) ||
req.containsHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD))) {
// user has set auth header for purpose, lets remove ours
req.removeHeaders(ClickHouseHttpProto.HEADER_DB_USER);
req.removeHeaders(ClickHouseHttpProto.HEADER_DB_PASSWORD);
}
}
private void addQueryParams(URIBuilder req, Map<String, String> chConfig, Map<String, Object> requestConfig) {
if (requestConfig == null) {
Expand Down Expand Up @@ -487,15 +503,26 @@ private void addQueryParams(URIBuilder req, Map<String, String> chConfig, Map<St
}
}

private HttpEntity wrapEntity(HttpEntity httpEntity, boolean isResponse) {
boolean serverCompression = chConfiguration.getOrDefault(ClickHouseClientOption.COMPRESS.getKey(), "false").equalsIgnoreCase("true");
boolean clientCompression = chConfiguration.getOrDefault(ClickHouseClientOption.DECOMPRESS.getKey(), "false").equalsIgnoreCase("true");
boolean useHttpCompression = chConfiguration.getOrDefault("client.use_http_compression", "false").equalsIgnoreCase("true");
if (serverCompression || clientCompression) {
return new LZ4Entity(httpEntity, useHttpCompression, serverCompression, clientCompression,
MapUtils.getInt(chConfiguration, "compression.lz4.uncompressed_buffer_size"), isResponse);
} else {
return httpEntity;
private HttpEntity wrapEntity(HttpEntity httpEntity, int httpStatus, boolean isResponse) {

switch (httpStatus) {
case HttpStatus.SC_OK:
case HttpStatus.SC_CREATED:
case HttpStatus.SC_ACCEPTED:
case HttpStatus.SC_NO_CONTENT:
case HttpStatus.SC_PARTIAL_CONTENT:
case HttpStatus.SC_RESET_CONTENT:
case HttpStatus.SC_NOT_MODIFIED:
case HttpStatus.SC_BAD_REQUEST:
boolean serverCompression = chConfiguration.getOrDefault(ClickHouseClientOption.COMPRESS.getKey(), "false").equalsIgnoreCase("true");
boolean clientCompression = chConfiguration.getOrDefault(ClickHouseClientOption.DECOMPRESS.getKey(), "false").equalsIgnoreCase("true");
boolean useHttpCompression = chConfiguration.getOrDefault("client.use_http_compression", "false").equalsIgnoreCase("true");
if (serverCompression || clientCompression) {
return new LZ4Entity(httpEntity, useHttpCompression, serverCompression, clientCompression,
MapUtils.getInt(chConfiguration, "compression.lz4.uncompressed_buffer_size"), isResponse);
}
default:
return httpEntity;
}
}

Expand Down
Loading
Loading