All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.hana.datalake.files.HdlfsConnectionConfigurator Maven / Gradle / Ivy

Go to download

An implementation of org.apache.hadoop.fs.FileSystem targeting SAP HANA Data Lake Files.

There is a newer version: 3.0.27
Show newest version
// © 2021-2024 SAP SE or an SAP affiliate company. All rights reserved.
package com.sap.hana.datalake.files;

import com.sap.hana.datalake.files.utils.PemSslUtils;
import com.sap.hana.datalake.files.utils.filewatcher.CertificateWatcherService;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
import org.apache.hadoop.security.ssl.FileBasedKeyStoresFactory;
import org.apache.hadoop.security.ssl.KeyStoresFactory;
import org.apache.hadoop.security.ssl.SSLFactory;
import org.apache.hadoop.security.ssl.SSLHostnameVerifier;
import org.apache.hadoop.util.ReflectionUtils;
import org.apache.hadoop.util.StringUtils;
import org.apache.http.HttpHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

public class HdlfsConnectionConfigurator implements ConnectionConfigurator {

  private static final Logger LOG = LoggerFactory.getLogger(HdlfsConnectionConfigurator.class);
  private static final String POM_PROPERTIES_FILE_PATH = "META-INF/maven/com.sap.hana.datalake.files/sap-hdlfs/pom.properties";
  private static final String USER_AGENT_PRODUCT_NAME = "SAP HDLFS Driver";
  private static final String USER_AGENT_PRODUCT_VERSION = getHdlfsDriverVersion();
  private static final String USER_AGENT = generateUserAgent();

  private static String getHdlfsDriverVersion() {
    final Properties properties = new Properties();
    final ClassLoader classLoader = HdlfsConnectionConfigurator.class.getClassLoader();

    try (final InputStream is = classLoader.getResourceAsStream(POM_PROPERTIES_FILE_PATH)) {
      properties.load(is);
    } catch (final Exception ex) {
      LOG.warn("Could not load pom.properties file", ex);
    }

    return properties.getProperty("version");
  }

  private static String generateUserAgent() {
    final String productVersion = Optional.ofNullable(USER_AGENT_PRODUCT_VERSION).orElse("");
    final String javaVmName = System.getProperty("java.vm.name");
    final String javaVmVersion = System.getProperty("java.vm.version");
    final String osName = System.getProperty("os.name");
    final String osVersion = System.getProperty("os.version");
    final String osArch = System.getProperty("os.arch");

    return String.format("%s/%s (%s/%s) %s/%s (%s)", USER_AGENT_PRODUCT_NAME, productVersion, javaVmName, javaVmVersion, osName, osVersion, osArch);
  }

  private final Configuration conf;
  private final int connectTimeoutMs;
  private final int readTimeoutMs;
  private final AtomicReference socketFactory;

  private SSLMaterialSourceMode sslMaterialSourceMode;
  private HostnameVerifier hostnameVerifier;
  private String fileContainer;
  private boolean impersonationEnabled;
  private String impersonationRoles;
  private String impersonationUserEncoded;
  private String connectionId;
  private boolean isSSLEnabled;
  private String[] enabledProtocols;
  private Map additionalHeaders;

  public HdlfsConnectionConfigurator(final Configuration conf, final ConnectionConfigurator sslConfigurator) throws IOException {
    this(conf, /* connectTimeoutMs */ null, /* readTimeoutMs */ null, sslConfigurator);
  }

  public HdlfsConnectionConfigurator(final Configuration conf, final Integer connectTimeoutMs, final Integer readTimeoutMs) throws IOException {
    this(conf, connectTimeoutMs, readTimeoutMs, /* sslConfigurator */ null);
  }

  private HdlfsConnectionConfigurator(final Configuration conf, final Integer connectTimeoutMs, final Integer readTimeoutMs, final ConnectionConfigurator sslConfigurator) throws IOException {
    this.conf = conf;
    this.connectTimeoutMs = Optional.ofNullable(connectTimeoutMs).orElse(HdlfsConstants.FS_HDLFS_CONNECTION_TIMEOUT_DEFAULT);
    this.readTimeoutMs = Optional.ofNullable(readTimeoutMs).orElse(HdlfsConstants.FS_HDLFS_READ_TIMEOUT_DEFAULT);
    this.socketFactory = new AtomicReference<>();

    try {
      this.parseConfig();
      this.createSocketFactory();
      this.setupCertificateRotation();
    } catch (final Exception ex) {
      throw new IOException(ex);
    }
  }

  public int getConnectTimeoutMs() {
    return this.connectTimeoutMs;
  }

  public int getReadTimeoutMs() {
    return this.readTimeoutMs;
  }

  public String getFileContainer() {
    return this.fileContainer;
  }

  public boolean isImpersonationEnabled() {
    return this.impersonationEnabled;
  }

  public String getImpersonationUserEncoded() {
    return this.impersonationUserEncoded;
  }

  public String getConnectionId() {
    return this.connectionId;
  }

  public boolean isSSLEnabled() {
    return this.isSSLEnabled;
  }

  public String getImpersonationRoles() {
    return this.impersonationRoles;
  }

  public Map getAdditionalHeaders() {
    return this.additionalHeaders;
  }

  protected SSLMaterialSourceMode getSslMaterialSourceMode() {
    return this.sslMaterialSourceMode;
  }

  protected SSLSocketFactory getSocketFactory() {
    return this.socketFactory.get();
  }

  public HostnameVerifier getHostnameVerifier() {
    return this.hostnameVerifier;
  }

  @Override
  public HttpURLConnection configure(final HttpURLConnection conn) {
    this.configureSSLConn(conn);

    for (final Map.Entry header : this.additionalHeaders.entrySet()) {
      conn.setRequestProperty(header.getKey(), header.getValue());
    }

    if (this.fileContainer != null && !this.fileContainer.isEmpty()) {
      LOG.debug("File container specified, setting header [{}]", HdlfsConstants.FS_HDLFS_CONTAINER_HEADER);
      conn.setRequestProperty(HdlfsConstants.FS_HDLFS_CONTAINER_HEADER, this.fileContainer);
    }

    conn.setRequestProperty(HttpHeaders.USER_AGENT, USER_AGENT);

    if (this.impersonationEnabled && this.impersonationUserEncoded != null && !this.impersonationUserEncoded.isEmpty()) {
      LOG.debug("Impersonation enabled, setting headers [{}] , [{}], and [{}]",
              HdlfsConstants.TRUSTED_USER_HEADER, HdlfsConstants.TRUSTED_USER_ROLES_HEADER, HdlfsConstants.TRUSTED_USER_ENCODING_HEADER);

      conn.setRequestProperty(HdlfsConstants.TRUSTED_USER_HEADER, this.impersonationUserEncoded);
      conn.setRequestProperty(HdlfsConstants.TRUSTED_USER_ENCODING_HEADER, HdlfsConstants.TRUSTED_USER_ENCODING);

      if (this.impersonationRoles != null && !this.impersonationRoles.isEmpty()) {
        conn.setRequestProperty(HdlfsConstants.TRUSTED_USER_ROLES_HEADER, this.impersonationRoles);
      }

    }

    if (this.connectionId != null && !this.connectionId.isEmpty()) {
      LOG.debug("Connection-Id specified, setting header [{}]", HdlfsConstants.FS_HDLFS_CONNECTION_ID_HEADER);
      conn.setRequestProperty(HdlfsConstants.FS_HDLFS_CONNECTION_ID_HEADER, this.getConnectionId());
    }

    return conn;
  }

  private void configureSSLConn(final HttpURLConnection conn) {
    if (conn instanceof HttpsURLConnection) {
      final HttpsURLConnection secureConn = (HttpsURLConnection) conn;
      secureConn.setSSLSocketFactory(this.socketFactory.get());
      secureConn.setHostnameVerifier(this.hostnameVerifier);
    }

    conn.setConnectTimeout(this.connectTimeoutMs);
    conn.setReadTimeout(this.readTimeoutMs);
  }

  /**
   * parse the file system config to check for specified properties.
   */
  private void parseConfig() throws GeneralSecurityException, IOException {
    // parse the filecontainer if present along with impersonation flag
    this.fileContainer = this.conf.get(HdlfsConstants.FS_HDLFS_FILECONTAINER_KEY);
    this.impersonationEnabled = this.conf.getBoolean(HdlfsConstants.FS_HDLFS_IMPERSONATION_ENABLED_KEY, false);
    this.additionalHeaders = this.conf.getPropsWithPrefix(HdlfsConstants.FS_HDLFS_HTTP_CLIENT_ADDITIONAL_HEADERS_KEY_PREFIX);

    if (this.impersonationEnabled) {
      final String impersonationUser = this.conf.get(HdlfsConstants.FS_HDLFS_IMPERSONATION_USER_KEY, "");

      if (!impersonationUser.isEmpty()) {
        this.impersonationUserEncoded = Base64.getEncoder().encodeToString(impersonationUser.getBytes(HdlfsConstants.DEFAULT_CHARSET));
        this.impersonationRoles = this.conf.get(HdlfsConstants.FS_HDLFS_IMPERSONATION_USER_ROLES);
      }
    }

    // parse the connection id property is present
    this.connectionId = this.conf.get(HdlfsConstants.FS_HDLFS_CONNECTION_ID);

    // SSL configuration
    this.isSSLEnabled = this.conf.getBoolean(HdlfsConstants.FS_HDLFS_SSL_ENABLED_KEY, true);
    this.enabledProtocols = this.conf.getStrings(SSLFactory.SSL_ENABLED_PROTOCOLS_KEY, SSLFactory.SSL_ENABLED_PROTOCOLS_DEFAULT);
    this.hostnameVerifier = SSLFactory.getHostnameVerifier(StringUtils.toUpperCase(this.conf.get(SSLFactory.SSL_HOSTNAME_VERIFIER_KEY, SSLHostnameVerifier.DEFAULT.toString()).trim()));
    this.sslMaterialSourceMode = this.isCustomKeyStoreFactoryConfigured() ?
            SSLMaterialSourceMode.CUSTOM_KEYSTORES_FACTORY :
            this.arePemFileConfigsSet() ?
                    SSLMaterialSourceMode.PEM_FILES :
                    SSLMaterialSourceMode.FILE_BASED_KEYSTORES_FACTORY;

    LOG.info("SSL Material source mode: [{}]", this.sslMaterialSourceMode);
  }

  private void createSocketFactory() throws GeneralSecurityException, IOException {
    if (this.sslMaterialSourceMode == SSLMaterialSourceMode.PEM_FILES) {
      this.socketFactory.set(this.createSocketFactoryForPem());
    } else {
      this.socketFactory.set(this.createSocketFactoryForKeyStores());
    }
  }

  private SSLSocketFactory createSocketFactoryForPem() throws GeneralSecurityException, IOException {
    final String certFileConfigValue = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_CERTFILE_KEY);
    final String keyFileConfigValue = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_KEYFILE_KEY);
    final String keyFilePassphrase = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_KEYFILE_PASSWORD_KEY, HdlfsConstants.FS_HDLFS_SSL_KEYFILE_PASSWORD_DEFAULT);
    final String caFileConfigValue = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_CAPATH_KEY);

    final KeyManager[] keyManagers = PemSslUtils.createKeyManagers(certFileConfigValue, keyFileConfigValue, keyFilePassphrase);
    final TrustManager[] trustManagers = PemSslUtils.createTrustManagers(caFileConfigValue);

    final SSLContext context = this.createSSLContext(keyManagers, trustManagers);

    return context.getSocketFactory();
  }

  private SSLSocketFactory createSocketFactoryForKeyStores() throws GeneralSecurityException, IOException {
    final Configuration sslConf = this.generateSSLConfiguration();
    final KeyStoresFactory keystoresFactory = this.getKeyStoresFactory(sslConf);

    keystoresFactory.init(SSLFactory.Mode.CLIENT);
    final SSLContext context = this.createSSLContext(keystoresFactory.getKeyManagers(), keystoresFactory.getTrustManagers());

    return context.getSocketFactory();
  }

  protected Configuration generateSSLConfiguration() throws IOException {
    final Configuration sslConf = new Configuration(this.conf);

    // We do not need to parse keystore/truststore config in case
    // 1) a custom keystores factory is used OR
    // 2) SSL is disabled
    if (this.isCustomKeyStoreFactoryConfigured() || !this.isSSLEnabled) {
      return sslConf;
    }

    sslConf.setBoolean(SSLFactory.SSL_REQUIRE_CLIENT_CERT_KEY, true);

    // Default file based, so we need to load our configurations
    final String keystoreLocation = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_KEYSTORE_LOCATION_KEY);
    final String keystorePassword = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_KEYSTORE_PASSWORD_KEY, HdlfsConstants.FS_HDLFS_SSL_KEYSTORE_PASSWORD_DEFAULT);
    final String keystoreKeyPassword = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_KEYSTORE_KEYPASSWORD_KEY, keystorePassword);
    final String keystoreType = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_KEYSTORE_TYPE_KEY, HdlfsConstants.FS_HDLFS_SSL_KEYSTORE_TYPE_DEFAULT);
    final String truststoreLocation = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_TRUSTSTORE_LOCATION_KEY, HdlfsConstants.FS_HDLFS_SSL_TRUSTSTORE_LOCATION_DEFAULT);
    final String truststorePassword = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_TRUSTSTORE_PASSWORD_KEY, HdlfsConstants.FS_HDLFS_SSL_TRUSTSTORE_PASSWORD_DEFAULT);
    final String truststoreType = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_TRUSTSTORE_TYPE_KEY, HdlfsConstants.FS_HDLFS_SSL_TRUSTSTORE_TYPE_DEFAULT);

    if (keystoreLocation == null || keystoreLocation.isEmpty()) {
      throw new IOException("Keystore location was not provided.");
    }

    sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT,
            FileBasedKeyStoresFactory.SSL_KEYSTORE_LOCATION_TPL_KEY), keystoreLocation);
    sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT,
            FileBasedKeyStoresFactory.SSL_KEYSTORE_PASSWORD_TPL_KEY), keystorePassword);
    sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT,
            FileBasedKeyStoresFactory.SSL_KEYSTORE_KEYPASSWORD_TPL_KEY), keystoreKeyPassword);
    sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT,
            FileBasedKeyStoresFactory.SSL_KEYSTORE_TYPE_TPL_KEY), keystoreType);
    sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT,
            FileBasedKeyStoresFactory.SSL_TRUSTSTORE_LOCATION_TPL_KEY), truststoreLocation);
    sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT,
            FileBasedKeyStoresFactory.SSL_TRUSTSTORE_PASSWORD_TPL_KEY), truststorePassword);
    sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT,
            FileBasedKeyStoresFactory.SSL_TRUSTSTORE_TYPE_TPL_KEY), truststoreType);

    // Disable auto-reload since rotation is already handled by this class (HdlfsConnectionConfiguration)
    sslConf.setLong(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT,
            FileBasedKeyStoresFactory.SSL_STORES_RELOAD_INTERVAL_TPL_KEY), 0L);
    sslConf.setLong(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT,
            FileBasedKeyStoresFactory.SSL_TRUSTSTORE_RELOAD_INTERVAL_TPL_KEY), 0L);

    return sslConf;
  }

  private KeyStoresFactory getKeyStoresFactory(final Configuration sslConf) {
    final Class klass = this.conf.getClass(HdlfsConstants.FS_HDLFS_KEYSTORES_FACTORY_CLASS_KEY,
            HdlfsConstants.FS_HDLFS_KEYSTORES_FACTORY_CLASS_DEFAULT,
            KeyStoresFactory.class);

    return ReflectionUtils.newInstance(klass, sslConf);
  }

  private SSLContext createSSLContext(final KeyManager[] keyManagers, final TrustManager[] trustManagers) throws GeneralSecurityException {
    final SSLContext context = SSLContext.getInstance("TLS");

    context.init(keyManagers, trustManagers, null);
    context.getDefaultSSLParameters().setProtocols(this.enabledProtocols);

    return context;
  }

  private boolean isCustomKeyStoreFactoryConfigured() {
    final String keyStoreFactoryClassConfig = this.conf.get(HdlfsConstants.FS_HDLFS_KEYSTORES_FACTORY_CLASS_KEY);

    return  keyStoreFactoryClassConfig != null && !keyStoreFactoryClassConfig.isEmpty();
  }

  private boolean arePemFileConfigsSet() {
    final String certFile = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_CERTFILE_KEY, "");
    final String keyFile = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_KEYFILE_KEY, "");

    return !certFile.isEmpty() && !keyFile.isEmpty();
  }

  private void setupCertificateRotation() {
    final boolean isCertificateRotationEnabled = this.conf.getBoolean(HdlfsConstants.FS_HDLFS_SSL_ROTATION_ENABLED_KEY,
            HdlfsConstants.FS_HDLFS_SSL_ROTATION_ENABLED_DEFAULT);

    if (!isCertificateRotationEnabled) {
      LOG.info("Certificate rotation is not enabled; skip setting up certificate watchers");
      return;
    }

    if (this.sslMaterialSourceMode == SSLMaterialSourceMode.PEM_FILES) {
      this.triggerPemFileWatchers();
    } else if (this.sslMaterialSourceMode == SSLMaterialSourceMode.FILE_BASED_KEYSTORES_FACTORY) {
      this.triggerFileBasedKeystoreFileWatchers();
    }
  }

  private void triggerPemFileWatchers() {
    final String certFileValue = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_CERTFILE_KEY);
    final String keyFileValue = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_KEYFILE_KEY);
    final String caFileValue = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_CAPATH_KEY);
    final List certificatesToWatch = this.buildListOfCertificatesToWatch(certFileValue, keyFileValue, caFileValue);

    this.watchCertFiles(certificatesToWatch);
  }

  private void triggerFileBasedKeystoreFileWatchers() {
    final String keystorePath = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_KEYSTORE_LOCATION_KEY);
    final String truststorePath = this.conf.get(HdlfsConstants.FS_HDLFS_SSL_TRUSTSTORE_LOCATION_KEY);
    final List certificatesToWatch = this.buildListOfCertificatesToWatch(keystorePath, truststorePath);

    this.watchCertFiles(certificatesToWatch);
  }

  private List buildListOfCertificatesToWatch(final String... files) {
    return Arrays.stream(files)
            .filter(file -> file != null && !file.isEmpty() && !PemSslUtils.isPemFileContent(file))
            .collect(Collectors.toList());
  }

  private void watchCertFiles(final List certificatesToWatch) {
    if (!certificatesToWatch.isEmpty()) {
      final CertificateWatcherService certificateWatcherService = CertificateWatcherService.getInstance();
      certificateWatcherService.watchCertificates(certificatesToWatch, this::handleCertificateRotation, this.conf);
    }
  }

  private void handleCertificateRotation() {
    LOG.info("Certificate rotation detected; updating SSL context and socket factory");

    try {
      this.createSocketFactory();
    } catch (final Exception ex) {
      LOG.error("Error occurred while updating SSL context and socket factory after certificate rotation", ex);
      throw new RuntimeException(ex);
    }

    LOG.info("Successfully updated SSL context and socket factory");
  }

  protected enum SSLMaterialSourceMode {
    CUSTOM_KEYSTORES_FACTORY,
    FILE_BASED_KEYSTORES_FACTORY,
    PEM_FILES
  }

}

// © 2021-2024 SAP SE or an SAP affiliate company. All rights reserved.




© 2015 - 2025 Weber Informatics LLC | Privacy Policy