mirror of
https://github.com/datahub-project/datahub.git
synced 2025-08-01 13:58:01 +00:00
Added code to programmatically load GMS certs into frontend SSL truststore (#14166)
This commit is contained in:
parent
8e1fbaffad
commit
21b5061b55
@ -40,6 +40,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import io.micrometer.core.instrument.util.HierarchicalNameMapper;
|
||||
import io.micrometer.jmx.JmxConfig;
|
||||
import io.micrometer.jmx.JmxMeterRegistry;
|
||||
import java.net.http.HttpClient;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import javax.annotation.Nonnull;
|
||||
@ -60,6 +61,8 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
|
||||
import play.Environment;
|
||||
import play.cache.SyncCacheApi;
|
||||
import utils.ConfigUtil;
|
||||
import utils.CustomHttpClientFactory;
|
||||
import utils.TruststoreConfig;
|
||||
|
||||
/** Responsible for configuring, validating, and providing authentication related components. */
|
||||
@Slf4j
|
||||
@ -312,8 +315,34 @@ public class AuthModule extends AbstractModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
protected CloseableHttpClient provideHttpClient() {
|
||||
return HttpClients.createDefault();
|
||||
protected CloseableHttpClient provideCloseableHttpClient(com.typesafe.config.Config config) {
|
||||
TruststoreConfig tsConfig = TruststoreConfig.fromConfig(config);
|
||||
try {
|
||||
if (tsConfig.isValid()) {
|
||||
return CustomHttpClientFactory.getApacheHttpClient(
|
||||
tsConfig.path, tsConfig.password, tsConfig.type);
|
||||
} else {
|
||||
return HttpClients.createDefault();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to initialize CloseableHttpClient", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
protected HttpClient provideHttpClient(com.typesafe.config.Config config) {
|
||||
TruststoreConfig tsConfig = TruststoreConfig.fromConfig(config);
|
||||
try {
|
||||
if (tsConfig.isValid()) {
|
||||
return CustomHttpClientFactory.getJavaHttpClient(
|
||||
tsConfig.path, tsConfig.password, tsConfig.type);
|
||||
} else {
|
||||
return HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to initialize HttpClient", e);
|
||||
}
|
||||
}
|
||||
|
||||
private com.linkedin.restli.client.Client buildRestliClient() {
|
||||
|
@ -41,14 +41,14 @@ public class Application extends Controller {
|
||||
private static final Logger logger = LoggerFactory.getLogger(Application.class.getName());
|
||||
private static final Set<String> RESTRICTED_HEADERS =
|
||||
Set.of("connection", "host", "content-length", "expect", "upgrade", "transfer-encoding");
|
||||
private final HttpClient httpClient =
|
||||
HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
|
||||
private final HttpClient httpClient;
|
||||
|
||||
private final Config config;
|
||||
private final Environment environment;
|
||||
|
||||
@Inject
|
||||
public Application(Environment environment, @Nonnull Config config) {
|
||||
public Application(HttpClient httpClient, Environment environment, @Nonnull Config config) {
|
||||
this.httpClient = httpClient;
|
||||
this.config = config;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
@ -13,6 +13,12 @@ public class ConfigUtil {
|
||||
public static final String METADATA_SERVICE_USE_SSL_CONFIG_PATH = "metadataService.useSsl";
|
||||
public static final String METADATA_SERVICE_SSL_PROTOCOL_CONFIG_PATH =
|
||||
"metadataService.sslProtocol";
|
||||
public static final String METADATA_SERVICE_SSL_TRUST_STORE_PATH =
|
||||
"metadataService.truststore.path";
|
||||
public static final String METADATA_SERVICE_SSL_TRUST_STORE_PASSWORD =
|
||||
"metadataService.truststore.password";
|
||||
public static final String METADATA_SERVICE_SSL_TRUST_STORE_TYPE =
|
||||
"metadataService.truststore.type";
|
||||
|
||||
// Legacy env-var based config values, for backwards compatibility:
|
||||
public static final String GMS_HOST_ENV_VAR = "DATAHUB_GMS_HOST";
|
||||
|
73
datahub-frontend/app/utils/CustomHttpClientFactory.java
Normal file
73
datahub-frontend/app/utils/CustomHttpClientFactory.java
Normal file
@ -0,0 +1,73 @@
|
||||
package utils;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.net.http.HttpClient;
|
||||
import java.security.KeyStore;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class CustomHttpClientFactory {
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(CustomHttpClientFactory.class.getName());
|
||||
|
||||
public static SSLContext getSslContext(String path, String pass, String type) throws Exception {
|
||||
return createSslContext(path, pass, type);
|
||||
}
|
||||
|
||||
public static HttpClient getJavaHttpClient(String path, String pass, String type) {
|
||||
try {
|
||||
log.info(
|
||||
"Initializing Java HttpClient with custom truststore at '{}' and type '{}'", path, type);
|
||||
return HttpClient.newBuilder().sslContext(getSslContext(path, pass, type)).build();
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to initialize Java HttpClient with custom truststore at '{}'. Falling back to default HttpClient. Reason: {}",
|
||||
path,
|
||||
e.getMessage(),
|
||||
e);
|
||||
return HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static CloseableHttpClient getApacheHttpClient(String path, String pass, String type) {
|
||||
try {
|
||||
log.info(
|
||||
"Initializing Apache CloseableHttpClient with custom truststore at '{}' and type '{}'",
|
||||
path,
|
||||
type);
|
||||
return HttpClients.custom()
|
||||
.setSSLSocketFactory(new SSLConnectionSocketFactory(getSslContext(path, pass, type)))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to initialize Apache HttpClient with custom truststore at '{}'. Falling back to default HttpClient. Reason: {}",
|
||||
path,
|
||||
e.getMessage(),
|
||||
e);
|
||||
return HttpClients.createDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public static SSLContext createSslContext(
|
||||
String truststorePath, String truststorePassword, String truststoreType) throws Exception {
|
||||
log.info(
|
||||
"Creating SSLContext with truststore at '{}' and type '{}'",
|
||||
truststorePath,
|
||||
truststoreType);
|
||||
KeyStore trustStore = KeyStore.getInstance(truststoreType);
|
||||
try (FileInputStream fis = new FileInputStream(truststorePath)) {
|
||||
trustStore.load(fis, truststorePassword.toCharArray());
|
||||
}
|
||||
TrustManagerFactory tmf =
|
||||
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(trustStore);
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, tmf.getTrustManagers(), null);
|
||||
return sslContext;
|
||||
}
|
||||
}
|
33
datahub-frontend/app/utils/TruststoreConfig.java
Normal file
33
datahub-frontend/app/utils/TruststoreConfig.java
Normal file
@ -0,0 +1,33 @@
|
||||
package utils;
|
||||
|
||||
import com.typesafe.config.Config;
|
||||
|
||||
public class TruststoreConfig {
|
||||
public final String path;
|
||||
public final String password;
|
||||
public final String type;
|
||||
public final boolean metadataServiceUseSsl;
|
||||
|
||||
public TruststoreConfig(
|
||||
String path, String password, String type, boolean metadataServiceUseSsl) {
|
||||
this.path = path;
|
||||
this.password = password;
|
||||
this.type = type;
|
||||
this.metadataServiceUseSsl = metadataServiceUseSsl;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return metadataServiceUseSsl && path != null && password != null;
|
||||
}
|
||||
|
||||
public static TruststoreConfig fromConfig(Config config) {
|
||||
return new TruststoreConfig(
|
||||
ConfigUtil.getString(config, ConfigUtil.METADATA_SERVICE_SSL_TRUST_STORE_PATH, null),
|
||||
ConfigUtil.getString(config, ConfigUtil.METADATA_SERVICE_SSL_TRUST_STORE_PASSWORD, null),
|
||||
ConfigUtil.getString(config, ConfigUtil.METADATA_SERVICE_SSL_TRUST_STORE_TYPE, "PKCS12"),
|
||||
ConfigUtil.getBoolean(
|
||||
config,
|
||||
ConfigUtil.METADATA_SERVICE_USE_SSL_CONFIG_PATH,
|
||||
ConfigUtil.DEFAULT_METADATA_SERVICE_USE_SSL));
|
||||
}
|
||||
}
|
@ -232,6 +232,9 @@ play.http.session.maxAge = ${?MAX_SESSION_TOKEN_AGE}
|
||||
metadataService.host=${?DATAHUB_GMS_HOST}
|
||||
metadataService.port=${?DATAHUB_GMS_PORT}
|
||||
metadataService.useSsl=${?DATAHUB_GMS_USE_SSL} # Internal SSL is not fully supported yet.
|
||||
metadataService.truststore.path=${?DATAHUB_GMS_SSL_TRUSTSTORE_PATH}
|
||||
metadataService.truststore.password=${?DATAHUB_GMS_SSL_TRUSTSTORE_PASSWORD}
|
||||
metadataService.truststore.type=${?DATAHUB_GMS_SSL_TRUSTSTORE_TYPE}
|
||||
|
||||
# Set to "true" to enable Metadata Service Authentication. False BY DEFAULT.
|
||||
metadataService.auth.enabled=${?METADATA_SERVICE_AUTH_ENABLED}
|
||||
@ -257,4 +260,4 @@ entityClient.restli.get.batchConcurrency = ${?ENTITY_CLIENT_RESTLI_GET_BATCH_CON
|
||||
graphql.verbose.logging = false
|
||||
graphql.verbose.logging = ${?GRAPHQL_VERBOSE_LOGGING}
|
||||
graphql.verbose.slowQueryMillis = 2500
|
||||
graphql.verbose.slowQueryMillis = ${?GRAPHQL_VERBOSE_LONG_QUERY_MILLIS}
|
||||
graphql.verbose.slowQueryMillis = ${?GRAPHQL_VERBOSE_LONG_QUERY_MILLIS}
|
||||
|
114
datahub-frontend/test/utils/CustomHttpClientFactoryTest.java
Normal file
114
datahub-frontend/test/utils/CustomHttpClientFactoryTest.java
Normal file
@ -0,0 +1,114 @@
|
||||
package utils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class CustomHttpClientFactoryTest {
|
||||
|
||||
@TempDir Path tempDir;
|
||||
|
||||
private static final String TRUSTSTORE_PASSWORD = "testpassword";
|
||||
private static final String TRUSTSTORE_TYPE = "PKCS12";
|
||||
|
||||
// Helper: Generate a temp PKCS12 truststore using keytool
|
||||
Path generateTempTruststore() throws IOException, InterruptedException {
|
||||
Path truststorePath = tempDir.resolve("test-truststore-" + System.nanoTime() + ".p12");
|
||||
String keytoolPath = System.getenv("KEYTOOL_PATH");
|
||||
if (keytoolPath == null) {
|
||||
keytoolPath = System.getProperty("java.home") + "/bin/keytool";
|
||||
}
|
||||
File keytoolFile = new File(keytoolPath);
|
||||
if (!keytoolFile.exists()) {
|
||||
keytoolPath = "keytool"; // fallback to system path
|
||||
}
|
||||
List<String> command =
|
||||
List.of(
|
||||
keytoolPath,
|
||||
"-genkeypair",
|
||||
"-alias",
|
||||
"testcert",
|
||||
"-keyalg",
|
||||
"RSA",
|
||||
"-keysize",
|
||||
"2048",
|
||||
"-storetype",
|
||||
"PKCS12",
|
||||
"-keystore",
|
||||
truststorePath.toString(),
|
||||
"-validity",
|
||||
"3650",
|
||||
"-storepass",
|
||||
TRUSTSTORE_PASSWORD,
|
||||
"-dname",
|
||||
"CN=Test, OU=Dev, O=Example, L=Test, S=Test, C=US");
|
||||
Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
|
||||
String output = new String(process.getInputStream().readAllBytes());
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0)
|
||||
throw new RuntimeException(
|
||||
"Could not generate truststore, exit code: " + exitCode + ", output: " + output);
|
||||
return truststorePath;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateSslContextWithValidTruststore() throws Exception {
|
||||
Path truststorePath = generateTempTruststore();
|
||||
SSLContext context =
|
||||
CustomHttpClientFactory.createSslContext(
|
||||
truststorePath.toString(), TRUSTSTORE_PASSWORD, TRUSTSTORE_TYPE);
|
||||
assertNotNull(context);
|
||||
assertEquals("TLS", context.getProtocol());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateSslContextWithInvalidTruststoreThrows() {
|
||||
assertThrows(
|
||||
Exception.class,
|
||||
() ->
|
||||
CustomHttpClientFactory.createSslContext(
|
||||
"doesnotexist.p12", "wrongpassword", TRUSTSTORE_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJavaHttpClientWithValidTruststore() throws Exception {
|
||||
Path truststorePath = generateTempTruststore();
|
||||
HttpClient client =
|
||||
CustomHttpClientFactory.getJavaHttpClient(
|
||||
truststorePath.toString(), TRUSTSTORE_PASSWORD, TRUSTSTORE_TYPE);
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJavaHttpClientWithInvalidTruststoreFallsBack() {
|
||||
HttpClient client =
|
||||
CustomHttpClientFactory.getJavaHttpClient(
|
||||
"doesnotexist.p12", "wrongpassword", TRUSTSTORE_TYPE);
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetApacheHttpClientWithValidTruststore() throws Exception {
|
||||
Path truststorePath = generateTempTruststore();
|
||||
CloseableHttpClient client =
|
||||
CustomHttpClientFactory.getApacheHttpClient(
|
||||
truststorePath.toString(), TRUSTSTORE_PASSWORD, TRUSTSTORE_TYPE);
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetApacheHttpClientWithInvalidTruststoreFallsBack() {
|
||||
CloseableHttpClient client =
|
||||
CustomHttpClientFactory.getApacheHttpClient(
|
||||
"doesnotexist.p12", "wrongpassword", TRUSTSTORE_TYPE);
|
||||
assertNotNull(client);
|
||||
}
|
||||
}
|
@ -89,6 +89,10 @@ datahub:
|
||||
host: ${DATAHUB_GMS_HOST:localhost}
|
||||
port: ${DATAHUB_GMS_PORT:8080}
|
||||
useSSL: ${DATAHUB_GMS_USE_SSL:${GMS_USE_SSL:false}}
|
||||
truststore:
|
||||
path: ${DATAHUB_GMS_SSL_TRUSTSTORE_PATH:#{null}} # Required if useSSL is true
|
||||
password: ${DATAHUB_GMS_SSL_TRUSTSTORE_PASSWORD:#{null}} # Required if useSSL is true
|
||||
type: ${DATAHUB_GMS_SSL_TRUSTSTORE_TYPE:PKCS12}
|
||||
async:
|
||||
request-timeout-ms: ${DATAHUB_GMS_ASYNC_REQUEST_TIMEOUT_MS:55000}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user