From 6e9a857d3eaf6d3a218b58811cef6de58de79019 Mon Sep 17 00:00:00 2001
From: Maciej Maciejko
Date: Wed, 31 Jan 2024 13:21:15 +0100
Subject: [PATCH 01/10] http client with kerb auth
---
.gitignore | 1 +
CHANGELOG.md | 2 +
.../clickhouse/client/ClickHouseConfig.java | 20 ++++-
.../client/ClickHouseCredentials.java | 52 +++++++-----
.../com/clickhouse/client/ClickHouseNode.java | 15 +++-
.../client/config/ClickHouseClientOption.java | 8 ++
.../client/config/ClickHouseDefaults.java | 6 +-
clickhouse-http-client/pom.xml | 6 ++
.../client/gss/GssAuthorizationContext.java | 83 +++++++++++++++++++
.../client/gss/SubjectProvider.java | 13 +++
.../client/http/ApacheHttpConnectionImpl.java | 6 +-
.../client/http/ClickHouseHttpClient.java | 9 +-
.../client/http/ClickHouseHttpConnection.java | 77 +++++++++++++----
.../http/ClickHouseHttpConnectionFactory.java | 7 +-
.../client/http/HttpUrlConnectionImpl.java | 5 +-
.../http/ClickHouseHttpConnectionFactory.java | 9 +-
.../client/http/HttpClientConnectionImpl.java | 5 +-
.../http/ClickHouseHttpConnectionTest.java | 68 +++++++++++++--
.../http/DefaultHttpConnectionTest.java | 2 +-
.../internal/ClickHouseJdbcUrlParser.java | 3 +-
.../internal/ClickHouseJdbcUrlParserTest.java | 25 +++++-
.../examples/jdbc/GssAuthClient.java | 32 +++++++
pom.xml | 1 +
23 files changed, 388 insertions(+), 67 deletions(-)
create mode 100644 clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java
create mode 100644 clickhouse-http-client/src/main/java/com/clickhouse/client/gss/SubjectProvider.java
create mode 100644 examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
diff --git a/.gitignore b/.gitignore
index f2ada3a6f..79c685324 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
*.war
*.ear
*.out
+.java-version
# VSCode
.bloop
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 88614def9..caa66fa7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
## 0.6.1
+- Supporting Kerberos auth in HTTP client.
+
### Bug Fixes
## 0.6.0
diff --git a/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseConfig.java b/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseConfig.java
index 60098dc9b..bbe13dea6 100644
--- a/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseConfig.java
+++ b/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseConfig.java
@@ -232,6 +232,8 @@ public static Map toClientOptions(Map, ?> prop
private final String keyStoreType;
private final String trustStore;
private final String trustStorePassword;
+ private final boolean gssEnabled;
+ private final String kerberosServerName;
private final int transactionTimeout;
private final boolean widenUnsignedTypes;
private final boolean useBinaryString;
@@ -352,6 +354,8 @@ public ClickHouseConfig(Map options, ClickHouseC
this.keyStoreType = getStrOption(ClickHouseClientOption.KEY_STORE_TYPE);
this.trustStore = getStrOption(ClickHouseClientOption.TRUST_STORE);
this.trustStorePassword = getStrOption(ClickHouseClientOption.KEY_STORE_PASSWORD);
+ this.gssEnabled = getBoolOption(ClickHouseClientOption.GSS_ENABLED);
+ this.kerberosServerName = getStrOption(ClickHouseClientOption.KERBEROS_SERVER_NAME);
this.transactionTimeout = getIntOption(ClickHouseClientOption.TRANSACTION_TIMEOUT);
this.widenUnsignedTypes = getBoolOption(ClickHouseClientOption.WIDEN_UNSIGNED_TYPES);
this.useBinaryString = getBoolOption(ClickHouseClientOption.USE_BINARY_STRING);
@@ -368,8 +372,12 @@ public ClickHouseConfig(Map options, ClickHouseC
this.timeZoneForDate = this.useServerTimeZoneForDates ? this.useTimeZone : null;
if (credentials == null) {
- this.credentials = ClickHouseCredentials.fromUserAndPassword(getStrOption(ClickHouseDefaults.USER),
- getStrOption(ClickHouseDefaults.PASSWORD));
+ String user = getStrOption(ClickHouseDefaults.USER);
+ if (this.gssEnabled) {
+ this.credentials = ClickHouseCredentials.withGss(user);
+ } else {
+ this.credentials = ClickHouseCredentials.fromUserAndPassword(user, getStrOption(ClickHouseDefaults.PASSWORD));
+ }
} else {
this.credentials = credentials;
}
@@ -642,6 +650,14 @@ public String getTrustStorePassword() {
return trustStorePassword;
}
+ public boolean isGssEnabled() {
+ return gssEnabled;
+ }
+
+ public String getKerberosServerName() {
+ return kerberosServerName;
+ }
+
public int getTransactionTimeout() {
return transactionTimeout < 1 ? sessionTimeout : transactionTimeout;
}
diff --git a/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseCredentials.java b/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseCredentials.java
index e9bf2c582..a723f96a0 100644
--- a/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseCredentials.java
+++ b/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseCredentials.java
@@ -17,6 +17,7 @@ public class ClickHouseCredentials implements Serializable {
private final String userName;
private final String password;
// TODO sslCert
+ private final boolean gssEnabled;
/**
* Create credentials from access token.
@@ -25,7 +26,7 @@ public class ClickHouseCredentials implements Serializable {
* @return credentials object for authentication
*/
public static ClickHouseCredentials fromAccessToken(String accessToken) {
- return new ClickHouseCredentials(accessToken);
+ return new ClickHouseCredentials(null, null, accessToken, false);
}
/**
@@ -36,43 +37,49 @@ public static ClickHouseCredentials fromAccessToken(String accessToken) {
* @return credentials object for authentication
*/
public static ClickHouseCredentials fromUserAndPassword(String userName, String password) {
- return new ClickHouseCredentials(userName, password);
+ return ClickHouseCredentials.fromUserAndPassword(userName, password, false);
}
- /**
- * Construct credentials object using access token.
- *
- * @param accessToken access token
- */
- protected ClickHouseCredentials(String accessToken) {
- this.accessToken = ClickHouseChecker.nonNull(accessToken, "accessToken");
- this.userName = null;
- this.password = null;
+ public static ClickHouseCredentials fromUserAndPassword(String userName, String password, boolean useGss) {
+ ClickHouseChecker.nonBlank(userName, "userName");
+ return new ClickHouseCredentials(userName, password != null ? password : "", null, useGss);
}
/**
- * Construct credentials using user name and password.
- *
+ * Create credentials for GSS authentication.
+ *
* @param userName user name
- * @param password password
+ * @return credentials object for authentication
*/
- protected ClickHouseCredentials(String userName, String password) {
- this.accessToken = null;
+ public static ClickHouseCredentials withGss(String userName) {
+ ClickHouseChecker.nonBlank(userName, "userName");
+ return new ClickHouseCredentials(userName, null, null, true);
+ }
- this.userName = ClickHouseChecker.nonBlank(userName, "userName");
- this.password = password != null ? password : "";
+ private ClickHouseCredentials(String userName, String password, String accessToken, boolean gssEnabled) {
+ this.userName = userName;
+ this.password = password;
+ this.accessToken = accessToken;
+ this.gssEnabled = gssEnabled;
}
public boolean useAccessToken() {
return accessToken != null;
}
+ public boolean isGssEnabled() {
+ return gssEnabled;
+ }
+
/**
* Get access token.
*
* @return access token
*/
public String getAccessToken() {
+ if (isGssEnabled()) {
+ throw new IllegalStateException("Authentication with access token disabled. Use GSS auth instead.");
+ }
if (!useAccessToken()) {
throw new IllegalStateException("No access token specified, please use user name and password instead.");
}
@@ -100,12 +107,15 @@ public String getPassword() {
if (useAccessToken()) {
throw new IllegalStateException("No user name and password specified, please use access token instead.");
}
+ if (isGssEnabled()) {
+ throw new IllegalStateException("Password authentication disabled. Use GSS auth instead.");
+ }
return this.password;
}
@Override
public int hashCode() {
- return Objects.hash(accessToken, userName, password);
+ return Objects.hash(accessToken, userName, password, gssEnabled);
}
@Override
@@ -119,7 +129,7 @@ public boolean equals(Object obj) {
}
ClickHouseCredentials c = (ClickHouseCredentials) obj;
- return Objects.equals(accessToken, c.accessToken) && Objects.equals(userName, c.userName)
- && Objects.equals(password, c.password);
+ return Objects.equals(accessToken, c.accessToken) && Objects.equals(userName, c.userName)
+ && Objects.equals(password, c.password) && Objects.equals(gssEnabled, c.gssEnabled);
}
}
diff --git a/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseNode.java b/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseNode.java
index e5a874fa2..37afb7cce 100644
--- a/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseNode.java
+++ b/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseNode.java
@@ -509,9 +509,13 @@ static ClickHouseCredentials extract(String rawUserInfo, Map par
ClickHouseCredentials credentials = defaultCredentials;
String user = "";
String passwd = "";
+ boolean gssEnabled = false;
if (credentials != null && !credentials.useAccessToken()) {
user = credentials.getUserName();
- passwd = credentials.getPassword();
+ gssEnabled = credentials.isGssEnabled();
+ if (!gssEnabled) {
+ passwd = credentials.getPassword();
+ }
}
if (!ClickHouseChecker.isNullOrEmpty(rawUserInfo)) {
@@ -535,11 +539,16 @@ static ClickHouseCredentials extract(String rawUserInfo, Map par
if (str != null) {
passwd = str;
}
+ str = params.remove(ClickHouseDefaults.GSS_ENABLED.getKey());
+ if (str != null) {
+ gssEnabled = Boolean.parseBoolean(str);
+ }
+
if (!ClickHouseChecker.isNullOrEmpty(user)) {
- credentials = ClickHouseCredentials.fromUserAndPassword(user, passwd);
+ credentials = ClickHouseCredentials.fromUserAndPassword(user, passwd, gssEnabled);
} else if (!ClickHouseChecker.isNullOrEmpty(passwd)) {
credentials = ClickHouseCredentials
- .fromUserAndPassword((String) ClickHouseDefaults.USER.getEffectiveDefaultValue(), passwd);
+ .fromUserAndPassword((String) ClickHouseDefaults.USER.getEffectiveDefaultValue(), passwd, gssEnabled);
}
return credentials;
}
diff --git a/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseClientOption.java b/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseClientOption.java
index c88963f20..2c6368cc3 100644
--- a/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseClientOption.java
+++ b/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseClientOption.java
@@ -355,6 +355,14 @@ public enum ClickHouseClientOption implements ClickHouseOption {
* Trust Store password.
*/
KEY_STORE_PASSWORD("key_store_password", "", "Password needed to access the keystore file specified in the keystore config", true),
+ /**
+ * Enable GSS authorization.
+ */
+ GSS_ENABLED("gss_enabled", false, "Enable Authorization via GSS. Supported only for http protocol"),
+ /**
+ * Kerberos server name. Used by GSS authorization.
+ */
+ KERBEROS_SERVER_NAME("kerberos_server_name", "clickhouse", "Kerberos server name for GSS authorization"),
/**
* Transaction timeout in seconds.
*/
diff --git a/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseDefaults.java b/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseDefaults.java
index b2f482b70..bad650bff 100644
--- a/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseDefaults.java
+++ b/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseDefaults.java
@@ -103,7 +103,11 @@ public enum ClickHouseDefaults implements ClickHouseOption {
* {@link com.clickhouse.client.naming.SrvResolver}(e.g. resolve SRV record to
* extract both host and port from a given name).
*/
- SRV_RESOLVE("srv_resolve", false, "Whether to resolve DNS SRV name.");
+ SRV_RESOLVE("srv_resolve", false, "Whether to resolve DNS SRV name."),
+ /**
+ * Enable GSS authentication.
+ */
+ GSS_ENABLED("gss_enabled", false, "Enable GSS authentication. Supported only by HTTP protocol.");
private final String key;
private final Serializable defaultValue;
diff --git a/clickhouse-http-client/pom.xml b/clickhouse-http-client/pom.xml
index 2c4eeb1bf..74a31a105 100644
--- a/clickhouse-http-client/pom.xml
+++ b/clickhouse-http-client/pom.xml
@@ -103,6 +103,12 @@
testng
test
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java
new file mode 100644
index 000000000..eb9b32751
--- /dev/null
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java
@@ -0,0 +1,83 @@
+package com.clickhouse.client.gss;
+
+import java.io.Serializable;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+
+import org.apache.hc.client5.http.utils.Base64;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+
+import com.clickhouse.client.config.ClickHouseDefaults;
+import com.clickhouse.logging.Logger;
+import com.clickhouse.logging.LoggerFactory;
+
+public class GssAuthorizationContext implements Serializable {
+
+ private static final Logger LOG = LoggerFactory.getLogger(GssAuthorizationContext.class);
+
+ private final Subject subject;
+
+ private GssAuthorizationContext(Subject subject) {
+ this.subject = subject;
+ }
+
+ public static GssAuthorizationContext initialize() {
+ return new GssAuthorizationContext(SubjectProvider.getSubject());
+ }
+
+ public String getAuthToken(String user, String serverName, String host) throws GSSException {
+ GSSCredential gssCredential = null;
+ if (subject != null) {
+ LOG.debug("Getting private credentials from subject");
+ Set gssCreds = subject.getPrivateCredentials(GSSCredential.class);
+ if (gssCreds != null && !gssCreds.isEmpty()) {
+ gssCredential = gssCreds.iterator().next();
+ }
+ }
+
+ GSSManager manager = GSSManager.getInstance();
+ Oid desiredMech = getKerberosMech();
+ if (gssCredential == null) {
+ if (hasSpnegoSupport(manager)) {
+ desiredMech = getSpnegoMech();
+ }
+
+ GSSName gssClientName = null;
+ if (!ClickHouseDefaults.USER.getDefaultValue().equals(user)) {
+ gssClientName = manager.createName(user, GSSName.NT_USER_NAME);
+ } else {
+ LOG.debug("GSS credential name ignored. User name is default");
+ }
+ gssCredential = manager.createCredential(gssClientName, 8 * 3600, desiredMech, GSSCredential.INITIATE_ONLY);
+ }
+ GSSName gssServerName = manager.createName(serverName + "@" + host, GSSName.NT_HOSTBASED_SERVICE);
+ GSSContext secContext = manager.createContext(gssServerName, desiredMech, gssCredential,
+ GSSContext.DEFAULT_LIFETIME);
+ secContext.requestMutualAuth(true);
+ return Base64.encodeBase64String(secContext.initSecContext(new byte[0], 0, 0));
+ }
+
+ private static Oid getSpnegoMech() throws GSSException {
+ return new Oid("1.3.6.1.5.5.2");
+ }
+
+ private static Oid getKerberosMech() throws GSSException {
+ return new Oid("1.2.840.113554.1.2.2");
+ }
+
+ private static boolean hasSpnegoSupport(GSSManager manager) throws GSSException {
+ Oid spnego = getSpnegoMech();
+ for (Oid mech : manager.getMechs()) {
+ if (mech.equals(spnego)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/SubjectProvider.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/SubjectProvider.java
new file mode 100644
index 000000000..2b1df842d
--- /dev/null
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/SubjectProvider.java
@@ -0,0 +1,13 @@
+package com.clickhouse.client.gss;
+
+import java.security.AccessController;
+
+import javax.security.auth.Subject;
+
+class SubjectProvider {
+
+ static Subject getSubject() {
+ return Subject.getSubject(AccessController.getContext()); // TODO add handling for java 18+
+ }
+
+}
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ApacheHttpConnectionImpl.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ApacheHttpConnectionImpl.java
index 93879e05a..3fb2105a4 100644
--- a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ApacheHttpConnectionImpl.java
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ApacheHttpConnectionImpl.java
@@ -10,6 +10,7 @@
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.config.ClickHouseProxyType;
import com.clickhouse.client.config.ClickHouseSslMode;
+import com.clickhouse.client.gss.GssAuthorizationContext;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.data.ClickHouseChecker;
import com.clickhouse.data.ClickHouseExternalTable;
@@ -57,7 +58,6 @@
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Socket;
-import java.net.StandardSocketOptions;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
@@ -73,9 +73,9 @@ public class ApacheHttpConnectionImpl extends ClickHouseHttpConnection {
private final CloseableHttpClient client;
- protected ApacheHttpConnectionImpl(ClickHouseNode server, ClickHouseRequest> request, ExecutorService executor)
+ protected ApacheHttpConnectionImpl(ClickHouseNode server, ClickHouseRequest> request, ExecutorService executor, GssAuthorizationContext gssAuthContext)
throws IOException {
- super(server, request);
+ super(server, request, gssAuthContext);
client = newConnection(config);
}
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpClient.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpClient.java
index 2c6f757ac..7cdf655d1 100644
--- a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpClient.java
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpClient.java
@@ -17,6 +17,7 @@
import com.clickhouse.client.ClickHouseRequest;
import com.clickhouse.client.ClickHouseResponse;
import com.clickhouse.client.ClickHouseTransaction;
+import com.clickhouse.client.gss.GssAuthorizationContext;
import com.clickhouse.client.ClickHouseStreamResponse;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.config.ClickHouseOption;
@@ -52,8 +53,14 @@ protected ClickHouseHttpConnection newConnection(ClickHouseHttpConnection connec
closeConnection(connection, false);
}
+ GssAuthorizationContext gssAuthContext = null;
+ if (connection != null) {
+ gssAuthContext = connection.getGssAuthorizationContext();
+ } else {
+ gssAuthContext = GssAuthorizationContext.initialize();
+ }
try {
- return ClickHouseHttpConnectionFactory.createConnection(server, request, getExecutor());
+ return ClickHouseHttpConnectionFactory.createConnection(server, request, getExecutor(), gssAuthContext);
} catch (IOException e) {
throw new CompletionException(e);
}
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnection.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnection.java
index 6ef61c976..73e590202 100644
--- a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnection.java
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnection.java
@@ -10,11 +10,15 @@
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
+
+import org.ietf.jgss.GSSException;
+
import java.util.Map.Entry;
import com.clickhouse.client.ClickHouseClient;
@@ -25,6 +29,7 @@
import com.clickhouse.client.ClickHouseRequestManager;
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.config.ClickHouseProxyType;
+import com.clickhouse.client.gss.GssAuthorizationContext;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.config.ClickHouseOption;
import com.clickhouse.data.ClickHouseByteUtils;
@@ -47,6 +52,8 @@ public abstract class ClickHouseHttpConnection implements AutoCloseable {
private static final byte[] HEADER_BINARY_ENCODING = "content-transfer-encoding: binary\r\n\r\n"
.getBytes(StandardCharsets.US_ASCII);
+ private static final String HEADER_AUTHORIZATION = "authorization";
+
private static final byte[] ERROR_MSG_PREFIX = "ode: ".getBytes(StandardCharsets.US_ASCII);
private static final byte[] DOUBLE_DASH = new byte[] { '-', '-' };
@@ -200,7 +207,7 @@ protected static Map createDefaultHeaders(ClickHouseConfig confi
if (value == null) {
continue;
}
- if ("authorization".equals(name)) {
+ if (HEADER_AUTHORIZATION.equals(name)) {
hasAuthorizationHeader = true;
}
map.put(name, value);
@@ -212,16 +219,18 @@ protected static Map createDefaultHeaders(ClickHouseConfig confi
}
map.put("user-agent", !ClickHouseChecker.isNullOrEmpty(userAgent) ? userAgent : config.getClientName());
- ClickHouseCredentials credentials = server.getCredentials(config);
- if (credentials.useAccessToken()) {
- // 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());
+ ClickHouseCredentials credentials = getCredentials(config, server);
+ if (!credentials.isGssEnabled()) {
+ if (credentials.useAccessToken()) {
+ // TODO check if auth-scheme is available and supported
+ map.put(HEADER_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());
+ }
}
}
@@ -241,6 +250,10 @@ protected static Map createDefaultHeaders(ClickHouseConfig confi
return map;
}
+ private static ClickHouseCredentials getCredentials(ClickHouseConfig config, ClickHouseNode server) {
+ return server.getCredentials(config);
+ }
+
protected static Proxy getProxy(ClickHouseConfig config) {
final ClickHouseProxyType proxyType = config.getProxyType();
@@ -350,10 +363,11 @@ protected static void postData(ClickHouseConfig config, byte[] boundary, String
protected final ClickHouseRequestManager rm;
protected final ClickHouseConfig config;
- protected final Map defaultHeaders;
protected final String url;
+ protected final Map defaultHeaders;
+ protected final GssAuthorizationContext gssAuthContext;
- protected ClickHouseHttpConnection(ClickHouseNode server, ClickHouseRequest> request) {
+ protected ClickHouseHttpConnection(ClickHouseNode server, ClickHouseRequest> request, GssAuthorizationContext gssAuthContext) {
if (server == null || request == null) {
throw new IllegalArgumentException("Non-null server and request are required");
}
@@ -363,9 +377,14 @@ protected ClickHouseHttpConnection(ClickHouseNode server, ClickHouseRequest> r
ClickHouseConfig c = request.getConfig();
this.config = c;
- this.defaultHeaders = Collections.unmodifiableMap(createDefaultHeaders(c, server, getUserAgent()));
this.url = buildUrl(server.getBaseUri(), request);
+ this.defaultHeaders = Collections.unmodifiableMap(createDefaultHeaders(c, server, getUserAgent()));
log.debug("url [%s]", this.url);
+ this.gssAuthContext = gssAuthContext;
+ }
+
+ protected GssAuthorizationContext getGssAuthorizationContext() {
+ return gssAuthContext;
}
protected void closeQuietly() {
@@ -416,13 +435,13 @@ protected final String getUserAgent() {
*/
protected Map mergeHeaders(Map requestHeaders) {
if (requestHeaders == null || requestHeaders.isEmpty()) {
- return defaultHeaders;
+ return setGssAuthHeader(new HashMap<>(defaultHeaders), config, server);
} else if (isReusable()) {
- return requestHeaders;
+ return setGssAuthHeader(requestHeaders, config, server);
}
Map merged = new LinkedHashMap<>();
- merged.putAll(defaultHeaders);
+ merged.putAll(requestHeaders);
for (Entry header : requestHeaders.entrySet()) {
String name = header.getKey().toLowerCase(Locale.ROOT);
String value = header.getValue();
@@ -432,9 +451,33 @@ protected Map mergeHeaders(Map requestHeaders) {
merged.put(name, value);
}
}
+ setGssAuthHeader(merged, config, server);
return merged;
}
+ private Map setGssAuthHeader(Map headers, ClickHouseConfig config,
+ ClickHouseNode server) {
+ if (headers.containsKey(HEADER_AUTHORIZATION)) {
+ log.warn("Authorization header is present. Skipping");
+ return headers;
+ }
+ ClickHouseCredentials credentials = getCredentials(config, server);
+ if (credentials.isGssEnabled()) {
+ if (gssAuthContext == null) {
+ throw new IllegalStateException("GssAuthorizer not initialized");
+ }
+ String userName = credentials.getUserName();
+ try {
+ headers.put(HEADER_AUTHORIZATION, "Negotiate "
+ + gssAuthContext.getAuthToken(userName, config.getKerberosServerName(), server.getHost()));
+ } catch (GSSException e) {
+ throw new RuntimeException("Can not generate GSS token for user " + userName + " host "
+ + server.getHost() + " with name " + config.getKerberosServerName(), e);
+ }
+ }
+ return headers;
+ }
+
/**
* Posts query and data to server.
*
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnectionFactory.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnectionFactory.java
index dc6247093..fa484fcce 100644
--- a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnectionFactory.java
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnectionFactory.java
@@ -5,6 +5,7 @@
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.client.ClickHouseRequest;
+import com.clickhouse.client.gss.GssAuthorizationContext;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.client.http.config.HttpConnectionProvider;
import com.clickhouse.logging.Logger;
@@ -14,12 +15,12 @@ public final class ClickHouseHttpConnectionFactory {
private static final Logger log = LoggerFactory.getLogger(ClickHouseHttpConnectionFactory.class);
public static ClickHouseHttpConnection createConnection(ClickHouseNode server, ClickHouseRequest> request,
- ExecutorService executor) throws IOException {
+ ExecutorService executor, GssAuthorizationContext gssAuthContext) throws IOException {
HttpConnectionProvider provider = request.getConfig().getOption(ClickHouseHttpOption.CONNECTION_PROVIDER,
HttpConnectionProvider.class);
if (provider == HttpConnectionProvider.APACHE_HTTP_CLIENT) {
try {
- return new ApacheHttpConnectionImpl(server, request, executor);
+ return new ApacheHttpConnectionImpl(server, request, executor, gssAuthContext);
} catch (ExceptionInInitializerError | NoClassDefFoundError t) {
log.warn("Error when creating %s, fall back to HTTP_URL_CONNECTION", provider, t);
}
@@ -27,7 +28,7 @@ public static ClickHouseHttpConnection createConnection(ClickHouseNode server, C
log.warn("HTTP_CLIENT is only supported in JDK 11 or above, fall back to HTTP_URL_CONNECTION");
}
- return new HttpUrlConnectionImpl(server, request, executor);
+ return new HttpUrlConnectionImpl(server, request, executor, gssAuthContext);
}
private ClickHouseHttpConnectionFactory() {
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/HttpUrlConnectionImpl.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/HttpUrlConnectionImpl.java
index 68df43c6a..0fed42bb8 100644
--- a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/HttpUrlConnectionImpl.java
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/HttpUrlConnectionImpl.java
@@ -7,6 +7,7 @@
import com.clickhouse.client.ClickHouseSslContextProvider;
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.config.ClickHouseSslMode;
+import com.clickhouse.client.gss.GssAuthorizationContext;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.data.ClickHouseChecker;
import com.clickhouse.data.ClickHouseExternalTable;
@@ -193,9 +194,9 @@ private void checkResponse(HttpURLConnection conn) throws IOException {
}
}
- protected HttpUrlConnectionImpl(ClickHouseNode server, ClickHouseRequest> request, ExecutorService executor)
+ protected HttpUrlConnectionImpl(ClickHouseNode server, ClickHouseRequest> request, ExecutorService executor, GssAuthorizationContext gssAuthContext)
throws IOException {
- super(server, request);
+ super(server, request, gssAuthContext);
conn = newConnection(url, true);
}
diff --git a/clickhouse-http-client/src/main/java11/com/clickhouse/client/http/ClickHouseHttpConnectionFactory.java b/clickhouse-http-client/src/main/java11/com/clickhouse/client/http/ClickHouseHttpConnectionFactory.java
index 3e49d4154..834cbc304 100644
--- a/clickhouse-http-client/src/main/java11/com/clickhouse/client/http/ClickHouseHttpConnectionFactory.java
+++ b/clickhouse-http-client/src/main/java11/com/clickhouse/client/http/ClickHouseHttpConnectionFactory.java
@@ -5,6 +5,7 @@
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.client.ClickHouseRequest;
+import com.clickhouse.client.gss.GssAuthorizationContext;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.client.http.config.HttpConnectionProvider;
import com.clickhouse.logging.Logger;
@@ -14,20 +15,20 @@ public final class ClickHouseHttpConnectionFactory {
private static final Logger log = LoggerFactory.getLogger(ClickHouseHttpConnectionFactory.class);
public static ClickHouseHttpConnection createConnection(ClickHouseNode server, ClickHouseRequest> request,
- ExecutorService executor) throws IOException {
+ ExecutorService executor, GssAuthorizationContext gssAuthContext) throws IOException {
HttpConnectionProvider provider = request.getConfig().getOption(ClickHouseHttpOption.CONNECTION_PROVIDER,
HttpConnectionProvider.class);
if (provider == HttpConnectionProvider.APACHE_HTTP_CLIENT) {
try {
- return new ApacheHttpConnectionImpl(server, request, executor);
+ return new ApacheHttpConnectionImpl(server, request, executor, gssAuthContext);
} catch (ExceptionInInitializerError | NoClassDefFoundError t) {
log.warn("Error when creating %s, fall back to HTTP_URL_CONNECTION", provider, t);
}
} else if (provider == HttpConnectionProvider.HTTP_CLIENT) {
- return new HttpClientConnectionImpl(server, request, executor);
+ return new HttpClientConnectionImpl(server, request, executor, gssAuthContext);
}
- return new HttpUrlConnectionImpl(server, request, executor);
+ return new HttpUrlConnectionImpl(server, request, executor, gssAuthContext);
}
private ClickHouseHttpConnectionFactory() {
diff --git a/clickhouse-http-client/src/main/java11/com/clickhouse/client/http/HttpClientConnectionImpl.java b/clickhouse-http-client/src/main/java11/com/clickhouse/client/http/HttpClientConnectionImpl.java
index a3006ba8d..effa81fa2 100644
--- a/clickhouse-http-client/src/main/java11/com/clickhouse/client/http/HttpClientConnectionImpl.java
+++ b/clickhouse-http-client/src/main/java11/com/clickhouse/client/http/HttpClientConnectionImpl.java
@@ -7,6 +7,7 @@
import com.clickhouse.client.ClickHouseSslContextProvider;
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.config.ClickHouseProxyType;
+import com.clickhouse.client.gss.GssAuthorizationContext;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.data.ClickHouseChecker;
import com.clickhouse.data.ClickHouseDataStreamFactory;
@@ -173,9 +174,9 @@ private HttpRequest newRequest(String url) {
.timeout(Duration.ofMillis(config.getSocketTimeout())).build();
}
- protected HttpClientConnectionImpl(ClickHouseNode server, ClickHouseRequest> request, ExecutorService executor)
+ protected HttpClientConnectionImpl(ClickHouseNode server, ClickHouseRequest> request, ExecutorService executor, GssAuthorizationContext gssAuthContext)
throws IOException {
- super(server, request);
+ super(server, request, gssAuthContext);
HttpClient.Builder builder = HttpClient.newBuilder().version(Version.HTTP_1_1)
.connectTimeout(Duration.ofMillis(config.getConnectionTimeout())).followRedirects(Redirect.NORMAL);
diff --git a/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ClickHouseHttpConnectionTest.java b/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ClickHouseHttpConnectionTest.java
index ca01c30b4..8938581f7 100644
--- a/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ClickHouseHttpConnectionTest.java
+++ b/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ClickHouseHttpConnectionTest.java
@@ -1,27 +1,39 @@
package com.clickhouse.client.http;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
import java.io.IOException;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.ietf.jgss.GSSException;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
import com.clickhouse.client.ClickHouseClient;
import com.clickhouse.client.ClickHouseConfig;
+import com.clickhouse.client.ClickHouseCredentials;
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.client.ClickHouseProtocol;
import com.clickhouse.client.ClickHouseRequest;
-import com.clickhouse.client.http.config.ClickHouseHttpOption;
+import com.clickhouse.client.config.ClickHouseClientOption;
+import com.clickhouse.client.gss.GssAuthorizationContext;
import com.clickhouse.data.ClickHouseExternalTable;
import com.clickhouse.data.ClickHouseFormat;
import com.clickhouse.data.ClickHouseInputStream;
import com.clickhouse.data.ClickHouseOutputStream;
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
public class ClickHouseHttpConnectionTest {
static class SimpleHttpConnection extends ClickHouseHttpConnection {
+
+ protected SimpleHttpConnection(ClickHouseNode server, ClickHouseRequest> request, GssAuthorizationContext gssAuthContext) {
+ super(server, request, gssAuthContext);
+ }
+
protected SimpleHttpConnection(ClickHouseNode server, ClickHouseRequest> request) {
- super(server, request);
+ this(server, request, null);
}
@Override
@@ -63,6 +75,52 @@ public void testDefaultHeaders() {
Assert.assertEquals(sc.defaultHeaders, sc.mergeHeaders(null));
}
+ @Test(groups = { "unit" })
+ public void testDefaultHeadersWithGssAuth() throws GSSException {
+ ClickHouseNode server = ClickHouseNode.builder()
+ .credentials(ClickHouseCredentials.withGss("userA"))
+ .addOption(ClickHouseClientOption.KERBEROS_SERVER_NAME.getKey(), "kerbServerName")
+ .build();
+
+ ClickHouseRequest> request = ClickHouseClient.newInstance().read(server);
+ GssAuthorizationContext gssAuthMode = mock(GssAuthorizationContext.class);
+ when(gssAuthMode.getAuthToken("userA", "kerbServerName", server.getHost())).thenReturn("AUTH_TOKEN_ABC");
+ SimpleHttpConnection sc = new SimpleHttpConnection(server, request, gssAuthMode);
+ Assert.assertFalse(sc.defaultHeaders.containsKey("authorization"));
+
+ Map headers = sc.mergeHeaders(null);
+
+ assertAuthHeader(headers, "AUTH_TOKEN_ABC");
+ }
+
+ @Test(groups = { "unit" })
+ public void testCustomHeadersWithGssAuth() throws GSSException {
+ ClickHouseNode server = ClickHouseNode.builder()
+ .credentials(ClickHouseCredentials.withGss("userB"))
+ .addOption(ClickHouseClientOption.KERBEROS_SERVER_NAME.getKey(), "kerbServerNameB")
+ .build();
+
+ ClickHouseRequest> request = ClickHouseClient.newInstance().read(server);
+ GssAuthorizationContext gssAuthMode = mock(GssAuthorizationContext.class);
+ when(gssAuthMode.getAuthToken("userB", "kerbServerNameB", server.getHost())).thenReturn("AUTH_TOKEN_ABCD");
+ SimpleHttpConnection sc = new SimpleHttpConnection(server, request, gssAuthMode);
+ Assert.assertFalse(sc.defaultHeaders.containsKey("authorization"));
+
+ Map customHeaders = new HashMap<>();
+ customHeaders.put("Content-type", "application/json");
+
+ Map headers = sc.mergeHeaders(customHeaders);
+
+ assertAuthHeader(headers, "AUTH_TOKEN_ABCD");
+ }
+
+ private void assertAuthHeader(Map headers, String token) {
+ Assert.assertTrue(!headers.isEmpty());
+ Assert.assertEquals(headers.get("authorization"), "Negotiate " + token);
+ Assert.assertFalse(headers.containsKey("x-clickhouse-user"));
+ Assert.assertFalse(headers.containsKey("x-clickhouse-key"));
+ }
+
@Test(groups = { "unit" })
public void testGetBaseUrl() {
ClickHouseNode server = ClickHouseNode.of("https://localhost/db1");
diff --git a/clickhouse-http-client/src/test/java/com/clickhouse/client/http/DefaultHttpConnectionTest.java b/clickhouse-http-client/src/test/java/com/clickhouse/client/http/DefaultHttpConnectionTest.java
index b6924500f..391de6a40 100644
--- a/clickhouse-http-client/src/test/java/com/clickhouse/client/http/DefaultHttpConnectionTest.java
+++ b/clickhouse-http-client/src/test/java/com/clickhouse/client/http/DefaultHttpConnectionTest.java
@@ -19,7 +19,7 @@ public void testConnectionReuse() throws IOException {
try (ClickHouseClient client = ClickHouseClient.newInstance()) {
ClickHouseRequest> req = client.read(server);
- ClickHouseHttpConnection conn = ClickHouseHttpConnectionFactory.createConnection(server, req, null);
+ ClickHouseHttpConnection conn = ClickHouseHttpConnectionFactory.createConnection(server, req, null, null);
Assert.assertNotNull(conn);
}
}
diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseJdbcUrlParser.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseJdbcUrlParser.java
index faf8650aa..2b8d5c406 100644
--- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseJdbcUrlParser.java
+++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseJdbcUrlParser.java
@@ -33,8 +33,9 @@ protected ConnectionInfo(String cacheKey, ClickHouseNodes nodes, Properties prop
if (props != null && !props.isEmpty()) {
String user = props.getProperty(ClickHouseDefaults.USER.getKey(), "");
String passwd = props.getProperty(ClickHouseDefaults.PASSWORD.getKey(), "");
+ String gssEnabled = props.getProperty(ClickHouseDefaults.GSS_ENABLED.getKey(), "" + ClickHouseDefaults.GSS_ENABLED.getDefaultValue());
if (!ClickHouseChecker.isNullOrEmpty(user)) {
- c = ClickHouseCredentials.fromUserAndPassword(user, passwd);
+ c = ClickHouseCredentials.fromUserAndPassword(user, passwd, Boolean.parseBoolean(gssEnabled));
}
}
this.credentials = c;
diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/internal/ClickHouseJdbcUrlParserTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/internal/ClickHouseJdbcUrlParserTest.java
index 406963f4e..3c4f297a3 100644
--- a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/internal/ClickHouseJdbcUrlParserTest.java
+++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/internal/ClickHouseJdbcUrlParserTest.java
@@ -146,12 +146,35 @@ public void testParseCredentials() throws SQLException {
ClickHouseNode server = connInfo.getServer();
Assert.assertEquals(connInfo.getDefaultCredentials().getUserName(), "default1");
Assert.assertEquals(connInfo.getDefaultCredentials().getPassword(), "password1");
+ Assert.assertFalse(connInfo.getDefaultCredentials().isGssEnabled());
Assert.assertEquals(server.getCredentials().get().getUserName(), "user");
Assert.assertEquals(server.getCredentials().get().getPassword(), "a:passwd");
+ Assert.assertFalse(server.getCredentials().get().isGssEnabled());
server = ClickHouseJdbcUrlParser.parse("jdbc:clickhouse://let%40me%3Ain:let%40me%3Ain@foo.ch", null)
- .getServer();
+ .getServer();
Assert.assertEquals(server.getCredentials().get().getUserName(), "let@me:in");
Assert.assertEquals(server.getCredentials().get().getPassword(), "let@me:in");
}
+
+ @Test(groups = "unit")
+ public void testParseGssCredentials() throws SQLException {
+ Properties props = new Properties();
+ props.setProperty("user", "default1");
+ props.setProperty("gss_enabled", "true");
+ ConnectionInfo connInfo = ClickHouseJdbcUrlParser.parse("jdbc:clickhouse://user:a:passwd@foo.ch/test",
+ props);
+ ClickHouseNode server = connInfo.getServer();
+ Assert.assertEquals(connInfo.getDefaultCredentials().getUserName(), "default1");
+ Assert.assertTrue(connInfo.getDefaultCredentials().isGssEnabled());
+ Assert.assertThrows(IllegalStateException.class, () -> connInfo.getDefaultCredentials().getPassword());
+ Assert.assertEquals(server.getCredentials().get().getUserName(), "user");
+ Assert.assertTrue(server.getCredentials().get().isGssEnabled());
+ Assert.assertThrows(IllegalStateException.class, () -> connInfo.getDefaultCredentials().getPassword());
+
+ server = ClickHouseJdbcUrlParser.parse("jdbc:clickhouse://let%40me%3Ain:let%40me%3Ain@foo.ch", null)
+ .getServer();
+ Assert.assertEquals(server.getCredentials().get().getUserName(), "let@me:in");
+ Assert.assertThrows(IllegalStateException.class, () -> connInfo.getDefaultCredentials().getPassword());
+ }
}
diff --git a/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java b/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
new file mode 100644
index 000000000..43a0c8f76
--- /dev/null
+++ b/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
@@ -0,0 +1,32 @@
+package com.clickhouse.examples.jdbc;
+
+/**
+ * Sample of using clickhouse jdbc client with kerberos auth.
+ *
+ * https://clickhouse.com/docs/en/operations/external-authenticators/kerberos
+ */
+public class GssAuthClient {
+
+ private void execute(String url) {
+ String url = "jdbc:ch:http://localhost:8123/default"; // only http protocol supports GSS auth
+ Properties props = new Properties();
+ props.setProperty("user", "userA");
+ props.setProperty("gss_enabled", "true");
+ props.setProperty("kerberos_server_name", "HTTP");
+ try (Connection conn = DriverManager.getConnection(url, props)) {
+ ResultSet rs = conn.createStatement().executeQuery("SELECT currentUser();");
+ while (rs.next()) {
+ System.out.println(rs.getString(1));
+ }
+ }
+ }
+
+
+ public static void main(String...args) {
+ System.setProperty("java.security.krb5.conf", "/etc/krb5.conf");
+ System.setProperty("java.security.auth.login.config", "/etc/jaas.conf");
+ System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
+
+ new GssAuthClient().execute();
+ }
+}
diff --git a/pom.xml b/pom.xml
index a8f2bb3a5..fb733cf96 100644
--- a/pom.xml
+++ b/pom.xml
@@ -115,6 +115,7 @@
1.18.3
7.5.1
+ 5.9.0
3.1.4
8.1.0
From c07099c747a9a0066d129c48765cf437c1bd3316 Mon Sep 17 00:00:00 2001
From: Maciej Maciejko
Date: Wed, 7 Feb 2024 11:14:43 +0100
Subject: [PATCH 02/10] integration test
---
.../clickhouse/client/KdcServerForTest.java | 175 ++++++++++++++++++
.../src/test/resources/client_jaas.conf | 10 +
.../src/test/resources/client_krb5.conf | 16 ++
.../config.d/custom_config.xml | 5 +-
.../clickhouse-server/users.d/users.xml | 8 +
.../containers/kdc-server/Dockerfile | 36 ++++
.../containers/kdc-server/index.html | 10 +
.../resources/containers/kdc-server/kadm5.acl | 8 +
.../resources/containers/kdc-server/kdc.conf | 29 +++
.../resources/containers/kdc-server/krb5.conf | 28 +++
.../containers/kdc-server/supervisord.conf | 41 ++++
.../client/gss/GssAuthorizationContext.java | 24 ++-
.../clickhouse/jdbc/SystemPropertiesMock.java | 50 +++++
.../ClickHouserDriverWIthGssAuthTest.java | 58 ++++++
pom.xml | 2 +-
15 files changed, 494 insertions(+), 6 deletions(-)
create mode 100644 clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
create mode 100644 clickhouse-client/src/test/resources/client_jaas.conf
create mode 100644 clickhouse-client/src/test/resources/client_krb5.conf
create mode 100644 clickhouse-client/src/test/resources/containers/kdc-server/Dockerfile
create mode 100644 clickhouse-client/src/test/resources/containers/kdc-server/index.html
create mode 100644 clickhouse-client/src/test/resources/containers/kdc-server/kadm5.acl
create mode 100644 clickhouse-client/src/test/resources/containers/kdc-server/kdc.conf
create mode 100644 clickhouse-client/src/test/resources/containers/kdc-server/krb5.conf
create mode 100644 clickhouse-client/src/test/resources/containers/kdc-server/supervisord.conf
create mode 100644 clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/SystemPropertiesMock.java
create mode 100644 clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
diff --git a/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java b/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
new file mode 100644
index 000000000..5af692437
--- /dev/null
+++ b/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
@@ -0,0 +1,175 @@
+package com.clickhouse.client;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+import org.apache.commons.compress.utils.IOUtils;
+import org.testcontainers.containers.Container.ExecResult;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.images.builder.ImageFromDockerfile;
+import org.testcontainers.shaded.org.apache.commons.lang3.StringUtils;
+import org.testcontainers.utility.MountableFile;
+
+import com.clickhouse.logging.Logger;
+import com.clickhouse.logging.LoggerFactory;
+
+public class KdcServerForTest {
+
+ private static final Logger log = LoggerFactory.getLogger(KdcServerForTest.class);
+
+ private static final int KDC_PORT = 88;
+ private static final int ADMIN_PORT = 749;
+ private static final String DOCKER_FILE = "containers/kdc-server/Dockerfile";
+ private static final String CLICKHOUSE_SNAME = "HTTP/clickhouse-server.example.com@EXAMPLE.COM";
+ private static final String BOB_KEYTAB_NAME = "bob.keytab";
+
+ private final GenericContainer> kdcContainer;
+ private final GenericContainer> clickhouseContainer;
+ private final File tmpDir;
+ private String bobJaasConfPath;
+ private String krb5ConfPath;
+
+ private static KdcServerForTest instance;
+
+ public static KdcServerForTest getInstance() {
+ if (instance == null) {
+ synchronized(KdcServerForTest.class) {
+ if (instance == null) {
+ instance = new KdcServerForTest(ClickHouseServerForTest.getClickHouseContainer());
+ }
+ }
+ }
+ return instance;
+ }
+
+ private KdcServerForTest(GenericContainer> clickhouseContainer) {
+ if (clickhouseContainer == null) {
+ throw new IllegalArgumentException("Clickhouse server container can not be null");
+ }
+ this.clickhouseContainer = clickhouseContainer;
+ this.kdcContainer = buildKdcContainer();
+ tmpDir = new File(System.getProperty("java.io.tmpdir"), "test-" + UUID.randomUUID());
+ }
+
+ private static GenericContainer> buildKdcContainer() {
+ String dockerFile = getClassLoader().getResource(DOCKER_FILE).getFile();
+ return new GenericContainer<>(
+ new ImageFromDockerfile().withDockerfile(new File(dockerFile).toPath()))
+ .withExposedPorts(KDC_PORT, ADMIN_PORT);
+ }
+
+ public void beforeSuite() {
+ if (!tmpDir.exists()) {
+ log.info("Creating tmp directory " + tmpDir.getAbsolutePath());
+ tmpDir.mkdir();
+ tmpDir.deleteOnExit();
+ }
+
+ if (kdcContainer != null) {
+ if (kdcContainer.isRunning()) {
+ return;
+ }
+
+ if (!clickhouseContainer.isRunning()) {
+ throw new IllegalStateException("ClickHouse server is not initialized");
+ }
+
+ try {
+ log.info("Starting KDC container...");
+ kdcContainer.start();
+ executeCmd("kadmin.local add_principal -pw bob bob@EXAMPLE.COM");
+ executeCmd("kadmin.local ktadd -k /etc/bob.keytab -norandkey bob@EXAMPLE.COM");
+ executeCmd("kadmin.local add_principal -randkey " + CLICKHOUSE_SNAME);
+ executeCmd("kadmin.local ktadd -k /etc/ch-service.keytab -norandkey " + CLICKHOUSE_SNAME);
+
+ File bobKeyTab = new File(tmpDir, BOB_KEYTAB_NAME);
+ kdcContainer.copyFileFromContainer("/etc/bob.keytab", bobKeyTab.getAbsolutePath());
+
+ File chServiceKeyTab = new File(tmpDir, "ch.keytab");
+ kdcContainer.copyFileFromContainer("/etc/ch-service.keytab", chServiceKeyTab.getAbsolutePath());
+ clickhouseContainer.copyFileToContainer(
+ MountableFile.forHostPath(chServiceKeyTab.getAbsolutePath(), 644), "/etc/krb5.keytab");
+
+ bobJaasConfPath = createBobJaasConf();
+ krb5ConfPath = createKrb5Conf();
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Can not initialize kdc server", e);
+ }
+ }
+ log.info("KDC container started");
+ }
+
+ public String getBobJaasConf() {
+ return bobJaasConfPath;
+ }
+
+ public String getKrb5Conf() {
+ return krb5ConfPath;
+ }
+
+ public void afterSuite() {
+ if (kdcContainer != null) {
+ kdcContainer.stop();
+ kdcContainer.close();
+ }
+ }
+
+ private void executeCmd(String cmd) throws UnsupportedOperationException, IOException, InterruptedException {
+ ExecResult result = kdcContainer.execInContainer(cmd.split(" "));
+ validate(result, cmd);
+ }
+
+ private void validate(ExecResult result, String cmd) {
+ if (result.getExitCode() != 0) {
+ String reason = result.getStdout();
+ if (StringUtils.isNoneBlank(result.getStderr())) {
+ reason = result.getStderr();
+ }
+ throw new RuntimeException(
+ "Command [" + cmd + "] failed with code " + result.getExitCode() + " and reason " + reason);
+ }
+ if (StringUtils.isNotBlank(result.getStdout())) {
+ log.info("[" + cmd + "]: " + result.getStdout());
+ }
+ }
+
+ private String createBobJaasConf() throws IOException {
+ Map params = new HashMap<>();
+ params.put("PRINCIPAL", "bob@EXAMPLE.COM");
+ params.put("KEYTAB", new File(tmpDir, BOB_KEYTAB_NAME).getAbsolutePath());
+ return prepareConfigFile("client_jaas.conf", "bob_jaas.conf", params);
+ }
+
+ private String createKrb5Conf() throws IOException {
+ Map params = new HashMap<>();
+ params.put("KDC_PORT", "" + kdcContainer.getMappedPort(KDC_PORT));
+ params.put("ADMIN_PORT", "" + kdcContainer.getMappedPort(ADMIN_PORT));
+ return prepareConfigFile("client_krb5.conf", "krb5.conf", params);
+ }
+
+ private String prepareConfigFile(String templateFileName, String outputFileName, Map params) throws IOException {
+ try (InputStream inputStream = KdcServerForTest.class.getClassLoader().getResourceAsStream(templateFileName)) {
+ String templateFile = new String(IOUtils.toByteArray(inputStream));
+ String content = templateFile;
+ for (Entry param : params.entrySet()) {
+ content = content.replaceAll(Pattern.quote("${" + param.getKey() + "}"), param.getValue());
+ }
+ File outputFile = new File(tmpDir, outputFileName);
+ try (FileWriter writer = new FileWriter(outputFile)) {
+ writer.write(content);
+ }
+ return outputFile.getAbsolutePath();
+ }
+ }
+
+ private static ClassLoader getClassLoader() {
+ return KdcServerForTest.class.getClassLoader();
+ }
+}
\ No newline at end of file
diff --git a/clickhouse-client/src/test/resources/client_jaas.conf b/clickhouse-client/src/test/resources/client_jaas.conf
new file mode 100644
index 000000000..71f2d6d39
--- /dev/null
+++ b/clickhouse-client/src/test/resources/client_jaas.conf
@@ -0,0 +1,10 @@
+com.sun.security.jgss.krb5.initiate {
+ com.sun.security.auth.module.Krb5LoginModule required
+ principal="${PRINCIPAL}"
+ debug=true
+ useKeyTab=true
+ keyTab="${KEYTAB}"
+ doNotPrompt=true
+ isInitiator=true
+ refreshKrb5Config=true
+;};
diff --git a/clickhouse-client/src/test/resources/client_krb5.conf b/clickhouse-client/src/test/resources/client_krb5.conf
new file mode 100644
index 000000000..c8922b67c
--- /dev/null
+++ b/clickhouse-client/src/test/resources/client_krb5.conf
@@ -0,0 +1,16 @@
+[libdefaults]
+ default_realm = EXAMPLE.COM
+ forwardable = true
+ allow_weak_crypto = true
+ udp_preference_limit = 1
+
+
+[realms]
+ EXAMPLE.COM = {
+ kdc = 127.0.0.1:${KDC_PORT}
+ admin_server = 127.0.0.1:${ADMIN_PORT}
+ }
+
+[domain_realm]
+ 127.0.0.1 = EXAMPLE.COM
+
\ No newline at end of file
diff --git a/clickhouse-client/src/test/resources/containers/clickhouse-server/config.d/custom_config.xml b/clickhouse-client/src/test/resources/containers/clickhouse-server/config.d/custom_config.xml
index d35eb09a6..514247ed3 100644
--- a/clickhouse-client/src/test/resources/containers/clickhouse-server/config.d/custom_config.xml
+++ b/clickhouse-client/src/test/resources/containers/clickhouse-server/config.d/custom_config.xml
@@ -14,7 +14,10 @@
false
/etc/clickhouse-server/certs/myCA.crt
-
+
+ EXAMPLE.COM
+ /etc/krb5.keytab
+
/etc/clickhouse-server/certs/localhost.crt
diff --git a/clickhouse-client/src/test/resources/containers/clickhouse-server/users.d/users.xml b/clickhouse-client/src/test/resources/containers/clickhouse-server/users.d/users.xml
index cf57c6907..e9a403594 100644
--- a/clickhouse-client/src/test/resources/containers/clickhouse-server/users.d/users.xml
+++ b/clickhouse-client/src/test/resources/containers/clickhouse-server/users.d/users.xml
@@ -67,5 +67,13 @@
me
+
+ default
+
+ ::/0
+
+ default
+
+
\ No newline at end of file
diff --git a/clickhouse-client/src/test/resources/containers/kdc-server/Dockerfile b/clickhouse-client/src/test/resources/containers/kdc-server/Dockerfile
new file mode 100644
index 000000000..f3cffde6d
--- /dev/null
+++ b/clickhouse-client/src/test/resources/containers/kdc-server/Dockerfile
@@ -0,0 +1,36 @@
+FROM minimal-ubuntu
+
+ENV DEBIAN_FRONTEND noninteractive
+
+WORKDIR /root/
+
+RUN apt update -y
+RUN apt install -y python3-dev python3-pip
+RUN apt install -y ntp krb5-admin-server krb5-kdc
+RUN pip install supervisor
+
+# remove default configuration
+RUN rm -r /var/lib/krb5kdc/
+RUN rm -r /etc/krb5kdc/
+
+# python web server configuration
+COPY ./index.html .
+
+# kerberos server configuration
+ENV KRB5_CONFIG=/etc/krb5.conf
+ENV KRB5_KDC_PROFILE=/var/kerberos/krb5kdc/kdc.conf
+RUN mkdir -pv /var/kerberos/krb5kdc
+COPY ./kdc.conf /var/kerberos/krb5kdc/kdc.conf
+COPY ./kadm5.acl /var/kerberos/krb5kdc/kadm5.acl
+COPY ./krb5.conf /etc/krb5.conf
+RUN mkdir -pv /var/log/kerberos/
+RUN touch /var/log/kerberos/krb5.log
+RUN touch /var/log/kerberos/kadmin.log
+RUN touch /var/log/kerberos/krb5lib.log
+RUN kdb5_util -r EXAMPLE.COM -P krb5 create -s
+
+RUN mkdir -p /var/log/supervisord/
+COPY ./supervisord.conf /etc/supervisord.conf
+
+# when container is starting
+CMD ["/usr/local/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
diff --git a/clickhouse-client/src/test/resources/containers/kdc-server/index.html b/clickhouse-client/src/test/resources/containers/kdc-server/index.html
new file mode 100644
index 000000000..f479d3029
--- /dev/null
+++ b/clickhouse-client/src/test/resources/containers/kdc-server/index.html
@@ -0,0 +1,10 @@
+krb5-kdc-server
+
+Running Kerberos service with:
+
+- Key distribution center (KDC)
+- Authentication Service (AS)
+
+
+
+Manual
diff --git a/clickhouse-client/src/test/resources/containers/kdc-server/kadm5.acl b/clickhouse-client/src/test/resources/containers/kdc-server/kadm5.acl
new file mode 100644
index 000000000..a3111969a
--- /dev/null
+++ b/clickhouse-client/src/test/resources/containers/kdc-server/kadm5.acl
@@ -0,0 +1,8 @@
+# /var/kerberos/krb5kdc/kadm5.acl -- Kerberos V5 general configuration.
+#
+# This file is the access control list for krb5 administration.
+# When this file is edited run /etc/init.d/krb5-admin-server restart to activate
+# One common way to set up Kerberos administration is to allow any principal
+# ending in /admin is given full administrative rights.
+# To enable this, uncomment the following line:
+*/admin@EXAMPLE.COM *
diff --git a/clickhouse-client/src/test/resources/containers/kdc-server/kdc.conf b/clickhouse-client/src/test/resources/containers/kdc-server/kdc.conf
new file mode 100644
index 000000000..f6758e21a
--- /dev/null
+++ b/clickhouse-client/src/test/resources/containers/kdc-server/kdc.conf
@@ -0,0 +1,29 @@
+# /var/kerberos/krb5kdc/kdc.conf -- Kerberos V5 general configuration.
+
+[kdcdefaults]
+ kdc_ports = 88
+ default_realm = EXAMPLE.COM
+
+[realms]
+EXAMPLE.COM = {
+ database_module = EXAMPLE.COM
+ acl_file = /var/kerberos/krb5kdc/kadm5.acl
+ key_stash_file = /var/kerberos/krb5kdc/.k5.EXAMPLE.COM
+ kdc_ports = 88
+ max_life = 10h 0m 0s
+ max_renewable_life = 7d 0h 0m 0s
+ master_key_type = des3-cbc-sha1
+ supported_enctypes = des3-cbc-sha1
+ default_principal_flags = +preauth
+}
+
+[dbmodules]
+EXAMPLE.COM = {
+ db_library = db2
+ database_name = /var/kerberos/krb5kdc/database
+}
+
+[logging]
+ default = FILE:/var/log/kerberos/krb5.log
+ admin_server = FILE:/var/log/kerberos/kadmin.log
+ kdc = FILE:/var/log/kerberos/krb5lib.log
diff --git a/clickhouse-client/src/test/resources/containers/kdc-server/krb5.conf b/clickhouse-client/src/test/resources/containers/kdc-server/krb5.conf
new file mode 100644
index 000000000..d0dbdc5fd
--- /dev/null
+++ b/clickhouse-client/src/test/resources/containers/kdc-server/krb5.conf
@@ -0,0 +1,28 @@
+[libdefaults]
+ default_realm = EXAMPLE.COM
+ kdc_timesync = 1
+ clockskew = 300
+ ccache_type = 4
+ forwardable = true
+ proxiable = true
+ permitted_enctypes = des3-cbc-sha1
+ default_tkt_enctypes = des3-cbc-sha1
+ default_tgs_enctypes = des3-cbc-sha1
+ dns_lookup_kdc = false
+ dns_lookup_realm = false
+ ticket_lifetime = 8h 0m 0s
+ renew_lifetime = 1d 0h 0m 0s
+ allow_weak_crypto = true
+
+
+[realms]
+ EXAMPLE.COM = {
+ kdc = krb5-kdc-server.example.com
+ admin_server = krb5-kdc-server.example.com
+ default_domain = EXAMPLE.COM
+ }
+
+[domain_realm]
+ .example.com = EXAMPLE.COM
+ example.com = EXAMPLE.COM
+
\ No newline at end of file
diff --git a/clickhouse-client/src/test/resources/containers/kdc-server/supervisord.conf b/clickhouse-client/src/test/resources/containers/kdc-server/supervisord.conf
new file mode 100644
index 000000000..d6e348f51
--- /dev/null
+++ b/clickhouse-client/src/test/resources/containers/kdc-server/supervisord.conf
@@ -0,0 +1,41 @@
+; Generated from Jinja template
+; DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
+
+; supervisord.conf - kdc-server
+[unix_http_server]
+file=/tmp/supervisor.sock ; path to your socket file
+
+[supervisord]
+logfile=/var/log/supervisord/supervisord.log ; supervisord log file
+logfile_maxbytes=50MB ; maximum size of logfile before rotation
+logfile_backups=10 ; number of backed up logfiles
+loglevel=error ; info, debug, warn, trace
+pidfile=/var/run/supervisord.pid ; pidfile location
+nodaemon=false ; run supervisord as a daemon
+minfds=1024 ; number of startup file descriptors
+minprocs=200 ; number of process descriptors
+user=root ; default user
+childlogdir=/var/log/supervisord/ ; where child log files will live
+
+[rpcinterface:supervisor]
+supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
+
+[supervisorctl]
+serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
+
+[program:httpserver]
+command=python3 -m http.server 8080
+autostart=true
+autorestart=true
+
+[program:krb5-kdc]
+; https://web.mit.edu/kerberos/krb5-latest/doc/admin/admin_commands/krb5kdc.html
+command=/usr/sbin/krb5kdc -n
+autostart=true
+autorestart=true
+
+[program:krb5-admin-server]
+; https://web.mit.edu/kerberos/krb5-latest/doc/admin/admin_commands/kadmind.html
+command=/usr/sbin/kadmind -nofork
+autostart=true
+autorestart=true
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java
index eb9b32751..3a13e1c52 100644
--- a/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java
@@ -19,7 +19,8 @@
public class GssAuthorizationContext implements Serializable {
- private static final Logger LOG = LoggerFactory.getLogger(GssAuthorizationContext.class);
+ private static final String INTEGRATION_TEST_SNAME_PROP_KEY = "clickhouse.test.kerb.sname";
+ private static final Logger log = LoggerFactory.getLogger(GssAuthorizationContext.class);
private final Subject subject;
@@ -34,7 +35,7 @@ public static GssAuthorizationContext initialize() {
public String getAuthToken(String user, String serverName, String host) throws GSSException {
GSSCredential gssCredential = null;
if (subject != null) {
- LOG.debug("Getting private credentials from subject");
+ log.debug("Getting private credentials from subject");
Set gssCreds = subject.getPrivateCredentials(GSSCredential.class);
if (gssCreds != null && !gssCreds.isEmpty()) {
gssCredential = gssCreds.iterator().next();
@@ -52,17 +53,32 @@ public String getAuthToken(String user, String serverName, String host) throws G
if (!ClickHouseDefaults.USER.getDefaultValue().equals(user)) {
gssClientName = manager.createName(user, GSSName.NT_USER_NAME);
} else {
- LOG.debug("GSS credential name ignored. User name is default");
+ log.debug("GSS credential name ignored. User name is default");
}
gssCredential = manager.createCredential(gssClientName, 8 * 3600, desiredMech, GSSCredential.INITIATE_ONLY);
}
- GSSName gssServerName = manager.createName(serverName + "@" + host, GSSName.NT_HOSTBASED_SERVICE);
+ GSSName gssServerName = manager.createName(getSName(serverName, host), GSSName.NT_HOSTBASED_SERVICE);
GSSContext secContext = manager.createContext(gssServerName, desiredMech, gssCredential,
GSSContext.DEFAULT_LIFETIME);
secContext.requestMutualAuth(true);
return Base64.encodeBase64String(secContext.initSecContext(new byte[0], 0, 0));
}
+ private String getSName(String serverName, String host) {
+ if (System.getProperty(INTEGRATION_TEST_SNAME_PROP_KEY) != null && isLocalhost(host)) {
+ // integration test mode - it allows to integrate with servers
+ // without editing /etc/hosts
+ String serverNameIT = System.getProperty(INTEGRATION_TEST_SNAME_PROP_KEY);
+ log.warn("Integration test mode. Using sname %s", serverNameIT);
+ return serverNameIT;
+ }
+ return serverName + "@" + host;
+ }
+
+ private boolean isLocalhost(String host) {
+ return "localhost".equals(host) || "127.0.0.1".equals(host);
+ }
+
private static Oid getSpnegoMech() throws GSSException {
return new Oid("1.3.6.1.5.5.2");
}
diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/SystemPropertiesMock.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/SystemPropertiesMock.java
new file mode 100644
index 000000000..0e561d426
--- /dev/null
+++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/SystemPropertiesMock.java
@@ -0,0 +1,50 @@
+package com.clickhouse.jdbc;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+@NotThreadSafe
+public class SystemPropertiesMock implements AutoCloseable {
+
+ private Map previousProperties = new HashMap<>();
+
+ private SystemPropertiesMock(String...keyValues) {
+ if (keyValues == null || keyValues.length % 2 != 0) {
+ throw new IllegalArgumentException("Improper key-value pairs");
+ }
+ Map mockProperties = new HashMap<>();
+
+ for (int i=0; i properties) {
+ for (String key : properties.keySet()) {
+ String value = properties.get(key);
+ if (value == null) {
+ System.clearProperty(key);
+ } else {
+ System.setProperty(key, value);
+ }
+ }
+ }
+
+}
diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
new file mode 100644
index 000000000..edd3bb031
--- /dev/null
+++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
@@ -0,0 +1,58 @@
+package com.clickhouse.jdbc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.Properties;
+
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import com.clickhouse.client.ClickHouseProtocol;
+import com.clickhouse.client.KdcServerForTest;
+
+public class ClickHouserDriverWIthGssAuthTest extends JdbcIntegrationTest {
+
+ private static final KdcServerForTest kdcServer = KdcServerForTest.getInstance();
+
+ @Test(groups = "integration")
+ public void testConnect() throws Exception {
+ String address = getServerAddress(ClickHouseProtocol.HTTP, true);
+ Properties props = new Properties();
+ props.setProperty("user", "bob"); // user with kerb auth
+ props.setProperty("gss_enabled", "true");
+
+ try (SystemPropertiesMock mock = SystemPropertiesMock.of(
+ "java.security.krb5.conf", kdcServer.getKrb5Conf(),
+ "java.security.auth.login.config", kdcServer.getBobJaasConf(),
+ "javax.security.auth.useSubjectCredsOnly", "false",
+ "sun.security.krb5.debug", "true",
+ "clickhouse.test.kerb.sname", "HTTP@clickhouse-server.example.com")) {
+ ClickHouseDriver driver = new ClickHouseDriver();
+ try (ClickHouseConnection conn = driver.connect("jdbc:clickhouse://" + address, props);
+ PreparedStatement ps = conn.prepareStatement("SELECT currentUser()")) {
+
+ ResultSet rs = ps.executeQuery();
+
+ assertTrue(rs.next());
+ assertEquals("bob", rs.getString(1));
+ } catch (Exception e) {
+ throw e;
+ }
+ }
+ }
+
+ @BeforeTest
+ public static void setupKdc() {
+ kdcServer.beforeSuite();
+ }
+
+ @AfterTest
+ public static void shutdownKdc() {
+ kdcServer.afterSuite();
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index fb733cf96..2a9c39e21 100644
--- a/pom.xml
+++ b/pom.xml
@@ -115,7 +115,7 @@
1.18.3
7.5.1
- 5.9.0
+ 4.11.0
3.1.4
8.1.0
From ff8be987ddf8f7df5604fb36cac2507b7ac86851 Mon Sep 17 00:00:00 2001
From: Maciej Maciejko
Date: Wed, 7 Feb 2024 15:08:43 +0100
Subject: [PATCH 03/10] GssCredential initialization from subject fix
---
.../clickhouse/client/KdcServerForTest.java | 11 ++-
.../client/gss/GssAuthorizationContext.java | 64 ++++++++++++------
.../client/http/ClickHouseHttpClient.java | 4 +-
.../client/http/ClickHouseHttpConnection.java | 8 +--
.../http/ClickHouseHttpConnectionTest.java | 10 +--
.../ClickHouserDriverWIthGssAuthTest.java | 67 +++++++++++++++----
6 files changed, 117 insertions(+), 47 deletions(-)
diff --git a/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java b/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
index 5af692437..79272ba51 100644
--- a/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
+++ b/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
@@ -35,6 +35,7 @@ public class KdcServerForTest {
private final File tmpDir;
private String bobJaasConfPath;
private String krb5ConfPath;
+ private String bobKeyTabPath;
private static KdcServerForTest instance;
@@ -89,8 +90,8 @@ public void beforeSuite() {
executeCmd("kadmin.local add_principal -randkey " + CLICKHOUSE_SNAME);
executeCmd("kadmin.local ktadd -k /etc/ch-service.keytab -norandkey " + CLICKHOUSE_SNAME);
- File bobKeyTab = new File(tmpDir, BOB_KEYTAB_NAME);
- kdcContainer.copyFileFromContainer("/etc/bob.keytab", bobKeyTab.getAbsolutePath());
+ bobKeyTabPath = new File(tmpDir, BOB_KEYTAB_NAME).getAbsolutePath();
+ kdcContainer.copyFileFromContainer("/etc/bob.keytab", bobKeyTabPath);
File chServiceKeyTab = new File(tmpDir, "ch.keytab");
kdcContainer.copyFileFromContainer("/etc/ch-service.keytab", chServiceKeyTab.getAbsolutePath());
@@ -110,6 +111,10 @@ public String getBobJaasConf() {
return bobJaasConfPath;
}
+ public String getBobKeyTabPath() {
+ return bobKeyTabPath;
+ }
+
public String getKrb5Conf() {
return krb5ConfPath;
}
@@ -143,7 +148,7 @@ private void validate(ExecResult result, String cmd) {
private String createBobJaasConf() throws IOException {
Map params = new HashMap<>();
params.put("PRINCIPAL", "bob@EXAMPLE.COM");
- params.put("KEYTAB", new File(tmpDir, BOB_KEYTAB_NAME).getAbsolutePath());
+ params.put("KEYTAB", getBobKeyTabPath());
return prepareConfigFile("client_jaas.conf", "bob_jaas.conf", params);
}
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java
index 3a13e1c52..0420eccaa 100644
--- a/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/gss/GssAuthorizationContext.java
@@ -1,6 +1,5 @@
package com.clickhouse.client.gss;
-import java.io.Serializable;
import java.util.Set;
import javax.security.auth.Subject;
@@ -17,22 +16,27 @@
import com.clickhouse.logging.Logger;
import com.clickhouse.logging.LoggerFactory;
-public class GssAuthorizationContext implements Serializable {
+public class GssAuthorizationContext {
private static final String INTEGRATION_TEST_SNAME_PROP_KEY = "clickhouse.test.kerb.sname";
private static final Logger log = LoggerFactory.getLogger(GssAuthorizationContext.class);
- private final Subject subject;
+ private final String user;
+ private final String serverName;
+ private final String host;
- private GssAuthorizationContext(Subject subject) {
- this.subject = subject;
- }
+ private GSSCredential gssCredential;
+ private Oid desiredMech;
- public static GssAuthorizationContext initialize() {
- return new GssAuthorizationContext(SubjectProvider.getSubject());
+ private GssAuthorizationContext(GSSCredential gssCredential, String user, String serverName, String host) {
+ this.gssCredential = gssCredential;
+ this.user = user;
+ this.serverName = serverName;
+ this.host = host;
}
- public String getAuthToken(String user, String serverName, String host) throws GSSException {
+ public static GssAuthorizationContext initialize(String user, String serverName, String host) {
+ Subject subject = SubjectProvider.getSubject();
GSSCredential gssCredential = null;
if (subject != null) {
log.debug("Getting private credentials from subject");
@@ -41,30 +45,48 @@ public String getAuthToken(String user, String serverName, String host) throws G
gssCredential = gssCreds.iterator().next();
}
}
+ return new GssAuthorizationContext(gssCredential, user, serverName, host);
+ }
+ public String getAuthToken() throws GSSException {
GSSManager manager = GSSManager.getInstance();
- Oid desiredMech = getKerberosMech();
- if (gssCredential == null) {
- if (hasSpnegoSupport(manager)) {
- desiredMech = getSpnegoMech();
- }
+ GSSName gssServerName = manager.createName(getSName(), GSSName.NT_HOSTBASED_SERVICE);
+ GSSContext secContext = manager.createContext(gssServerName, getDesiredMech(manager), getCredentials(manager),
+ GSSContext.DEFAULT_LIFETIME);
+ secContext.requestMutualAuth(true);
+ return Base64.encodeBase64String(secContext.initSecContext(new byte[0], 0, 0));
+ }
+
+ public String getUserName() {
+ return user;
+ }
+ private GSSCredential getCredentials(GSSManager manager) throws GSSException {
+ if (gssCredential == null || gssCredential.getRemainingLifetime() <= 0) {
+ log.debug("Creating credentials");
GSSName gssClientName = null;
if (!ClickHouseDefaults.USER.getDefaultValue().equals(user)) {
gssClientName = manager.createName(user, GSSName.NT_USER_NAME);
} else {
log.debug("GSS credential name ignored. User name is default");
}
- gssCredential = manager.createCredential(gssClientName, 8 * 3600, desiredMech, GSSCredential.INITIATE_ONLY);
+ gssCredential = manager.createCredential(gssClientName, 8 * 3600, getDesiredMech(manager), GSSCredential.INITIATE_ONLY);
}
- GSSName gssServerName = manager.createName(getSName(serverName, host), GSSName.NT_HOSTBASED_SERVICE);
- GSSContext secContext = manager.createContext(gssServerName, desiredMech, gssCredential,
- GSSContext.DEFAULT_LIFETIME);
- secContext.requestMutualAuth(true);
- return Base64.encodeBase64String(secContext.initSecContext(new byte[0], 0, 0));
+ return gssCredential;
+ }
+
+ private Oid getDesiredMech(GSSManager manager) throws GSSException {
+ if (desiredMech == null) {
+ if (hasSpnegoSupport(manager)) {
+ desiredMech = getSpnegoMech();
+ } else {
+ desiredMech = getKerberosMech();
+ }
+ }
+ return desiredMech;
}
- private String getSName(String serverName, String host) {
+ private String getSName() {
if (System.getProperty(INTEGRATION_TEST_SNAME_PROP_KEY) != null && isLocalhost(host)) {
// integration test mode - it allows to integrate with servers
// without editing /etc/hosts
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpClient.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpClient.java
index 7cdf655d1..0ec1013b5 100644
--- a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpClient.java
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpClient.java
@@ -57,7 +57,9 @@ protected ClickHouseHttpConnection newConnection(ClickHouseHttpConnection connec
if (connection != null) {
gssAuthContext = connection.getGssAuthorizationContext();
} else {
- gssAuthContext = GssAuthorizationContext.initialize();
+ ClickHouseConfig config = request.getConfig();
+ String userName = server.getCredentials(config).getUserName();
+ gssAuthContext = GssAuthorizationContext.initialize(userName, config.getKerberosServerName(), server.getHost());
}
try {
return ClickHouseHttpConnectionFactory.createConnection(server, request, getExecutor(), gssAuthContext);
diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnection.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnection.java
index 73e590202..0bfd5b46e 100644
--- a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnection.java
+++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ClickHouseHttpConnection.java
@@ -466,13 +466,11 @@ private Map setGssAuthHeader(Map headers, ClickH
if (gssAuthContext == null) {
throw new IllegalStateException("GssAuthorizer not initialized");
}
- String userName = credentials.getUserName();
try {
- headers.put(HEADER_AUTHORIZATION, "Negotiate "
- + gssAuthContext.getAuthToken(userName, config.getKerberosServerName(), server.getHost()));
+ headers.put(HEADER_AUTHORIZATION, "Negotiate " + gssAuthContext.getAuthToken());
} catch (GSSException e) {
- throw new RuntimeException("Can not generate GSS token for user " + userName + " host "
- + server.getHost() + " with name " + config.getKerberosServerName(), e);
+ throw new RuntimeException("Can not generate GSS token for host "
+ + server.getHost() + " with name " + config.getKerberosServerName(), e);
}
}
return headers;
diff --git a/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ClickHouseHttpConnectionTest.java b/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ClickHouseHttpConnectionTest.java
index 8938581f7..5d351b4ff 100644
--- a/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ClickHouseHttpConnectionTest.java
+++ b/clickhouse-http-client/src/test/java/com/clickhouse/client/http/ClickHouseHttpConnectionTest.java
@@ -78,13 +78,13 @@ public void testDefaultHeaders() {
@Test(groups = { "unit" })
public void testDefaultHeadersWithGssAuth() throws GSSException {
ClickHouseNode server = ClickHouseNode.builder()
- .credentials(ClickHouseCredentials.withGss("userA"))
+ .credentials(ClickHouseCredentials.withGss("user"))
.addOption(ClickHouseClientOption.KERBEROS_SERVER_NAME.getKey(), "kerbServerName")
.build();
ClickHouseRequest> request = ClickHouseClient.newInstance().read(server);
GssAuthorizationContext gssAuthMode = mock(GssAuthorizationContext.class);
- when(gssAuthMode.getAuthToken("userA", "kerbServerName", server.getHost())).thenReturn("AUTH_TOKEN_ABC");
+ when(gssAuthMode.getAuthToken()).thenReturn("AUTH_TOKEN_ABC");
SimpleHttpConnection sc = new SimpleHttpConnection(server, request, gssAuthMode);
Assert.assertFalse(sc.defaultHeaders.containsKey("authorization"));
@@ -96,13 +96,13 @@ public void testDefaultHeadersWithGssAuth() throws GSSException {
@Test(groups = { "unit" })
public void testCustomHeadersWithGssAuth() throws GSSException {
ClickHouseNode server = ClickHouseNode.builder()
- .credentials(ClickHouseCredentials.withGss("userB"))
- .addOption(ClickHouseClientOption.KERBEROS_SERVER_NAME.getKey(), "kerbServerNameB")
+ .credentials(ClickHouseCredentials.withGss("user"))
+ .addOption(ClickHouseClientOption.KERBEROS_SERVER_NAME.getKey(), "kerbServerName")
.build();
ClickHouseRequest> request = ClickHouseClient.newInstance().read(server);
GssAuthorizationContext gssAuthMode = mock(GssAuthorizationContext.class);
- when(gssAuthMode.getAuthToken("userB", "kerbServerNameB", server.getHost())).thenReturn("AUTH_TOKEN_ABCD");
+ when(gssAuthMode.getAuthToken()).thenReturn("AUTH_TOKEN_ABCD");
SimpleHttpConnection sc = new SimpleHttpConnection(server, request, gssAuthMode);
Assert.assertFalse(sc.defaultHeaders.containsKey("authorization"));
diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
index edd3bb031..eaaf2b902 100644
--- a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
+++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
@@ -3,16 +3,23 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import java.security.PrivilegedExceptionAction;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Properties;
+import javax.security.auth.Subject;
+import javax.security.auth.spi.LoginModule;
+
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import com.clickhouse.client.ClickHouseProtocol;
import com.clickhouse.client.KdcServerForTest;
+import com.sun.security.auth.module.Krb5LoginModule;
public class ClickHouserDriverWIthGssAuthTest extends JdbcIntegrationTest {
@@ -21,28 +28,64 @@ public class ClickHouserDriverWIthGssAuthTest extends JdbcIntegrationTest {
@Test(groups = "integration")
public void testConnect() throws Exception {
String address = getServerAddress(ClickHouseProtocol.HTTP, true);
+ ClickHouseDriver driver = new ClickHouseDriver();
Properties props = new Properties();
- props.setProperty("user", "bob"); // user with kerb auth
+ props.setProperty("user", "bob"); // user with kerb auth
props.setProperty("gss_enabled", "true");
try (SystemPropertiesMock mock = SystemPropertiesMock.of(
- "java.security.krb5.conf", kdcServer.getKrb5Conf(),
- "java.security.auth.login.config", kdcServer.getBobJaasConf(),
- "javax.security.auth.useSubjectCredsOnly", "false",
- "sun.security.krb5.debug", "true",
- "clickhouse.test.kerb.sname", "HTTP@clickhouse-server.example.com")) {
- ClickHouseDriver driver = new ClickHouseDriver();
- try (ClickHouseConnection conn = driver.connect("jdbc:clickhouse://" + address, props);
+ "java.security.krb5.conf", kdcServer.getKrb5Conf(),
+ "java.security.auth.login.config", kdcServer.getBobJaasConf(),
+ "javax.security.auth.useSubjectCredsOnly", "false",
+ "sun.security.krb5.debug", "true",
+ "clickhouse.test.kerb.sname", "HTTP@clickhouse-server.example.com");
+ ClickHouseConnection conn = driver.connect("jdbc:clickhouse://" + address, props);
PreparedStatement ps = conn.prepareStatement("SELECT currentUser()")) {
-
- ResultSet rs = ps.executeQuery();
+ ResultSet rs = ps.executeQuery();
+ assertTrue(rs.next());
+
+ assertEquals("bob", rs.getString(1));
+ }
+ }
+
+ @Test(groups = "integration")
+ public void testConnectWithUseSubjectCredOnly() throws Exception {
+ String address = getServerAddress(ClickHouseProtocol.HTTP, true);
+ ClickHouseDriver driver = new ClickHouseDriver();
+ Properties props = new Properties();
+ props.setProperty("gss_enabled", "true");
+
+ Subject subject = new Subject();
+ LoginModule krb5Module = new Krb5LoginModule();
+
+ Map options = new HashMap<>();
+ options.put("principal", "bob@EXAMPLE.COM");
+ options.put("useKeyTab", "true");
+ options.put("keyTab", kdcServer.getBobKeyTabPath());
+ options.put("doNotPrompt", "true");
+ options.put("isInitiator", "true");
+ options.put("refreshKrb5Config", "true");
+ options.put("debug", "true");
+ krb5Module.initialize(subject, null, null, options);
+ try (SystemPropertiesMock mock = SystemPropertiesMock.of(
+ "java.security.krb5.conf", kdcServer.getKrb5Conf(),
+ "sun.security.krb5.debug", "true",
+ "javax.security.auth.useSubjectCredsOnly", "true",
+ "clickhouse.test.kerb.sname", "HTTP@clickhouse-server.example.com")) {
+ krb5Module.login();
+ krb5Module.commit();
+ try (ClickHouseConnection conn = Subject.doAs(subject,
+ (PrivilegedExceptionAction) () -> driver
+ .connect("jdbc:clickhouse://" + address + "?user=bob", props));
+ PreparedStatement ps = conn.prepareStatement("SELECT currentUser()")) {
+ ResultSet rs = ps.executeQuery();
assertTrue(rs.next());
+
assertEquals("bob", rs.getString(1));
- } catch (Exception e) {
- throw e;
}
}
+
}
@BeforeTest
From 85a8f60f64827bbfcbe074d6375ecfbd0974bd73 Mon Sep 17 00:00:00 2001
From: Maciej Maciejko
Date: Wed, 7 Feb 2024 18:12:12 +0100
Subject: [PATCH 04/10] integration test fix
---
.../clickhouse/client/KdcServerForTest.java | 26 ++++++++++++-------
.../ClickHouserDriverWIthGssAuthTest.java | 5 ++--
2 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java b/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
index 79272ba51..4c5531182 100644
--- a/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
+++ b/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
@@ -4,7 +4,9 @@
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
@@ -26,13 +28,12 @@ public class KdcServerForTest {
private static final int KDC_PORT = 88;
private static final int ADMIN_PORT = 749;
- private static final String DOCKER_FILE = "containers/kdc-server/Dockerfile";
private static final String CLICKHOUSE_SNAME = "HTTP/clickhouse-server.example.com@EXAMPLE.COM";
private static final String BOB_KEYTAB_NAME = "bob.keytab";
private final GenericContainer> kdcContainer;
private final GenericContainer> clickhouseContainer;
- private final File tmpDir;
+ public final File tmpDir;
private String bobJaasConfPath;
private String krb5ConfPath;
private String bobKeyTabPath;
@@ -60,10 +61,14 @@ private KdcServerForTest(GenericContainer> clickhouseContainer) {
}
private static GenericContainer> buildKdcContainer() {
- String dockerFile = getClassLoader().getResource(DOCKER_FILE).getFile();
- return new GenericContainer<>(
- new ImageFromDockerfile().withDockerfile(new File(dockerFile).toPath()))
- .withExposedPorts(KDC_PORT, ADMIN_PORT);
+ String dockerDir = "containers/kdc-server/";
+ List dockerFiles = Arrays.asList("Dockerfile", "index.html", "kadm5.acl", "kdc.conf", "krb5.conf", "supervisord.conf");
+
+ ImageFromDockerfile dockerImage = new ImageFromDockerfile();
+ for (String file : dockerFiles) {
+ dockerImage = dockerImage.withFileFromClasspath(file, dockerDir + file);
+ }
+ return new GenericContainer<>(dockerImage).withExposedPorts(KDC_PORT, ADMIN_PORT);
}
public void beforeSuite() {
@@ -93,6 +98,11 @@ public void beforeSuite() {
bobKeyTabPath = new File(tmpDir, BOB_KEYTAB_NAME).getAbsolutePath();
kdcContainer.copyFileFromContainer("/etc/bob.keytab", bobKeyTabPath);
+ if (!new File(bobKeyTabPath).exists()) {
+ throw new IllegalStateException("Bob keytab not created at " + bobJaasConfPath);
+ }
+ log.info("BOB KEYTAB FILE " + bobKeyTabPath);
+
File chServiceKeyTab = new File(tmpDir, "ch.keytab");
kdcContainer.copyFileFromContainer("/etc/ch-service.keytab", chServiceKeyTab.getAbsolutePath());
clickhouseContainer.copyFileToContainer(
@@ -173,8 +183,4 @@ private String prepareConfigFile(String templateFileName, String outputFileName,
return outputFile.getAbsolutePath();
}
}
-
- private static ClassLoader getClassLoader() {
- return KdcServerForTest.class.getClassLoader();
- }
}
\ No newline at end of file
diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
index eaaf2b902..40e93a512 100644
--- a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
+++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
@@ -67,6 +67,7 @@ public void testConnectWithUseSubjectCredOnly() throws Exception {
options.put("refreshKrb5Config", "true");
options.put("debug", "true");
krb5Module.initialize(subject, null, null, options);
+
try (SystemPropertiesMock mock = SystemPropertiesMock.of(
"java.security.krb5.conf", kdcServer.getKrb5Conf(),
"sun.security.krb5.debug", "true",
@@ -88,12 +89,12 @@ public void testConnectWithUseSubjectCredOnly() throws Exception {
}
- @BeforeTest
+ @BeforeTest(groups = "integration")
public static void setupKdc() {
kdcServer.beforeSuite();
}
- @AfterTest
+ @AfterTest(groups = "integration")
public static void shutdownKdc() {
kdcServer.afterSuite();
}
From 002ed396e3b1bf92cc8e348c2118054cb1a388f9 Mon Sep 17 00:00:00 2001
From: Maciej Maciejko
Date: Wed, 7 Feb 2024 18:14:17 +0100
Subject: [PATCH 05/10] refactor
---
.../test/java/com/clickhouse/client/KdcServerForTest.java | 5 -----
1 file changed, 5 deletions(-)
diff --git a/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java b/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
index 4c5531182..28c871f6f 100644
--- a/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
+++ b/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
@@ -98,11 +98,6 @@ public void beforeSuite() {
bobKeyTabPath = new File(tmpDir, BOB_KEYTAB_NAME).getAbsolutePath();
kdcContainer.copyFileFromContainer("/etc/bob.keytab", bobKeyTabPath);
- if (!new File(bobKeyTabPath).exists()) {
- throw new IllegalStateException("Bob keytab not created at " + bobJaasConfPath);
- }
- log.info("BOB KEYTAB FILE " + bobKeyTabPath);
-
File chServiceKeyTab = new File(tmpDir, "ch.keytab");
kdcContainer.copyFileFromContainer("/etc/ch-service.keytab", chServiceKeyTab.getAbsolutePath());
clickhouseContainer.copyFileToContainer(
From 27297e38a84a86fa438c04d231440b972e396c62 Mon Sep 17 00:00:00 2001
From: Maciej Maciejko
Date: Wed, 7 Feb 2024 18:56:19 +0100
Subject: [PATCH 06/10] refactor
---
.../clickhouse/client/KdcServerForTest.java | 19 ++++---------------
.../src/test/resources/client_krb5.conf | 1 -
.../clickhouse-server/users.d/users.xml | 2 +-
.../resources/containers/kdc-server/krb5.conf | 1 -
...a => ClickHouseDriverWithGssAuthTest.java} | 7 ++++---
5 files changed, 9 insertions(+), 21 deletions(-)
rename clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/{ClickHouserDriverWIthGssAuthTest.java => ClickHouseDriverWithGssAuthTest.java} (93%)
diff --git a/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java b/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
index 28c871f6f..38dcb9c7b 100644
--- a/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
+++ b/clickhouse-client/src/test/java/com/clickhouse/client/KdcServerForTest.java
@@ -38,26 +38,14 @@ public class KdcServerForTest {
private String krb5ConfPath;
private String bobKeyTabPath;
- private static KdcServerForTest instance;
-
- public static KdcServerForTest getInstance() {
- if (instance == null) {
- synchronized(KdcServerForTest.class) {
- if (instance == null) {
- instance = new KdcServerForTest(ClickHouseServerForTest.getClickHouseContainer());
- }
- }
- }
- return instance;
- }
-
- private KdcServerForTest(GenericContainer> clickhouseContainer) {
+ public KdcServerForTest(GenericContainer> clickhouseContainer) {
if (clickhouseContainer == null) {
throw new IllegalArgumentException("Clickhouse server container can not be null");
}
this.clickhouseContainer = clickhouseContainer;
this.kdcContainer = buildKdcContainer();
tmpDir = new File(System.getProperty("java.io.tmpdir"), "test-" + UUID.randomUUID());
+ tmpDir.deleteOnExit();
}
private static GenericContainer> buildKdcContainer() {
@@ -129,6 +117,7 @@ public void afterSuite() {
kdcContainer.stop();
kdcContainer.close();
}
+ tmpDir.delete();
}
private void executeCmd(String cmd) throws UnsupportedOperationException, IOException, InterruptedException {
@@ -178,4 +167,4 @@ private String prepareConfigFile(String templateFileName, String outputFileName,
return outputFile.getAbsolutePath();
}
}
-}
\ No newline at end of file
+}
diff --git a/clickhouse-client/src/test/resources/client_krb5.conf b/clickhouse-client/src/test/resources/client_krb5.conf
index c8922b67c..f08a4fb7d 100644
--- a/clickhouse-client/src/test/resources/client_krb5.conf
+++ b/clickhouse-client/src/test/resources/client_krb5.conf
@@ -13,4 +13,3 @@
[domain_realm]
127.0.0.1 = EXAMPLE.COM
-
\ No newline at end of file
diff --git a/clickhouse-client/src/test/resources/containers/clickhouse-server/users.d/users.xml b/clickhouse-client/src/test/resources/containers/clickhouse-server/users.d/users.xml
index e9a403594..39b7da6e7 100644
--- a/clickhouse-client/src/test/resources/containers/clickhouse-server/users.d/users.xml
+++ b/clickhouse-client/src/test/resources/containers/clickhouse-server/users.d/users.xml
@@ -76,4 +76,4 @@
-
\ No newline at end of file
+
diff --git a/clickhouse-client/src/test/resources/containers/kdc-server/krb5.conf b/clickhouse-client/src/test/resources/containers/kdc-server/krb5.conf
index d0dbdc5fd..5333af96b 100644
--- a/clickhouse-client/src/test/resources/containers/kdc-server/krb5.conf
+++ b/clickhouse-client/src/test/resources/containers/kdc-server/krb5.conf
@@ -25,4 +25,3 @@
[domain_realm]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM
-
\ No newline at end of file
diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseDriverWithGssAuthTest.java
similarity index 93%
rename from clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
rename to clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseDriverWithGssAuthTest.java
index 40e93a512..9812fb4f6 100644
--- a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouserDriverWIthGssAuthTest.java
+++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseDriverWithGssAuthTest.java
@@ -18,12 +18,13 @@
import org.testng.annotations.Test;
import com.clickhouse.client.ClickHouseProtocol;
+import com.clickhouse.client.ClickHouseServerForTest;
import com.clickhouse.client.KdcServerForTest;
import com.sun.security.auth.module.Krb5LoginModule;
-public class ClickHouserDriverWIthGssAuthTest extends JdbcIntegrationTest {
+public class ClickHouseDriverWithGssAuthTest extends JdbcIntegrationTest {
- private static final KdcServerForTest kdcServer = KdcServerForTest.getInstance();
+ private static final KdcServerForTest kdcServer = new KdcServerForTest(ClickHouseServerForTest.getClickHouseContainer());
@Test(groups = "integration")
public void testConnect() throws Exception {
@@ -67,7 +68,7 @@ public void testConnectWithUseSubjectCredOnly() throws Exception {
options.put("refreshKrb5Config", "true");
options.put("debug", "true");
krb5Module.initialize(subject, null, null, options);
-
+
try (SystemPropertiesMock mock = SystemPropertiesMock.of(
"java.security.krb5.conf", kdcServer.getKrb5Conf(),
"sun.security.krb5.debug", "true",
From 67e5ab919fb7a6f74b4f0ba1b3f40de3061ef919 Mon Sep 17 00:00:00 2001
From: Maciej Maciejko
Date: Sun, 11 Feb 2024 14:09:34 +0100
Subject: [PATCH 07/10] docker file fix
---
.../src/test/resources/containers/kdc-server/Dockerfile | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/clickhouse-client/src/test/resources/containers/kdc-server/Dockerfile b/clickhouse-client/src/test/resources/containers/kdc-server/Dockerfile
index f3cffde6d..0854c5575 100644
--- a/clickhouse-client/src/test/resources/containers/kdc-server/Dockerfile
+++ b/clickhouse-client/src/test/resources/containers/kdc-server/Dockerfile
@@ -1,11 +1,10 @@
-FROM minimal-ubuntu
+FROM python:3.9.18-slim-bullseye
ENV DEBIAN_FRONTEND noninteractive
WORKDIR /root/
RUN apt update -y
-RUN apt install -y python3-dev python3-pip
RUN apt install -y ntp krb5-admin-server krb5-kdc
RUN pip install supervisor
From 08fa9e02d410ee960c39ff6ba0ba541b727f9802 Mon Sep 17 00:00:00 2001
From: Maciej Maciejko
Date: Sun, 11 Feb 2024 14:39:25 +0100
Subject: [PATCH 08/10] sample fix
---
.../main/java/com/clickhouse/examples/jdbc/GssAuthClient.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java b/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
index 43a0c8f76..9316a12a5 100644
--- a/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
+++ b/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
@@ -7,7 +7,7 @@
*/
public class GssAuthClient {
- private void execute(String url) {
+ private void execute() {
String url = "jdbc:ch:http://localhost:8123/default"; // only http protocol supports GSS auth
Properties props = new Properties();
props.setProperty("user", "userA");
From d044ea5cc8c1719e5d8cc47ca6197f69800ebfea Mon Sep 17 00:00:00 2001
From: Maciej Maciejko
Date: Sun, 11 Feb 2024 18:33:10 +0100
Subject: [PATCH 09/10] sample missing imports
---
.../com/clickhouse/examples/jdbc/GssAuthClient.java | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java b/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
index 9316a12a5..c27afb884 100644
--- a/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
+++ b/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
@@ -1,5 +1,11 @@
package com.clickhouse.examples.jdbc;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Properties;
+
/**
* Sample of using clickhouse jdbc client with kerberos auth.
*
@@ -7,7 +13,7 @@
*/
public class GssAuthClient {
- private void execute() {
+ private void execute() throws SQLException {
String url = "jdbc:ch:http://localhost:8123/default"; // only http protocol supports GSS auth
Properties props = new Properties();
props.setProperty("user", "userA");
@@ -22,7 +28,7 @@ private void execute() {
}
- public static void main(String...args) {
+ public static void main(String...args) throws SQLException {
System.setProperty("java.security.krb5.conf", "/etc/krb5.conf");
System.setProperty("java.security.auth.login.config", "/etc/jaas.conf");
System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
From ef00a6c6718901cf3e0d21e6cd7a94d32b7d53a6 Mon Sep 17 00:00:00 2001
From: Maciej Maciejko
Date: Sun, 11 Feb 2024 19:46:13 +0100
Subject: [PATCH 10/10] clean up
---
.../examples/jdbc/GssAuthClient.java | 38 -------------------
1 file changed, 38 deletions(-)
delete mode 100644 examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
diff --git a/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java b/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
deleted file mode 100644
index c27afb884..000000000
--- a/examples/client/src/main/java/com/clickhouse/examples/jdbc/GssAuthClient.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.clickhouse.examples.jdbc;
-
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.Properties;
-
-/**
- * Sample of using clickhouse jdbc client with kerberos auth.
- *
- * https://clickhouse.com/docs/en/operations/external-authenticators/kerberos
- */
-public class GssAuthClient {
-
- private void execute() throws SQLException {
- String url = "jdbc:ch:http://localhost:8123/default"; // only http protocol supports GSS auth
- Properties props = new Properties();
- props.setProperty("user", "userA");
- props.setProperty("gss_enabled", "true");
- props.setProperty("kerberos_server_name", "HTTP");
- try (Connection conn = DriverManager.getConnection(url, props)) {
- ResultSet rs = conn.createStatement().executeQuery("SELECT currentUser();");
- while (rs.next()) {
- System.out.println(rs.getString(1));
- }
- }
- }
-
-
- public static void main(String...args) throws SQLException {
- System.setProperty("java.security.krb5.conf", "/etc/krb5.conf");
- System.setProperty("java.security.auth.login.config", "/etc/jaas.conf");
- System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
-
- new GssAuthClient().execute();
- }
-}