
com.sap.cloud.sdk.cloudplatform.connectivity.ScpCfDestination 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.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.commons.io.FilenameUtils;
import org.apache.http.HttpHeaders;
import org.slf4j.Logger;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.json.JsonSanitizer;
import com.netflix.hystrix.exception.HystrixBadRequestException;
import com.netflix.hystrix.exception.HystrixRuntimeException;
import com.sap.cloud.sdk.cloudplatform.CloudPlatform;
import com.sap.cloud.sdk.cloudplatform.CloudPlatformAccessor;
import com.sap.cloud.sdk.cloudplatform.ScpCfCloudPlatform;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
import com.sap.cloud.sdk.cloudplatform.exception.CloudPlatformException;
import com.sap.cloud.sdk.cloudplatform.exception.ShouldNotHappenException;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.cloudplatform.security.AuthToken;
import com.sap.cloud.sdk.cloudplatform.security.AuthTokenAccessor;
import com.sap.cloud.sdk.cloudplatform.security.BasicCredentials;
import lombok.AccessLevel;
import lombok.Setter;
/**
* Implementation of {@link Destination} on SAP Cloud Platform Cloud Foundry.
*
* @see ScpCfGenericDestination
* @see ScpCfRfcDestination
*/
public class ScpCfDestination extends AbstractDestination
{
private static final Logger logger = CloudLoggerFactory.getLogger(ScpCfDestination.class);
private static final String ON_PREMISE_PROXY_HOST = "onpremise_proxy_host";
private static final String ON_PREMISE_PROXY_PORT = "onpremise_proxy_port";
private static final String SAP_CONNECTIVITY_SCC_LOCATION_ID_HEADER = "SAP-Connectivity-SCC-Location_ID";
private static final String BEARER_PREFIX = "Bearer ";
// @formatter:off
private static final Map KEY_STORE_TYPE_BY_FILE_EXT = ImmutableMap.of(
"pfx", "PKCS12",
"p12", "PKCS12",
"jks", "JKS",
"cer", "CERT"
);
// @formatter:on
@Nullable
private final String cloudConnectorLocationId;
private final transient AtomicReference destinationCache = new AtomicReference<>(null);
private final transient AtomicReference> onPremiseProxyHeadersCache = new AtomicReference<>(null);
private final transient AtomicReference trustStoreCache = new AtomicReference<>(null);
private final transient AtomicReference keyStoreCache = new AtomicReference<>(null);
@Nonnull
private final XsuaaService xsuaaService;
@Nonnull
private final DestinationService destinationService;
@Nonnull
private final ConnectivityService connectivityService;
@Setter( AccessLevel.PACKAGE )
private boolean usingProviderTenant = false;
/**
* Creates a destination to be used on SAP Cloud Platform Cloud Foundry.
*
* @param destinationAsJson
* The actual JSON object returned by the destination service.
* @param xsuaaService
* The XSUAA service to be used to retrieve further destination information such as headers for
* on-premise connections.
* @param destinationService
* The destinations service to be used to retrieve further destination information.
* @param connectivityService
* The connectivity service to be used to retrieve further destination information regarding on-premise
* connections.
* @param name
* The name of the destination.
* @param description
* A description of this destination.
* @param uri
* The uri of this destination.
* @param authenticationType
* The {@code AuthenticationType} of this destination.
* @param basicCredentials
* The credentials to be used if {@code authenticationType} is set to {@code BASIC_AUTHENTICATION}.
* @param proxyType
* The type of proxy to be used for this destination.
* @param proxyConfiguration
* The configuration of the proxy to be used (if given).
* @param isTrustingAllCertificates
* Flag indicating whether all certificates should be accepted when communicating with the destination.
* @param trustStoreLocation
* The name of the trust store to search for in the certificates obtained by the destination service.
* @param trustStorePassword
* The password to access the trust store.
* @param keyStoreLocation
* The name of the key store to search for in the certificates obtained by the destination service.
* @param keyStorePassword
* The password to access the key store.
* @param cloudConnectorLocationId
* The id to be used when communicating in {@code ON_PREMISE} {@code proxyType} with an on-premise
* system.
* @param propertiesByName
* A map containing all additional properties.
*/
public ScpCfDestination(
@Nullable final JsonObject destinationAsJson,
@Nonnull final XsuaaService xsuaaService,
@Nonnull final DestinationService destinationService,
@Nonnull final ConnectivityService connectivityService,
@Nonnull final String name,
@Nullable final String description,
@Nonnull final URI uri,
@Nonnull final AuthenticationType authenticationType,
@Nullable final BasicCredentials basicCredentials,
@Nonnull final ProxyType proxyType,
@Nullable final ProxyConfiguration proxyConfiguration,
final boolean isTrustingAllCertificates,
@Nullable final String trustStoreLocation,
@Nullable final String trustStorePassword,
@Nullable final String keyStoreLocation,
@Nullable final String keyStorePassword,
@Nullable final String cloudConnectorLocationId,
@Nonnull final Map propertiesByName )
{
this(
destinationAsJson,
xsuaaService,
destinationService,
connectivityService,
name,
description,
uri.toString(),
authenticationType,
basicCredentials,
proxyType,
proxyConfiguration,
isTrustingAllCertificates,
trustStoreLocation,
trustStorePassword,
keyStoreLocation,
keyStorePassword,
cloudConnectorLocationId,
propertiesByName);
}
/**
* Creates a destination to be used on SAP Cloud Platform Cloud Foundry.
*
* @param destinationAsJson
* The actual JSON object returned by the destination service.
* @param xsuaaService
* The XSUAA service to be used to retrieve further destination information such as headers for
* on-premise connections.
* @param destinationService
* The destinations service to be used to retrieve further destination information.
* @param connectivityService
* The connectivity service to be used to retrieve further destination information regarding on-premise
* connections.
* @param name
* The name of the destination.
* @param description
* A description of this destination.
* @param uri
* The uri of this destination.
* @param authenticationType
* The {@code AuthenticationType} of this destination.
* @param basicCredentials
* The credentials to be used if {@code authenticationType} is set to {@code BASIC_AUTHENTICATION}.
* @param proxyType
* The type of proxy to be used for this destination.
* @param proxyConfiguration
* The configuration of the proxy to be used (if given).
* @param isTrustingAllCertificates
* Flag indicating whether all certificates should be accepted when communicating with the destination.
* @param trustStoreLocation
* The name of the trust store to search for in the certificates obtained by the destination service.
* @param trustStorePassword
* The password to access the trust store.
* @param keyStoreLocation
* The name of the key store to search for in the certificates obtained by the destination service.
* @param keyStorePassword
* The password to access the key store.
* @param cloudConnectorLocationId
* The id to be used when communicating in {@code ON_PREMISE} {@code proxyType} with an on-premise
* system.
* @param propertiesByName
* A map containing all additional properties.
*/
public ScpCfDestination(
@Nullable final JsonObject destinationAsJson,
@Nonnull final XsuaaService xsuaaService,
@Nonnull final DestinationService destinationService,
@Nonnull final ConnectivityService connectivityService,
@Nonnull final String name,
@Nullable final String description,
@Nonnull final String uri,
@Nonnull final AuthenticationType authenticationType,
@Nullable final BasicCredentials basicCredentials,
@Nonnull final ProxyType proxyType,
@Nullable final ProxyConfiguration proxyConfiguration,
final boolean isTrustingAllCertificates,
@Nullable final String trustStoreLocation,
@Nullable final String trustStorePassword,
@Nullable final String keyStoreLocation,
@Nullable final String keyStorePassword,
@Nullable final String cloudConnectorLocationId,
@Nonnull final Map propertiesByName )
{
super(
name,
description,
uri,
authenticationType,
basicCredentials,
proxyType,
proxyConfiguration,
isTrustingAllCertificates,
trustStoreLocation,
trustStorePassword,
keyStoreLocation,
keyStorePassword,
propertiesByName);
this.xsuaaService = xsuaaService;
this.destinationService = destinationService;
this.connectivityService = connectivityService;
this.cloudConnectorLocationId = cloudConnectorLocationId;
if( destinationAsJson != null ) {
destinationCache.compareAndSet(null, destinationAsJson);
}
}
/**
* Creates a mocked {@link ScpCfDestination} returning an empty destination name.
*
* This no-arguments constructor is required to ensure compatibility with mocking frameworks such as Mockito.
*/
private ScpCfDestination()
{
this(
null,
new XsuaaService(),
new DestinationService(),
new ConnectivityService(),
"",
null,
"",
AuthenticationType.NO_AUTHENTICATION,
null,
ProxyType.INTERNET,
null,
false,
null,
null,
null,
null,
null,
Collections.emptyMap());
}
@Nonnull
@Override
public Optional getTrustStore()
throws DestinationAccessException
{
if( trustStoreLocation != null && trustStorePassword != null && trustStoreCache.get() == null ) {
trustStoreCache.compareAndSet(null, getKeyStore(trustStoreLocation, trustStorePassword));
}
return Optional.ofNullable(trustStoreCache.get());
}
@Nonnull
@Override
public Optional getKeyStore()
throws DestinationAccessException
{
if( keyStoreLocation != null && keyStorePassword != null && keyStoreCache.get() == null ) {
keyStoreCache.compareAndSet(null, getKeyStore(keyStoreLocation, keyStorePassword));
}
return Optional.ofNullable(keyStoreCache.get());
}
/**
* Getter for the location identifier used by the SAP Cloud Connector.
*
* @return The location identifier.
*/
@Nonnull
public Optional getCloudConnectorLocationId()
{
return Optional.ofNullable(cloudConnectorLocationId);
}
private JsonObject fetchDestination()
throws DestinationAccessException
{
final String servicePath = "destinations/" + getName();
final boolean propagateUser = authenticationTypeRequiresUserPropagation();
try {
final String responsePayload =
new DestinationServiceCommand(
xsuaaService,
propagateUser,
destinationService,
servicePath,
usingProviderTenant).execute();
return new JsonParser().parse(JsonSanitizer.sanitize(responsePayload)).getAsJsonObject();
}
catch( final
HystrixRuntimeException
| HystrixBadRequestException
| IllegalStateException
| JsonParseException e ) {
throw new DestinationAccessException(
"Failed to access the configuration of destination '" + getName() + "'.",
e);
}
}
private boolean authenticationTypeRequiresUserPropagation()
{
return (authenticationType == AuthenticationType.OAUTH2_SAML_BEARER_ASSERTION)
|| (authenticationType == AuthenticationType.OAUTH2_USER_TOKEN_EXCHANGE);
}
private JsonObject getOrFetchDestination()
throws DestinationAccessException
{
if( destinationCache.get() == null ) {
destinationCache.compareAndSet(null, fetchDestination());
}
return destinationCache.get();
}
private boolean hasCertificateType( @Nullable final JsonElement type )
{
return type != null && type.isJsonPrimitive() && type.getAsString().equals("CERTIFICATE");
}
private boolean hasCertificateName( @Nonnull final String keyStoreLocation, @Nullable final JsonElement name )
{
return name != null && name.isJsonPrimitive() && name.getAsString().equals(keyStoreLocation);
}
private boolean hasCertificateContent( @Nullable final JsonElement content )
{
return content != null && content.isJsonPrimitive();
}
private KeyStore getKeyStore( @Nonnull final String keyStoreLocation, @Nullable final String password )
throws DestinationAccessException
{
@Nullable
final JsonElement certificatesElement = getOrFetchDestination().get("certificates");
if( certificatesElement != null && certificatesElement.isJsonArray() ) {
for( final JsonElement certificate : certificatesElement.getAsJsonArray() ) {
if( certificate.isJsonObject() ) {
final JsonObject cert = certificate.getAsJsonObject();
@Nullable
final JsonElement type = cert.get("Type");
@Nullable
final JsonElement name = cert.get("Name");
@Nullable
final JsonElement content = cert.get("Content");
if( hasCertificateType(type)
&& hasCertificateName(keyStoreLocation, name)
&& hasCertificateContent(content) ) {
final String fileExtension = FilenameUtils.getExtension(name.getAsString().toLowerCase());
@Nullable
final String typeOfFileExt = KEY_STORE_TYPE_BY_FILE_EXT.get(fileExtension);
final String keyStoreType;
if( typeOfFileExt != null ) {
keyStoreType = typeOfFileExt;
if( logger.isDebugEnabled() ) {
logger.debug(
"Using key store type '"
+ keyStoreType
+ "' based on file extension '"
+ fileExtension
+ "'.");
}
} else {
keyStoreType = KeyStore.getDefaultType();
if( logger.isDebugEnabled() ) {
logger.debug("Using default key store type '" + keyStoreType + "'.");
}
}
final KeyStore ks;
try {
ks = KeyStore.getInstance(keyStoreType);
}
catch( final KeyStoreException e ) {
throw new DestinationAccessException("Failed to load key store.", e);
}
final byte[] bytes = Base64.getDecoder().decode(content.getAsString());
try( final ByteArrayInputStream is = new ByteArrayInputStream(bytes) ) {
ks.load(is, password == null ? null : password.toCharArray());
return ks;
}
catch( final IOException | CertificateException | NoSuchAlgorithmException e ) {
throw new DestinationAccessException("Failed to load key store.", e);
}
}
}
}
}
throw new DestinationAccessException("Failed to find key store '" + keyStoreLocation + "'.");
}
private ScpCfCloudPlatform getCloudPlatform()
{
final CloudPlatform cloudPlatform = CloudPlatformAccessor.getCloudPlatform();
if( !(cloudPlatform instanceof ScpCfCloudPlatform) ) {
throw new ShouldNotHappenException(
"The current Cloud platform is not an instance of "
+ ScpCfCloudPlatform.class.getSimpleName()
+ ". Please make sure to specify a dependency to com.sap.cloud.s4hana.cloudplatform:core-scp-cf.");
}
return (ScpCfCloudPlatform) cloudPlatform;
}
private ProxyConfiguration getOnPremiseProxyConfiguration()
throws DestinationAccessException
{
String proxyHost = null;
Integer proxyPort = null;
final JsonObject connectivityServiceCredentials;
try {
connectivityServiceCredentials = getCloudPlatform().getConnectivityServiceCredentials();
}
catch( final CloudPlatformException e ) {
throw new DestinationAccessException(e);
}
@Nullable
final JsonElement onPremiseHost = connectivityServiceCredentials.get(ON_PREMISE_PROXY_HOST);
if( onPremiseHost != null && onPremiseHost.isJsonPrimitive() ) {
proxyHost = onPremiseHost.getAsString();
}
@Nullable
final JsonElement onPremisePort = connectivityServiceCredentials.get(ON_PREMISE_PROXY_PORT);
if( onPremisePort != null && onPremisePort.isJsonPrimitive() ) {
try {
proxyPort = Integer.valueOf(onPremisePort.getAsString());
}
catch( final NumberFormatException e ) {
if( logger.isWarnEnabled() ) {
logger.warn("Failed to parse on-premise port.", e);
}
proxyPort = null;
}
}
if( proxyHost == null || proxyPort == null ) {
throw new DestinationAccessException(
"Failed to configure on-premise proxy for destination '"
+ getName()
+ "'. Please make sure to correctly bind your application to a service instance.");
}
try {
final URI uri = new URI("http://" + proxyHost + ":" + proxyPort);
final ProxyConfiguration proxyConfiguration = new ProxyConfiguration(uri);
if( logger.isDebugEnabled() ) {
logger.debug("Using on-premise proxy configuration: " + proxyConfiguration + ".");
}
return proxyConfiguration;
}
catch( final URISyntaxException e ) {
throw new DestinationAccessException("Invalid proxy URI.", e);
}
}
@Nonnull
@Override
public Optional getProxyConfiguration()
throws DestinationAccessException
{
if( getProxyType() == ProxyType.ON_PREMISE ) {
return Optional.of(getOnPremiseProxyConfiguration());
} else {
return Optional.ofNullable(proxyConfiguration);
}
}
private List getAppToAppSsoHeaders()
throws DestinationAccessException
{
final List result = new ArrayList<>();
final Optional currentJwt = AuthTokenAccessor.getCurrentToken();
if( currentJwt.isPresent() ) {
final DecodedJWT jwt = currentJwt.get().getJwt();
result.add(new Header(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + jwt.getToken()));
} else {
logger.error(
"Failed to add '"
+ HttpHeaders.AUTHORIZATION
+ "' header for app-to-app single sign-on: no JWT bearer found in '"
+ HttpHeaders.AUTHORIZATION
+ "' header of request. Continuing without header.");
}
return result;
}
private List getAuthTokenHeaders()
throws DestinationAccessException
{
final List result = new ArrayList<>();
@Nullable
final JsonElement authTokensElement = getOrFetchDestination().get("authTokens");
if( authTokensElement != null && authTokensElement.isJsonArray() ) {
final JsonArray authTokens = authTokensElement.getAsJsonArray();
for( final JsonElement authToken : authTokens ) {
if( authToken.isJsonObject() ) {
final JsonObject token = authToken.getAsJsonObject();
@Nullable
final JsonElement error = token.get("error");
if( error != null ) {
throw new DestinationAccessException(
"Failed to get authentication headers. "
+ "Destination service returned error"
+ (error.isJsonPrimitive()
? ": " + error.getAsString() + (error.getAsString().endsWith(".") ? "" : ".")
: "."));
}
@Nullable
final JsonElement type = token.get("type");
@Nullable
final JsonElement value = token.get("value");
if( type != null && type.isJsonPrimitive() && value != null && value.isJsonPrimitive() ) {
result
.add(new Header(HttpHeaders.AUTHORIZATION, type.getAsString() + " " + value.getAsString()));
}
}
}
}
return result;
}
private List getOnPremiseProxyHeaders()
throws DestinationAccessException
{
if( onPremiseProxyHeadersCache.get() == null ) {
final List headers = new ArrayList<>();
if( !Strings.isNullOrEmpty(cloudConnectorLocationId) ) {
headers.add(new Header(SAP_CONNECTIVITY_SCC_LOCATION_ID_HEADER, cloudConnectorLocationId));
if( logger.isDebugEnabled() ) {
logger.debug(
"Successfully added "
+ SAP_CONNECTIVITY_SCC_LOCATION_ID_HEADER
+ " header with location identifier '"
+ cloudConnectorLocationId
+ "'.");
}
}
try {
headers.addAll(
new GetOnPremiseProxyHeadersCommand(
xsuaaService,
connectivityService,
usingProviderTenant,
authenticationTypeRequiresUserPropagation()).execute());
}
catch( final HystrixRuntimeException | HystrixBadRequestException | IllegalStateException e ) {
throw new DestinationAccessException(
"Failed to get on-premise headers for destination '" + getName() + "'.",
e);
}
onPremiseProxyHeadersCache.compareAndSet(null, headers);
}
return onPremiseProxyHeadersCache.get();
}
@Nonnull
@Override
public List getHeaders( @Nullable final URI requestUri )
throws DestinationAccessException
{
final List headers = super.getHeaders(requestUri);
final AuthenticationType authenticationType = getAuthenticationType();
if( authenticationType == AuthenticationType.APP_TO_APP_SSO ) {
headers.addAll(getAppToAppSsoHeaders());
} else if( authenticationType != AuthenticationType.NO_AUTHENTICATION
&& authenticationType != AuthenticationType.BASIC_AUTHENTICATION ) {
headers.addAll(getAuthTokenHeaders());
}
if( ProxyType.ON_PREMISE == getProxyType() ) {
headers.addAll(getOnPremiseProxyHeaders());
}
return headers;
}
}