Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sap.cloud.sdk.cloudplatform.connectivity.ScpCfService Maven / Gradle / Ivy
Go to download
Implementation of the Cloud platform abstraction for general-purpose connectivity
on the SAP Cloud Platform (Cloud Foundry).
/*
* Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved.
*/
package com.sap.cloud.sdk.cloudplatform.connectivity;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.slf4j.Logger;
import com.google.common.annotations.Beta;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.sap.cloud.sdk.cloudplatform.ScpCfServiceDesignator;
import com.sap.cloud.sdk.cloudplatform.ScpCfServiceInfo;
import com.sap.cloud.sdk.cloudplatform.cache.CacheKey;
import com.sap.cloud.sdk.cloudplatform.cache.CacheManager;
import com.sap.cloud.sdk.cloudplatform.exception.CloudPlatformException;
import com.sap.cloud.sdk.cloudplatform.exception.NoServiceBindingException;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.cloudplatform.security.BasicCredentials;
import com.sap.cloud.sdk.cloudplatform.security.exception.TokenRequestDeniedException;
import com.sap.cloud.sdk.cloudplatform.security.exception.TokenRequestFailedException;
import com.sap.cloud.sdk.cloudplatform.tenant.exception.TenantAccessException;
import com.sap.cloud.sdk.cloudplatform.tenant.exception.TenantNotAvailableException;
/**
* A facilitation of access to the SAP CP services with OAuth auth listed in VCAP_SERVICES.
*
*
* Be aware that this class is in Beta (as indicated by the annotation) and therefore subject to breaking changes.
*
*/
@Beta
public class ScpCfService
{
private static final Logger logger = CloudLoggerFactory.getSanitizedLogger(ScpCfService.class);
private static final String AUTHORIZATION_HEADER = HttpHeaders.AUTHORIZATION;
private static final String BEARER_PREFIX = "Bearer ";
private static final CacheBuilder tokenCacheBuilder =
CacheBuilder.newBuilder().concurrencyLevel(10).maximumSize(100000).expireAfterAccess(60, TimeUnit.MINUTES);
private static final Cache tokenCache = CacheManager.register(tokenCacheBuilder.build());
private final ScpCfServiceInfo serviceInfo;
private final URI authUri;
private final String serviceLocationInfo;
private final BasicCredentials clientCredentials;
private final boolean useProviderTenant;
private final SubdomainReplacer subdomainReplacer;
/**
* Constructor open for extension.
*
* @param serviceInfo
* json from the VCAP_SERVICES
* @param authUri
* OAuth authentication service
* @param clientCredentials
* credentials for authentications to OAuth service
* @param serviceLocationInfo
* url or host of the service represented by this object
*/
public ScpCfService(
@Nonnull final ScpCfServiceInfo serviceInfo,
@Nonnull final URI authUri,
@Nonnull final BasicCredentials clientCredentials,
@Nonnull final String serviceLocationInfo )
{
this(serviceInfo, authUri, clientCredentials, serviceLocationInfo, false);
}
/**
* Constructor open for extension.
*
* @param serviceInfo
* json from the VCAP_SERVICES
* @param authUri
* OAuth authentication service
* @param clientCredentials
* credentials for authentications to OAuth service
* @param serviceLocationInfo
* url or host of the service represented by this object
* @param useProviderTenant
* for applications that other tenants can subscribe to, this flag controls whether to always use the
* service with the credentials of the provider tenant.
*/
public ScpCfService(
@Nonnull final ScpCfServiceInfo serviceInfo,
@Nonnull final URI authUri,
@Nonnull final BasicCredentials clientCredentials,
@Nonnull final String serviceLocationInfo,
final boolean useProviderTenant )
{
this(
serviceInfo,
authUri,
clientCredentials,
serviceLocationInfo,
useProviderTenant,
new DefaultSubdomainReplacer());
}
/**
* Constructor open for extension.
*
* @param serviceInfo
* json from the VCAP_SERVICES
* @param authUri
* OAuth authentication service
* @param clientCredentials
* credentials for authentications to OAuth service
* @param serviceLocationInfo
* url or host of the service represented by this object
* @param useProviderTenant
* for applications that other tenants can subscribe to, this flag controls whether to always use the
* service with the credentials of the provider tenant.
* @param subdomainReplacer
* if you want to override the subdomain-replacing behavior (useful for testing)
*/
public ScpCfService(
@Nonnull final ScpCfServiceInfo serviceInfo,
@Nonnull final URI authUri,
@Nonnull final BasicCredentials clientCredentials,
@Nonnull final String serviceLocationInfo,
final boolean useProviderTenant,
final SubdomainReplacer subdomainReplacer )
{
this.serviceInfo = serviceInfo;
this.authUri = authUri;
this.serviceLocationInfo = serviceLocationInfo;
this.clientCredentials = clientCredentials;
this.useProviderTenant = useProviderTenant;
this.subdomainReplacer = subdomainReplacer;
}
/**
* Factory method reading information about service from VCAP_SERVICES. Takes paths in the form of
* 'credentials/clientId' to make information extraction generic.
*
* Doesn't validate the connection to the auth server or service itself yet.
*
* @param designator
* service designator indicatig the service information to use
* @param authUrlJsonPath
* path to the OAuth server URL.
* @param clientIdJsonPath
* path to client id
* @param clientSecretJsonPath
* path to client secret
* @param serviceLocationJsonPath
* path to the service's url or host information or similar
*
* @return The created service.
*
* @throws CloudPlatformException
* On problems like the service type not being present in VCAP_SERVICES.
* @throws ScpCfServiceCreationFailedException
* If the service creation is otherwise incomplete or incorrect.
*/
@Nonnull
public static ScpCfService of(
@Nonnull final ScpCfServiceDesignator designator,
@Nonnull final String authUrlJsonPath,
@Nonnull final String clientIdJsonPath,
@Nonnull final String clientSecretJsonPath,
@Nonnull final String serviceLocationJsonPath )
throws CloudPlatformException,
ScpCfServiceCreationFailedException
{
return ofMultiTenant(
designator,
authUrlJsonPath,
clientIdJsonPath,
clientSecretJsonPath,
serviceLocationJsonPath,
false,
new DefaultSubdomainReplacer());
}
/**
* Factory method for testing.
*/
static ScpCfService of(
@Nonnull final String serviceType,
@Nullable final String servicePlan,
@Nonnull final String authUrlJsonPath,
@Nonnull final String clientIdJsonPath,
@Nonnull final String clientSecretJsonPath,
@Nonnull final String serviceLocationJsonPath,
@Nonnull final SubdomainReplacer subdomainReplacer )
throws CloudPlatformException
{
return ofMultiTenant(
ScpCfServiceDesignator.builder().serviceType(serviceType).servicePlan(servicePlan).build(),
authUrlJsonPath,
clientIdJsonPath,
clientSecretJsonPath,
serviceLocationJsonPath,
false,
subdomainReplacer);
}
/**
* Factory method reading information about service from VCAP_SERVICES. Takes paths in the form of
* 'credentials/clientId' to make information extraction generic.
*
* Doesn't validate the connection to the auth server or service itself yet.
*
* @param serviceType
* classification of services
* @param servicePlan
* optional service plan identifier to distinguish different instances of the same service type.
* @param authUrlJsonPath
* path to the OAuth server URL.
* @param clientIdJsonPath
* path to client id
* @param clientSecretJsonPath
* path to client secret
* @param serviceLocationJsonPath
* path to the service's url or host information or similar
*
* @return The created service.
*
* @throws CloudPlatformException
* On problems like the service type not being present in VCAP_SERVICES.
* @throws ScpCfServiceCreationFailedException
* If the service creation is otherwise incomplete or incorrect.
*/
@Nonnull
public static ScpCfService of(
@Nonnull final String serviceType,
@Nullable final String servicePlan,
@Nonnull final String authUrlJsonPath,
@Nonnull final String clientIdJsonPath,
@Nonnull final String clientSecretJsonPath,
@Nonnull final String serviceLocationJsonPath )
throws CloudPlatformException,
ScpCfServiceCreationFailedException
{
return ofMultiTenant(
serviceType,
servicePlan,
authUrlJsonPath,
clientIdJsonPath,
clientSecretJsonPath,
serviceLocationJsonPath,
false);
}
/**
* Factory method reading information about service from VCAP_SERVICES. Takes paths in the form of
* 'credentials/clientId' to make information extraction generic.
*
* Doesn't validate the connection to the auth server or service itself yet.
*
* This one takes into account multi-tenant subscription environments and allows for always using the provider
* tenant when accessing the service.
*
* @param serviceType
* classification of services
* @param servicePlan
* optional service plan identifier to distinguish different instances of the same service type.
* @param authUrlJsonPath
* path to the OAuth server URL.
* @param clientIdJsonPath
* path to client id
* @param clientSecretJsonPath
* path to client secret
* @param serviceLocationJsonPath
* path to the service's url or host information or similar
* @param useProviderTenant
* for applications that other tenants can subscribe to, this flag controls whether to always use the
* service with the credentials ofMultiTenant the provider tenant.
* @return the created service.
* @throws CloudPlatformException
* On problems like the service type not being present in VCAP_SERVICES.
* @throws ScpCfServiceCreationFailedException
* If the service creation is otherwise incomplete or incorrect.
*/
@Nonnull
public static ScpCfService ofMultiTenant(
@Nonnull final String serviceType,
@Nullable final String servicePlan,
@Nonnull final String authUrlJsonPath,
@Nonnull final String clientIdJsonPath,
@Nonnull final String clientSecretJsonPath,
@Nonnull final String serviceLocationJsonPath,
final boolean useProviderTenant )
throws CloudPlatformException,
ScpCfServiceCreationFailedException
{
return ofMultiTenant(
ScpCfServiceDesignator.builder().serviceType(serviceType).servicePlan(servicePlan).build(),
authUrlJsonPath,
clientIdJsonPath,
clientSecretJsonPath,
serviceLocationJsonPath,
useProviderTenant,
new DefaultSubdomainReplacer());
}
/**
* Factory method reading information about service from VCAP_SERVICES. Takes paths in the form of
* 'credentials/clientId' to make information extraction generic.
*
* Doesn't validate the connection to the auth server or service itself yet.
*
* This one takes into account multi-tenant subscription environments and allows for always using the provider
* tenant when accessing the service.
*
* @param designator
* service designator indicatig the service information to use
* @param authUrlJsonPath
* path to the OAuth server URL.
* @param clientIdJsonPath
* path to client id
* @param clientSecretJsonPath
* path to client secret
* @param serviceLocationJsonPath
* path to the service's url or host information or similar
* @param useProviderTenant
* for applications that other tenants can subscribe to, this flag controls whether to always use the
* service with the credentials ofMultiTenant the provider tenant.
* @return the created service.
* @throws CloudPlatformException
* On problems like the service type not being present in VCAP_SERVICES and if the service information
* is otherwise incomplete or incorrect.
*/
@Nonnull
public static ScpCfService ofMultiTenant(
@Nonnull final ScpCfServiceDesignator designator,
@Nonnull final String authUrlJsonPath,
@Nonnull final String clientIdJsonPath,
@Nonnull final String clientSecretJsonPath,
@Nonnull final String serviceLocationJsonPath,
final boolean useProviderTenant,
@Nonnull final SubdomainReplacer subdomainReplacer )
throws CloudPlatformException
{
try {
final List serviceInstances = ScpCfServiceInfo.createFor(designator);
if( serviceInstances.isEmpty() ) {
throw new NoServiceBindingException(String.format("No service designated by %s bound.", designator));
}
final ScpCfServiceInfo serviceInfo = serviceInstances.get(0);
if( serviceInstances.size() > 1 ) {
logger.warn(
"Found more than one instance of {}, using the first: {}",
createForServiceDesignatorString(designator),
serviceInfo.getServiceName());
} else {
logger.debug("Setting up service {}", serviceInfo.getServiceName());
}
final String serviceLocationInfo =
serviceInfo.getServiceInfoPathNonEmptyStringOrThrow(serviceLocationJsonPath, "Service location info");
final BasicCredentials clientCredentials =
new BasicCredentials(
serviceInfo.getServiceInfoPathNonEmptyStringOrThrow(clientIdJsonPath, "Client id"),
serviceInfo.getServiceInfoPathNonEmptyStringOrThrow(clientSecretJsonPath, "Client secret"));
final URI authUrl =
new URI(serviceInfo.getServiceInfoPathNonEmptyStringOrThrow(authUrlJsonPath, "Auth url"));
return new ScpCfService(
serviceInfo,
authUrl,
clientCredentials,
serviceLocationInfo,
useProviderTenant,
subdomainReplacer);
}
catch( final URISyntaxException e ) {
throw new ScpCfServiceCreationFailedException(
String.format(
"Failed to setup access to %s: %s",
createForServiceDesignatorString(designator),
e.getMessage()),
e);
}
}
@Nonnull
private static String createForServiceDesignatorString( @Nonnull final ScpCfServiceDesignator designator )
{
return String.format("service designated by %s", designator);
}
/**
* Provides access to the rest of the service info.
*
* @return an nice wrapper for the json.
*/
@Nonnull
public ScpCfServiceInfo getServiceInfo()
{
return serviceInfo;
}
/**
* Service access.
*
* @return the URL or host name or similar - if present
*/
@Nullable
public String getServiceLocationInfo()
{
return serviceLocationInfo;
}
/**
* The oauth server url from the service config.
*
* @return the url
*/
@Nonnull
public URI getAuthUri()
{
return authUri;
}
/**
*
* @return the client credentials from the service info
*/
@Nonnull
public BasicCredentials getClientCredentials()
{
return clientCredentials;
}
/**
* Configures a provided request to the service to bear required authentication token. Requests authentication from
* the oauth server or uses a cached token.
*
* @param request
* to add authentication token to.
* @return the request for chaining.
* @throws ScpCfServiceAuthorizationFailedException
* if the request was forbidden or failed
*/
@Nonnull
public HttpRequest addBearerTokenHeader( @Nonnull final HttpRequest request )
throws ScpCfServiceAuthorizationFailedException
{
try {
request.setHeader(
ScpCfService.AUTHORIZATION_HEADER,
ScpCfService.BEARER_PREFIX + getServiceToken().getValue());
return request;
}
catch( final TokenRequestFailedException e ) {
throw new ScpCfServiceAuthorizationFailedException(
"Authorization token request failed: " + e.getMessage(),
e);
}
catch( final TokenRequestDeniedException e ) {
throw new ScpCfServiceAuthorizationFailedException(
"Authorization token request was denied: " + e.getMessage(),
e);
}
}
private AccessToken requestAccessToken( final URI xsuaaUri, final BasicCredentials clientCredentials )
throws TokenRequestFailedException,
TokenRequestDeniedException
{
final TokenRequest tokenRequest = new TokenRequest(subdomainReplacer);
return tokenRequest.requestTokenWithClientCredentialsGrant(xsuaaUri, clientCredentials, useProviderTenant);
}
@Nonnull
private CacheKey getCacheKey( @Nonnull final String serviceName, @Nullable final String servicePlan )
throws TokenRequestFailedException
{
final CacheKey cacheKey;
try {
cacheKey = CacheKey.ofTenantIsolation();
}
catch( final TenantNotAvailableException | TenantAccessException e ) {
throw new TokenRequestFailedException("Failed to determine cache key.", e);
}
cacheKey.append(serviceName);
if( servicePlan != null ) {
cacheKey.append(servicePlan);
}
return cacheKey;
}
private AccessToken getServiceToken()
throws TokenRequestFailedException,
TokenRequestDeniedException
{
final CacheKey cacheKey = getCacheKey(getServiceInfo().getServiceType(), getServiceInfo().getServicePlan());
// Note: strict cache synchronization is not required here.
// In the worst case, the token is fetched multiple times and the freshest one is cached.
@Nullable
final AccessToken accessToken = tokenCache.getIfPresent(cacheKey);
if( accessToken != null && accessToken.isValid() ) {
logger.debug(
"Returning access token from cache (cache key: {}, token expiry: {}).",
cacheKey,
accessToken.getExpiry());
return accessToken;
}
logger.debug("Requesting fresh access token for cache key {}.", cacheKey);
final AccessToken freshToken = requestAccessToken(authUri, clientCredentials);
tokenCache.put(cacheKey, freshToken);
return freshToken;
}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public String toString()
{
return "ScpCfService{"
+ "serviceInfo='"
+ getServiceInfo()
+ '\''
+ ", serviceLocationInfo="
+ serviceLocationInfo
+ ", authUri="
+ authUri
+ '}';
}
}