Skip to content

Commit

Permalink
added configuration flags to switch between basic auth and CH auth he…
Browse files Browse the repository at this point in the history
…aders
  • Loading branch information
chernser committed Oct 31, 2024
1 parent 8533f46 commit 8ffa30a
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,18 @@ protected static Map<String, String> createDefaultHeaders(ClickHouseConfig confi
map.put("authorization", credentials.getAccessToken());
} else if (!hasAuthorizationHeader) {
if (config.isSsl() && !ClickHouseChecker.isNullOrEmpty(config.getSslCert())) {
map.put("x-clickhouse-user", credentials.getUserName());
map.put("x-clickhouse-ssl-certificate-auth", "on");
map.put(ClickHouseHttpProto.HEADER_DB_USER, credentials.getUserName());
map.put(ClickHouseHttpProto.HEADER_SSL_CERT_AUTH, "on");
} else {
String password = credentials.getPassword() == null ? "" : credentials.getPassword();
map.put(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder()
.encodeToString((credentials.getUserName() + ":" + password).getBytes(StandardCharsets.UTF_8)));
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 @@ -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 @@ -179,7 +179,7 @@ public void testSetRolesAccessingTableRows() throws SQLException {

@Test(groups = "integration", dataProvider = "passwordAuthMethods")
public void testPasswordAuthentication(String identifyWith, String identifyBy) throws SQLException {
if (isCloud()) return; // TODO: testPasswordAuthentication - Revisit, see:
// if (isCloud()) return; // TODO: testPasswordAuthentication - Revisit, see:
String url = String.format("jdbc:ch:%s", getEndpointString());
Properties properties = new Properties();
properties.setProperty(ClickHouseHttpOption.REMEMBER_LAST_SET_ROLES.getKey(), "true");
Expand Down Expand Up @@ -216,4 +216,49 @@ private static Object[][] passwordAuthMethods() {
{ "sha256_password", "S3Cr?=t"},
};
}

@Test(groups = "integration", dataProvider = "headerAuthDataProvider")
public void testSwitchingBasicAuthToClickHouseHeaders(String identifyWith, String identifyBy, boolean shouldFail) throws SQLException {
// if (isCloud()) return; // TODO: testPasswordAuthentication - Revisit, see:
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 @@ -397,12 +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());
}
}

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 {
} 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 @@ -431,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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.github.tomakehurst.wiremock.http.Fault;
import com.github.tomakehurst.wiremock.http.trafficlistener.WiremockNetworkTrafficListener;
import org.apache.hc.core5.http.ConnectionRequestTimeoutException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.net.URIBuilder;
import org.eclipse.jetty.server.Server;
Expand All @@ -36,6 +37,7 @@
import java.nio.ByteBuffer;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -493,7 +495,7 @@ public void testSSLAuthentication() throws Exception {
}

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


try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), false)
.setUsername("some_user")
.setPassword(identifyBy)
Expand All @@ -521,22 +524,73 @@ public void testPasswordAuthentication(String identifyWith, String identifyBy) t
} catch (Exception e) {
Assert.fail("Failed to authenticate", e);
}

if (failsWithHeaders) {
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), false)
.setUsername("some_user")
.setPassword(identifyBy)
.useHTTPBasicAuth(false)
.build()) {

Assert.expectThrows(ClientException.class, () ->
client.queryAll("SELECT user()").get(0).getString(1));

} catch (Exception e) {
Assert.fail("Unexpected exception", e);
}
}
}

@DataProvider(name = "testPasswordAuthenticationProvider")
public static Object[][] testPasswordAuthenticationProvider() {
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"},
{ "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},
};
}

@Test(groups = { "integration" })
public void testAuthHeaderIsKeptFromUser() throws Exception {
if (isCloud()) {
return; // Current test is working only with local server because of self-signed certificates.
}
ClickHouseNode server = getServer(ClickHouseProtocol.HTTP);

String identifyWith = "sha256_password";
String identifyBy = "123§";
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), false)
.setUsername("default")
.setPassword("")
.build()) {

try (CommandResponse resp = client.execute("DROP USER IF EXISTS some_user").get()) {
}
try (CommandResponse resp = client.execute("CREATE USER some_user IDENTIFIED WITH " + identifyWith + " BY '" + identifyBy + "'").get()) {
}
} catch (Exception e) {
Assert.fail("Failed on setup", e);
}


try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost",server.getPort(), false)
.setUsername("some_user")
.setPassword(identifyBy)
.useHTTPBasicAuth(false) // disable basic auth to produce CH headers
.httpHeader(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString(("some_user:" +identifyBy).getBytes()))
.build()) {

Assert.assertEquals(client.queryAll("SELECT user()").get(0).getString(1), "some_user");
} catch (Exception e) {
Assert.fail("Failed to authenticate", e);
}
}

@Test(groups = { "integration" })
public void testSSLAuthentication_invalidConfig() throws Exception {
if (isCloud()) {
Expand Down

0 comments on commit 8ffa30a

Please sign in to comment.