com.sap.cloud.security.ams.dcn.BundleGatewayUpdater Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jakarta-ams Show documentation
Show all versions of jakarta-ams Show documentation
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