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

com.sap.cloud.security.ams.dcn.BundleGatewayUpdater Maven / Gradle / Ivy

Go to download

Client Library for integrating Jakarta EE applications with SAP Authorization Management Service (AMS)

The newest version!
/************************************************************************
 * © 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cloud.security.ams.dcn;

import com.sap.cloud.security.ams.dcl.client.dcn.DcnContainer;
import com.sap.cloud.security.ams.dcl.client.dcn.DcnTools;
import com.sap.cloud.security.ams.dcl.client.pdp.PolicyDecisionPoint.Parameters;
import com.sap.cloud.security.ams.dcl.spi.pdp.BundleStatusImpl;
import com.sap.cloud.security.ams.dcn.Result.Failure;
import com.sap.cloud.security.ams.dcn.Result.Success;
import com.sap.cloud.security.config.Environment;
import com.sap.cloud.security.mtls.SSLContextFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.security.GeneralSecurityException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.net.ssl.SSLContext;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This implementation of the {@link BundleUpdater} interface loads DCN bundles from the
 * bundle-gateway.
 */
final class BundleGatewayUpdater implements BundleUpdater {

  static final class ErrorMessages {
    static final String NO_IAS_CONFIGURATION = "No IAS configuration found";
    static final String UNEXPECTED_NUMBER_OF_DATA_JSON_FILES =
        "Unexpected number(!=1) of data.json files found.";
  }

  final String bundleUrl;
  final HttpClient httpClient;
  final DcnTools dcnTools;
  final long updateInterval;
  static final long DEFAULT_UPDATE_INTERVAL_S = 30;
  static final Logger LOGGER = LoggerFactory.getLogger(BundleGatewayUpdater.class);
  final ScheduledExecutorService executor;
  final long startupHealthCheckTimeout;

  private String etag;

  BundleGatewayUpdater(String bundleUrl, HttpClient httpClient) {
    this(
        bundleUrl,
        httpClient,
        DEFAULT_UPDATE_INTERVAL_S,
        DcnTools.getInstance(),
        Executors.newScheduledThreadPool(1),
        Parameters.STARTUP_HEALTH_CHECK_TIMEOUT.getValue());
  }

  BundleGatewayUpdater(
      String bundleUrl,
      HttpClient httpClient,
      long updateInterval,
      DcnTools dcnTools,
      ScheduledExecutorService executor,
      long startupHealthCheckTimeout) {

    this.updateInterval = updateInterval;
    this.bundleUrl = bundleUrl;
    this.httpClient = httpClient;
    this.etag = "";
    this.dcnTools = dcnTools;
    this.executor = executor;
    // The used parameter is allowed to be set to less than 1 to disable the check.
    // But here it is reused slightly different and a timeout less than 1 second is not useful.
    this.startupHealthCheckTimeout = startupHealthCheckTimeout > 1 ? startupHealthCheckTimeout : 1;
  }

  static Result create(
      Supplier> connectionDataSupplier,
      Function> httpClientBuilder) {

    var connectionDataResult = connectionDataSupplier.get();
    if (connectionDataResult instanceof Result.Success connectionData) {
      LOGGER.debug(
          "Connection data extracted retrieved. Bundle-gateway URL: {}",
          connectionData.getValue().bundleUrl());

      var httpClientResult = httpClientBuilder.apply(connectionData.value());
      if (httpClientResult instanceof Success httpClient) {
        return Result.success(
            new BundleGatewayUpdater(connectionData.value().bundleUrl(), httpClient.value()));
      } else {
        return Result.failure(((Failure) httpClientResult).error());
      }
    } else {
      return Result.failure(((Failure) connectionDataResult).error());
    }
  }

  Map> deserializeDcnContainers(
      DcnTools dcnTools, Map files) {
    return files.entrySet().stream()
        .filter(e -> e.getKey().endsWith(".dcn"))
        .collect(
            Collectors.toMap(
                Entry::getKey,
                v -> {
                  try {
                    return Result.success(
                        dcnTools.deserialize(new ByteArrayInputStream(v.getValue())));
                  } catch (IOException e) {
                    return Result.failure(new Error(e.getMessage()));
                  }
                }));
  }

  Result deserializeDataJson(Map files) {
    var dataJsonFiles =
        files.entrySet().stream()
            .filter(e -> e.getKey().endsWith("data.json"))
            .map(e -> DataJson.fromJson(new String(e.getValue())))
            .toList();

    if (dataJsonFiles.size() == 1) {
      return Result.success(dataJsonFiles.get(0));
    } else {
      return Result.failure(new Error(ErrorMessages.UNEXPECTED_NUMBER_OF_DATA_JSON_FILES));
    }
  }

  public CompletableFuture> get() {
    var bundleRequest =
        HttpRequest.newBuilder()
            .uri(URI.create(this.bundleUrl))
            .header("If-None-Match", etag)
            .GET()
            .build();

    return httpClient
        .sendAsync(bundleRequest, BodyHandlers.ofByteArray())
        .thenApply(
            response -> {
              LOGGER.debug(
                  "Response from bundle-gateway received. Response code: {}",
                  response.statusCode());

              if (response.statusCode() == 200) {
                var body = response.body();
                var contents = extractFilesFromTarGz.apply(body);

                if (contents instanceof Result.Success> files) {

                  Map> f =
                      deserializeDcnContainers(dcnTools, files.value());

                  var dataJson = deserializeDataJson(files.value());

                  if (dataJson.isFailure()) {
                    return Result.failure(dataJson.getError());
                  }

                  var headers = response.headers();
                  if (headers != null) {
                    setEtag(headers.firstValue("ETag").orElse(""));
                  }

                  return Result.success(new BundleContent(new Repository(f), dataJson.getValue()));
                } else {
                  return Result.failure(contents.getError());
                }
              } else {
                return Result.failure(new Error("Status Code:" + response.statusCode()));
              }
            });
  }

  synchronized void setEtag(String etag) {
    this.etag = etag;
  }

  @Override
  public void syncGet(EngineDataHolder engineDataHolder, boolean compatibilityMode) {

    var bs = new BundleStatusImpl();
    bs.setName(this.bundleUrl);
    bs.setLastRequest(Instant.now());

    var updateRequest = get();
    try {

      var loadResult = updateRequest.get(startupHealthCheckTimeout, TimeUnit.SECONDS);

      if (loadResult.isSuccess()) {
        BundleUpdaterTools.setBundleStatusTimesToNow(bs);
        BundleUpdaterTools.setNoError(bs);

        var newEngine =
            DcnPolicyDecisionPoint.createEngineAndDataJson(
                loadResult.getValue().repository().getDcnRepository(),
                loadResult.getValue().dataJson(),
                compatibilityMode);

        engineDataHolder.setEngineData(newEngine.getLeft(), newEngine.getRight());
        engineDataHolder.setBundleStatus(bs);
      } else {
        BundleUpdaterTools.setErrorMessage(bs, loadResult.getError().errorMsg());
        engineDataHolder.setBundleStatus(bs);
      }
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
      var errorMsg = e.toString(); //provides exception type and message + handles message = null case
      LOGGER.error("Update failed: {}", errorMsg);
      bs.setBundleError(true);
      bs.setMessage(errorMsg);
      engineDataHolder.setBundleStatus(bs);
    }
  }

  @Override
  public void startPeriodicUpdates(EngineDataHolder engineDataHolder, boolean compatibilityMode) {

    executor.scheduleWithFixedDelay(
        () -> syncGet(engineDataHolder, compatibilityMode), 0, updateInterval, TimeUnit.SECONDS);
  }

  static final Function>> extractFilesFromTarGz =
      (fileContent) -> {
        Map extractedFiles = new HashMap<>();

        try (TarArchiveInputStream tarInput =
            new TarArchiveInputStream(
                new GzipCompressorInputStream(new ByteArrayInputStream(fileContent)))) {

          TarArchiveEntry entry;
          while ((entry = tarInput.getNextEntry()) != null) {
            if (!entry.isDirectory()) {
              byte[] content = tarInput.readAllBytes();
              extractedFiles.put(entry.getName(), content);
            }
          }
        } catch (IOException e) {
          return new Result.Failure<>(new Error(e.getMessage()));
        }

        return new Success<>(extractedFiles);
      };

  static Result environmentToConnectionData(Environment env) {
    var config = env.getIasConfiguration();
    if (config == null) {
      return Result.failure(new Error("No IAS configuration found"));
    }

    var certificate = config.getProperty("certificate");
    var key = config.getProperty("key");
    var amsInstanceId = config.getProperty("authorization_instance_id");
    var amsBundleUrl = config.getProperty("authorization_bundle_url");
    var bundleUrl = amsBundleUrl + "/" + amsInstanceId + ".dcn.tar.gz";

    return Result.success(new ConnectionData(certificate, key, bundleUrl));
  }

  static Function> buildHttpClient =
      (connectionData) -> {
        try {
          SSLContext sslContext =
              SSLContextFactory.getInstance()
                  .create(connectionData.certificate(), connectionData.key());

          HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build();

          return new Success<>(client);
        } catch (IOException | GeneralSecurityException e) {
          return new Failure<>(new Error(e.toString()));
        }
      };

  /**
   * Represents the data required downloading a DCN bundle from the AMS bundle-gateway via a mtls
   * connection.
   *
   * @param certificate the certificate from the identity binding
   * @param key the private key from the identity binding
   * @param bundleUrl the URL of bundle
   */
  record ConnectionData(String certificate, String key, String bundleUrl) {}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy