mirror of
				https://github.com/datahub-project/datahub.git
				synced 2025-10-31 18:59:23 +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
	 rahul MALAWADKAR
						rahul MALAWADKAR