Added code to programmatically load GMS certs into frontend SSL truststore (#14166)

This commit is contained in:
rahul MALAWADKAR 2025-07-25 03:39:04 +05:30 committed by GitHub
parent 8e1fbaffad
commit 21b5061b55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 268 additions and 6 deletions

View File

@ -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() {

View File

@ -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;
}

View File

@ -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";

View 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;
}
}

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

View File

@ -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}

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

View File

@ -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}