
com.sap.cloud.sdk.cloudplatform.connectivity.ScpCfDestinationFacade Maven / Gradle / Ivy
Show all versions of connectivity-scp-cf Show documentation
/*
* Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved.
*/
package com.sap.cloud.sdk.cloudplatform.connectivity;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.json.JsonSanitizer;
import com.sap.cloud.sdk.cloudplatform.cache.CacheKey;
import com.sap.cloud.sdk.cloudplatform.cache.CacheManager;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.cloudplatform.security.user.ScpCfUserFacade;
import com.sap.cloud.sdk.cloudplatform.security.user.User;
import com.sap.cloud.sdk.cloudplatform.security.user.UserAccessor;
import com.sap.cloud.sdk.cloudplatform.tenant.Tenant;
import com.sap.cloud.sdk.cloudplatform.tenant.TenantAccessor;
import lombok.RequiredArgsConstructor;
/**
* Implementation of Cloud platform abstraction for destinations on SAP Cloud Platform, Cloud Foundry.
*
* In addition to reading destinations via the SAP CP CF destination service REST API , this class provides the ability
* to read HTTP destination information from the environment variable "destinations". The environment variable is
* expected to be in JSON format (see {@link AbstractDestinationFacade}).
*/
@RequiredArgsConstructor
public class ScpCfDestinationFacade extends AbstractDestinationFacade
{
private static final Logger logger = CloudLoggerFactory.getLogger(ScpCfDestinationFacade.class);
@Nonnull
private final DestinationService destinationService;
@Nonnull
private final XsuaaService xsuaaService;
/**
* Creates a new default facade.
*/
public ScpCfDestinationFacade()
{
this(new DestinationService(), new XsuaaService());
}
private
Map
parseDestinations( final String responsePayload, final boolean providerTenantUsed )
throws DestinationAccessException
{
final JsonArray destinationConfigurations =
new JsonParser().parse(JsonSanitizer.sanitize(responsePayload)).getAsJsonArray();
final Map destinations = new HashMap<>();
final ScpCfDestinationFactory destinationFactory = new ScpCfDestinationFactory();
for( final JsonElement destinationConfiguration : destinationConfigurations ) {
if( destinationConfiguration.isJsonObject() ) {
final GenericDestination destination =
destinationFactory.create(new ScpCfDestinationParser(destinationConfiguration.getAsJsonObject()));
if( destination instanceof ScpCfDestination ) {
if( logger.isDebugEnabled() ) {
logger.debug(
String.format(
"Cloud Foundry Destination %s is of HTTP type, "
+ "which supports provider/subscriber retrieval strategies.",
destination.getName()));
}
((ScpCfDestination) destination).setUsingProviderTenant(providerTenantUsed);
} else {
if( logger.isDebugEnabled() ) {
logger.debug(
String.format(
"Cloud Foundry Destination %s is of non-HTTP type. "
+ "Provider/subscriber retrieval strategies are not supported, "
+ "meaning only the subscriber's destinations will be retrieved!",
destination.getName()));
}
}
if( logger.isDebugEnabled() ) {
logger.debug(
String.format(
"Adding %s destination %s.",
destination.getDestinationType().toString(),
destination.getName()));
}
destinations.put(destination.getName(), destination);
} else {
if( logger.isWarnEnabled() ) {
logger.warn("Ignoring destination configuration: not a JSON object.");
}
}
}
return destinations;
}
static final Cache> destinationsCache =
CacheManager.register(
CacheBuilder
.newBuilder()
.concurrencyLevel(10)
.maximumSize(100000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build());
CacheKey getCacheKey()
{
// TODO test isolation
return CacheKey.of(
TenantAccessor.getCurrentTenantIfAvailable().map(Tenant::getTenantId).orElse(null),
UserAccessor.getCurrentUserIfAuthenticated().map(User::getName).orElse(null));
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public Class extends GenericDestination> getGenericDestinationClass()
{
return ScpCfGenericDestination.class;
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public Class extends Destination> getDestinationClass()
{
return ScpCfDestination.class;
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public Class extends RfcDestination> getRfcDestinationClass()
{
return ScpCfRfcDestination.class;
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public Map getGenericDestinationsByName()
throws DestinationAccessException
{
if( getDestinationsFromEnvironmentVariable() != null ) {
if( logger.isWarnEnabled() ) {
logger.warn(
"Environment variable '"
+ AbstractDestinationFacade.VARIABLE_DESTINATIONS
+ "' is set. "
+ "Destinations will only be read from this variable. "
+ "Unset this variable to read destinations from the destination service on SAP Cloud Platform.");
}
return getDestinationsFromEnvironmentVariable(new ScpCfDestinationFactory());
}
final CacheKey cacheKey = getCacheKey();
try {
return destinationsCache.get(cacheKey, () -> {
final Map result = new HashMap<>();
final Map providerDestinations = fetchAllDestinations(true);
final Map subscriberDestinations = fetchAllDestinations(false);
for( final String destinationName : Sets
.union(providerDestinations.keySet(), subscriberDestinations.keySet()) ) {
final GenericDestination chosenDestination =
chooseDestinationByStrategy(
destinationName,
providerDestinations.get(destinationName),
subscriberDestinations.get(destinationName));
if( chosenDestination != null ) {
result.put(destinationName, chosenDestination);
}
}
return result;
});
}
catch( final ExecutionException | UncheckedExecutionException e ) {
throw new DestinationAccessException(e.getMessage(), e);
}
}
@Nonnull
private Map fetchAllDestinations( final boolean useProviderTenant )
throws DestinationAccessException
{
final DestinationServiceCommand subaccountDestinationCommand =
new DestinationServiceCommand(
xsuaaService,
false,
destinationService,
"subaccountDestinations",
useProviderTenant);
final DestinationServiceCommand instanceDestinationCommand =
new DestinationServiceCommand(
xsuaaService,
false,
destinationService,
"instanceDestinations",
useProviderTenant);
final Map result = new HashMap<>();
try {
final Future subaccountDestinations = subaccountDestinationCommand.queue();
final Future instanceDestinations = instanceDestinationCommand.queue();
result.putAll(parseDestinations(subaccountDestinations.get(), useProviderTenant));
result.putAll(parseDestinations(instanceDestinations.get(), useProviderTenant));
}
catch( final ExecutionException | UncheckedExecutionException | InterruptedException e ) {
// quick noop test
final String tenantString = useProviderTenant ? "Provider" : "Subscriber";
if( logger.isDebugEnabled() ) {
logger.debug(
"Failed to fetch any destination for the "
+ tenantString
+ " Tenant. See the following stacktrace for more information.",
buildDestinationAccessException(
subaccountDestinationCommand,
instanceDestinationCommand,
useProviderTenant,
e));
} else {
logger.info(
"Failed to fetch any destination for the "
+ tenantString
+ " Tenant. This is not necessarily a problem on it's own. However, if you expect to retrieve destinations from the "
+ tenantString
+ " set your log level to DEBUG to see the causing stack trace.");
}
}
return result;
}
@Nullable
private GenericDestination chooseDestinationByStrategy(
@Nonnull final String destinationName,
@Nullable final GenericDestination providerDestination,
@Nullable final GenericDestination subscriberDestination )
{
switch( DestinationAccessor.getRetrievalStrategy(destinationName) ) {
case PLATFORM_DEFAULT:
case ALWAYS_SUBSCRIBER: {
return subscriberDestination;
}
case ALWAYS_PROVIDER: {
return providerDestination;
}
case SUBSCRIBER_THEN_PROVIDER: {
if( subscriberDestination != null ) {
return subscriberDestination;
} else {
return providerDestination;
}
}
}
return null;
}
@Nonnull
private DestinationAccessException buildDestinationAccessException(
@Nonnull final DestinationServiceCommand subaccountDestinationCommand,
@Nonnull final DestinationServiceCommand instanceDestinationCommand,
final boolean providerTenantUsed,
@Nonnull final Throwable exceptionThrown )
{
final DestinationServiceCommand failedCommand =
subaccountDestinationCommand.isFailedExecution()
? subaccountDestinationCommand
: instanceDestinationCommand.isFailedExecution() ? instanceDestinationCommand : null;
final String message;
final String reason;
if( failedCommand != null ) {
message =
String.format(
"Failed to get destinations of %s %s",
providerTenantUsed ? "provider" : "subscriber",
instanceDestinationCommand.isFailedExecution() ? "service instance" : "");
reason = failedCommand.getExecutionException().getMessage();
} else {
message = "Failed to get destinations";
reason =
exceptionThrown.getCause() != null
? exceptionThrown.getCause().getMessage()
: exceptionThrown.getMessage();
}
return new DestinationAccessException(
message
+ (reason == null ? "." : ": " + (reason.endsWith(".") ? reason : reason + "."))
+ " If your application is running on Cloud Foundry, "
+ "make sure to have a binding to both the destination "
+ "service and the authorization and trust management (xsuaa) "
+ "service, AND that you either properly secured your "
+ "application or have set the '"
+ ScpCfUserFacade.VARIABLE_ALLOW_MOCKED_AUTH_HEADER
+ "' environment variable to true. "
+ "Please note that authentication types with user propagation, "
+ "for example, principal propagation or the OAuth2 SAML Bearer flow, "
+ "require that you secure your application and will not work when using the '"
+ ScpCfUserFacade.VARIABLE_ALLOW_MOCKED_AUTH_HEADER
+ "' environment variable. "
+ "If your application is not running on Cloud Foundry, "
+ "for example, when deploying to a local container, "
+ "consider declaring the '"
+ VARIABLE_DESTINATIONS
+ "' environment variable to configure destinations.",
exceptionThrown);
}
}