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(); - } -}