diff --git a/cloudplatform/cloudplatform-connectivity/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DefaultHttpDestination.java b/cloudplatform/cloudplatform-connectivity/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DefaultHttpDestination.java index 73f43aaca..4527e41aa 100644 --- a/cloudplatform/cloudplatform-connectivity/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DefaultHttpDestination.java +++ b/cloudplatform/cloudplatform-connectivity/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DefaultHttpDestination.java @@ -169,7 +169,13 @@ public Collection
getHeaders( @Nonnull final URI requestUri ) final Collection
allHeaders = new ArrayList<>(); allHeaders.addAll(customHeaders); - allHeaders.addAll(getHeadersFromHeaderProviders(requestUri)); + allHeaders + .addAll( + getHeadersFromHeaderProviders( + this, + requestUri, + customHeaderProviders, + headerProvidersFromClassLoading)); allHeaders.addAll(cachedHeadersFromProperties); if( allHeaders.stream().noneMatch(header -> header.getName().equalsIgnoreCase(HttpHeaders.AUTHORIZATION)) ) { allHeaders.addAll(getHeadersForAuthType()); @@ -180,7 +186,11 @@ public Collection
getHeaders( @Nonnull final URI requestUri ) return allHeaders; } - private List
getHeadersFromHeaderProviders( @Nonnull final URI requestUri ) + static List
getHeadersFromHeaderProviders( + @Nonnull final HttpDestination destination, + @Nonnull final URI requestUri, + @Nonnull final List customHeaderProviders, + @Nonnull final List headerProvidersFromClassLoading ) { final List aggregatedHeaderProviders = new ArrayList<>(); aggregatedHeaderProviders.addAll(customHeaderProviders); @@ -189,7 +199,7 @@ private List
getHeadersFromHeaderProviders( @Nonnull final URI requestUr final String msg = "Found these {} destination header providers: {}"; log.debug(msg, aggregatedHeaderProviders.size(), aggregatedHeaderProviders); - final DestinationRequestContext requestContext = new DestinationRequestContext(this, requestUri); + final DestinationRequestContext requestContext = new DestinationRequestContext(destination, requestUri); final List
result = new ArrayList<>(); for( final DestinationHeaderProvider headerProvider : aggregatedHeaderProviders ) { diff --git a/cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/TransparentProxyDestination.java b/cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/TransparentProxyDestination.java new file mode 100644 index 000000000..581f61cea --- /dev/null +++ b/cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/TransparentProxyDestination.java @@ -0,0 +1,690 @@ +package com.sap.cloud.sdk.cloudplatform.connectivity; + +import java.net.URI; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import com.google.common.collect.ImmutableList; +import com.sap.cloud.sdk.cloudplatform.security.AuthTokenAccessor; +import com.sap.cloud.sdk.cloudplatform.security.BasicCredentials; +import com.sap.cloud.sdk.cloudplatform.tenant.TenantAccessor; +import com.sap.cloud.sdk.cloudplatform.util.FacadeLocator; + +import io.vavr.control.Option; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.Accessors; +import lombok.experimental.Delegate; +import lombok.extern.slf4j.Slf4j; + +/** + * Immutable implementation of the {@link HttpDestination} interface. + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/transparent-proxy-for-kubernetes + */ +@Slf4j +public class TransparentProxyDestination implements HttpDestination +{ + static final String DESTINATION_NAME_HEADER_KEY = "x-destination-name"; + static final String FRAGMENT_NAME_HEADER_KEY = "x-fragment-name"; + static final String TENANT_SUBDOMAIN_HEADER_KEY = "x-tenant-subdomain"; + static final String TENANT_ID_HEADER_KEY = "x-tenant-id"; + static final String FRAGMENT_OPTIONAL_HEADER_KEY = "x-fragment-optional"; + static final String TOKEN_SERVICE_TENANT_HEADER_KEY = "x-token-service-tenant"; + static final String CLIENT_ASSERTION_HEADER_KEY = "x-client-assertion"; + static final String CLIENT_ASSERTION_TYPE_HEADER_KEY = "x-client-assertion-type"; + static final String CLIENT_ASSERTION_DESTINATION_NAME_HEADER_KEY = "x-client-assertion-destination-name"; + static final String AUTHORIZATION_HEADER_KEY = "authorization"; + static final String SUBJECT_TOKEN_TYPE_HEADER_KEY = "x-subject-token-type"; + static final String ACTOR_TOKEN_HEADER_KEY = "x-actor-token"; + static final String ACTOR_TOKEN_TYPE_HEADER_KEY = "x-actor-token-type"; + static final String REDIRECT_URI_HEADER_KEY = "x-redirect-uri"; + static final String CODE_VERIFIER_HEADER_KEY = "x-code-verifier"; + static final String CHAIN_NAME_HEADER_KEY = "x-chain-name"; + static final String CHAIN_VAR_SUBJECT_TOKEN_HEADER_KEY = "x-chain-var-subjectToken"; + static final String CHAIN_VAR_SUBJECT_TOKEN_TYPE_HEADER_KEY = "x-chain-var-subjectTokenType"; + static final String CHAIN_VAR_SAML_PROVIDER_DESTINATION_NAME_HEADER_KEY = "x-chain-var-samlProviderDestinationName"; + static final String TENANT_ID_AND_TENANT_SUBDOMAIN_BOTH_PASSED_ERROR_MESSAGE = + "Tenant id and tenant subdomain cannot be passed at the same time."; + + @Delegate + private final DestinationProperties baseProperties; + + @Nonnull + final ImmutableList
customHeaders; + + @Nonnull + @Getter( AccessLevel.PACKAGE ) + private final ImmutableList customHeaderProviders; + + @Nonnull + private final ImmutableList headerProvidersFromClassLoading; + + private TransparentProxyDestination( + @Nonnull final DestinationProperties baseProperties, + @Nullable final List
customHeaders, + @Nullable final List customHeaderProviders ) + { + this.baseProperties = baseProperties; + this.customHeaders = + customHeaders != null ? ImmutableList.
builder().addAll(customHeaders).build() : ImmutableList.of(); + + final Collection headerProvidersFromClassLoading = + FacadeLocator.getFacades(DestinationHeaderProvider.class); + this.headerProvidersFromClassLoading = + ImmutableList. builder().addAll(headerProvidersFromClassLoading).build(); + + this.customHeaderProviders = + customHeaderProviders != null + ? ImmutableList. builder().addAll(customHeaderProviders).build() + : ImmutableList.of(); + + } + + @Nonnull + @Override + public URI getUri() + { + return URI.create(baseProperties.get(DestinationProperty.URI).get()); + } + + @Nonnull + @Override + public Collection
getHeaders( @Nonnull final URI requestUri ) + { + final Collection
allHeaders = new ArrayList<>(); + + allHeaders.addAll(customHeaders); + allHeaders + .addAll( + DefaultHttpDestination + .getHeadersFromHeaderProviders( + this, + requestUri, + customHeaderProviders, + headerProvidersFromClassLoading)); + + // Automatically add tenant id if not already present + TenantAccessor.tryGetCurrentTenant().onSuccess(tenant -> { + if( !containsHeader(allHeaders, TENANT_ID_HEADER_KEY) + && !containsHeader(allHeaders, TENANT_SUBDOMAIN_HEADER_KEY) ) { + allHeaders.add(new Header(TENANT_ID_HEADER_KEY, tenant.getTenantId())); + } + }); + + AuthTokenAccessor.tryGetCurrentToken().onSuccess(token -> { + if( !containsHeader(allHeaders, AUTHORIZATION_HEADER_KEY) ) { + allHeaders.add(new Header(AUTHORIZATION_HEADER_KEY, token.getJwt().getToken())); + } + }); + + return allHeaders; + } + + static boolean containsHeader( final Collection
headers, final String headerName ) + { + return headers.stream().anyMatch(h -> h.getName().equalsIgnoreCase(headerName)); + } + + @Nonnull + @Override + public Option getTlsVersion() + { + return Option.none(); + } + + @Nonnull + @Override + public Option getProxyConfiguration() + { + return Option.none(); + } + + @Nonnull + @Override + public Option getKeyStore() + { + return Option.none(); + } + + @Nonnull + @Override + public Option getKeyStorePassword() + { + return Option.none(); + } + + @Override + public boolean isTrustingAllCertificates() + { + return false; + } + + @Nonnull + @Override + public Option getBasicCredentials() + { + return Option.none(); + } + + @Nonnull + @Override + public AuthenticationType getAuthenticationType() + { + return AuthenticationType.NO_AUTHENTICATION; + } + + @Nonnull + @Override + public Option getProxyType() + { + return Option.none(); + } + + @Nonnull + @Override + public Option getTrustStore() + { + return Option.none(); + } + + @Nonnull + @Override + public Option getTrustStorePassword() + { + return Option.none(); + } + + @Override + public boolean equals( @Nullable final Object o ) + { + if( this == o ) { + return true; + } + + if( o == null || getClass() != o.getClass() ) { + return false; + } + + final TransparentProxyDestination that = (TransparentProxyDestination) o; + return new EqualsBuilder() + .append(baseProperties, that.baseProperties) + .append(customHeaders, that.customHeaders) + .isEquals(); + } + + @Override + public int hashCode() + { + return new HashCodeBuilder(17, 37).append(baseProperties).append(customHeaders).toHashCode(); + } + + /** + * Creates a new builder for a "static" destination. + *

+ * A static destination connects directly to a specified URL and does not use the destination-gateway. It allows + * setting generic headers but does not support gateway-specific properties like destination name or fragments. + * + * @return A new {@link StaticBuilder} instance. + */ + @Nonnull + public static StaticBuilder staticDestination( @Nonnull final String uri ) + { + return new StaticBuilder(uri); + } + + /** + * Creates a new builder for a "dynamic" destination that is resolved via the destination-gateway. + *

+ * A dynamic destination requires a destination name and will be routed through the central destination-gateway. It + * supports all gateway-specific properties like fragments, tenant context, and authentication flows. + * + * @param destinationName + * The name of the destination to be resolved by the gateway. + * @return A new {@link DynamicBuilder} instance. + */ + @Nonnull + public static DynamicBuilder dynamicDestination( @Nonnull final String destinationName, @Nonnull final String uri ) + { + return new DynamicBuilder(destinationName, uri); + } + + /** + * Abstract base class for builders to share common functionality like adding headers and properties. + * + * @param + * The type of the builder subclass, used for fluent method chaining. + */ + @Accessors( fluent = true, chain = true ) + public abstract static class AbstractBuilder> + { + final List

headers = new ArrayList<>(); + final List customHeaderProviders = new ArrayList<>(); + final DefaultDestination.Builder propertiesBuilder = DefaultDestination.builder(); + + /** + * Returns the current builder instance. + *

+ * This method is used to support fluent method chaining in subclasses of {@code AbstractBuilder}. + * + * @return the current builder instance + */ + protected abstract B getThis(); + + /** + * Adds the given key-value pair to the destination to be created. This will overwrite any property already + * assigned to the key. + * + * @param key + * The key to assign a property for. + * @param value + * The property value to be assigned. + * @return This builder. + * @since 5.22.0 + */ + @Nonnull + public B property( @Nonnull final String key, @Nonnull final Object value ) + { + propertiesBuilder.property(key, value); + return getThis(); + } + + /** + * Adds the given key-value pair to the destination to be created. This will overwrite any property already + * assigned to the key. + * + * @param key + * The {@link DestinationPropertyKey} to assign a property for. + * @param value + * The property value to be assigned. + * @param + * The type of the property value. + * @return This builder. + * @since 5.22.0 + */ + @Nonnull + public B property( @Nonnull final DestinationPropertyKey key, @Nonnull final ValueT value ) + { + return property(key.getKeyName(), value); + } + + /** + * Adds the given headers to the list of headers added to every outgoing request for this destination. + * + * @param headers + * Headers to add to outgoing requests. + * @return This builder. + */ + @Nonnull + public B headers( @Nonnull final Collection

headers ) + { + this.headers.addAll(headers); + return getThis(); + } + + /** + * Adds the given header to the list of headers added to every outgoing request for this destination. + * + * @param header + * A header to add to outgoing requests. + * @return This builder. + */ + @Nonnull + public B header( @Nonnull final Header header ) + { + this.headers.add(header); + return getThis(); + } + + /** + * Adds a header given by the {@code headerName} and {@code headerValue} to the list of headers added to every + * outgoing request for this destination. + * + * @param headerName + * The name of the header to add. + * @param headerValue + * The value of the header to add. + * @return This builder. + */ + @Nonnull + public B header( @Nonnull final String headerName, @Nonnull final String headerValue ) + { + return header(new Header(headerName, headerValue)); + } + + /** + * Adds a tenant subdomain header to the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/multitenancy + *

+ * Note: Tenant subdomain and tenant ID cannot be set at the same time. Calling this method when a tenant ID + * header is already present will throw an exception. + * + * @param tenantSubdomain + * The tenant subdomain value. + * @return This builder instance for method chaining. + * @throws IllegalStateException + * if tenant ID header is already set + */ + @Nonnull + public B tenantSubdomain( @Nonnull final String tenantSubdomain ) + { + if( containsHeader(headers, TENANT_ID_HEADER_KEY) ) { + throw new IllegalStateException(TENANT_ID_AND_TENANT_SUBDOMAIN_BOTH_PASSED_ERROR_MESSAGE); + } + + return header(new Header(TENANT_SUBDOMAIN_HEADER_KEY, tenantSubdomain)); + } + + /** + * Adds a tenant ID header to the destination. Set automatically by the Cloud SDK per-request, if both tenant id + * and tenant subdomain are left unset. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/multitenancy + *

+ * Note: Tenant subdomain and tenant ID cannot be set at the same time. Calling this method when a tenant ID + * header is already present will throw an exception. + * + * @param tenantId + * The tenant ID value. + * @return This builder instance for method chaining. + * @throws IllegalStateException + * if tenant ID header is already set + */ + @Nonnull + public B tenantId( @Nonnull final String tenantId ) + { + if( containsHeader(headers, TENANT_SUBDOMAIN_HEADER_KEY) ) { + throw new IllegalStateException(TENANT_ID_AND_TENANT_SUBDOMAIN_BOTH_PASSED_ERROR_MESSAGE); + } + return header(new Header(TENANT_ID_HEADER_KEY, tenantId)); + } + + /** + * Adds a token service tenant header to the destination. Is send to the destination service as x-tenant header + * and should be used when tokenServiceURLType in the destination service is common. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/technical-user-propagation and + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/oauth-client-credentials-authentication-cf15900ca39242fb87a1fb081a54b9ca + * + * @param tokenServiceTenant + * The token service tenant value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B tokenServiceTenant( @Nonnull final String tokenServiceTenant ) + { + return header(new Header(TOKEN_SERVICE_TENANT_HEADER_KEY, tokenServiceTenant)); + } + + /** + * Adds a client assertion header to the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/provide-client-assertion-properties-as-headers + * + * @param clientAssertion + * The client assertion value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B clientAssertion( @Nonnull final String clientAssertion ) + { + return header(new Header(CLIENT_ASSERTION_HEADER_KEY, clientAssertion)); + } + + /** + * Adds a client assertion type header to the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/provide-client-assertion-properties-as-headers + * + * @param clientAssertionType + * The client assertion type value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B clientAssertionType( @Nonnull final String clientAssertionType ) + { + return header(new Header(CLIENT_ASSERTION_TYPE_HEADER_KEY, clientAssertionType)); + } + + /** + * Adds a client assertion destination name header to the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/client-assertion-with-automated-assertion-fetching-by-service + * + * @param clientAssertionDestinationName + * The client assertion destination name value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B clientAssertionDestinationName( @Nonnull final String clientAssertionDestinationName ) + { + return header(new Header(CLIENT_ASSERTION_DESTINATION_NAME_HEADER_KEY, clientAssertionDestinationName)); + } + + /** + * Adds an authorization header to the destination. Will be used for OAuth 2.0 token exchange and principal + * propagation by the transparent proxy. Will be sent to the destination service as x-user-token, x-code, + * x-refresh-token, x-subject-token or x-chain-var-subjectToken header depending on the destination + * authentication type. Set automatically by the Cloud SDK per-request, if unset here. + * + * @param authorization + * The authorization value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B authorization( @Nonnull final String authorization ) + { + return header(new Header(AUTHORIZATION_HEADER_KEY, authorization)); + } + + /** + * Adds a subject token type header to the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/oauth-token-exchange-authentication-8813df7e39e5472ca5bdcdd34598592d + * + * @param subjectTokenType + * The subject token type value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B subjectTokenType( @Nonnull final String subjectTokenType ) + { + return header(new Header(SUBJECT_TOKEN_TYPE_HEADER_KEY, subjectTokenType)); + } + + /** + * Adds an actor token header to the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/oauth-token-exchange-authentication-8813df7e39e5472ca5bdcdd34598592d + * + * @param actorToken + * The actor token value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B actorToken( @Nonnull final String actorToken ) + { + return header(new Header(ACTOR_TOKEN_HEADER_KEY, actorToken)); + } + + /** + * Adds an actor token type header to the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/oauth-token-exchange-authentication-8813df7e39e5472ca5bdcdd34598592d + * + * @param actorTokenType + * The actor token type value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B actorTokenType( @Nonnull final String actorTokenType ) + { + return header(new Header(ACTOR_TOKEN_TYPE_HEADER_KEY, actorTokenType)); + } + + /** + * Adds a redirect URI header to the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/oauth-authorization-code-authentication-7bdfed49c6d0451b8aafe1c94da8c770 + * + * @param redirectUri + * The redirect URI value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B redirectUri( @Nonnull final String redirectUri ) + { + return header(new Header(REDIRECT_URI_HEADER_KEY, redirectUri)); + } + + /** + * Adds a code verifier header to the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/oauth-authorization-code-authentication-7bdfed49c6d0451b8aafe1c94da8c770 + * + * @param codeVerifier + * The code verifier value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B codeVerifier( @Nonnull final String codeVerifier ) + { + return header(new Header(CODE_VERIFIER_HEADER_KEY, codeVerifier)); + } + + /** + * Sets the chain name header for the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/ias-signed-saml-bearer-assertion + * + * @param chainName + * The name of the chain. + * @return This builder instance for method chaining. + */ + @Nonnull + public B chainName( @Nonnull final String chainName ) + { + return header(new Header(CHAIN_NAME_HEADER_KEY, chainName)); + } + + /** + * Sets the chain variable subject token header for the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/ias-signed-saml-bearer-assertion + * + * @param subjectToken + * The subject token value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B chainVarSubjectToken( @Nonnull final String subjectToken ) + { + return header(new Header(CHAIN_VAR_SUBJECT_TOKEN_HEADER_KEY, subjectToken)); + } + + /** + * Sets the chain variable subject token type header for the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/ias-signed-saml-bearer-assertion + * + * @param subjectTokenType + * The subject token type value. + * @return This builder instance for method chaining. + */ + @Nonnull + public B chainVarSubjectTokenType( @Nonnull final String subjectTokenType ) + { + return header(new Header(CHAIN_VAR_SUBJECT_TOKEN_TYPE_HEADER_KEY, subjectTokenType)); + } + + /** + * Sets the chain variable SAML provider destination name header for the destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/ias-signed-saml-bearer-assertion + * + * @param samlProviderDestinationName + * The SAML provider destination name. + * @return This builder instance for method chaining. + */ + @Nonnull + public B chainVarSamlProviderDestinationName( @Nonnull final String samlProviderDestinationName ) + { + return header(new Header(CHAIN_VAR_SAML_PROVIDER_DESTINATION_NAME_HEADER_KEY, samlProviderDestinationName)); + } + + /** + * Finally creates the {@code TransparentProxyDestination} with the properties retrieved via the + * {@link #property(String, Object)} method. + * + * @return A fully instantiated {@code TransparentProxyDestination}. + */ + @Nonnull + public TransparentProxyDestination build() + { + return new TransparentProxyDestination(propertiesBuilder.build(), headers, customHeaderProviders); + } + } + + /** + * Builder for creating a "static" {@link TransparentProxyDestination}. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/destination-custom-resource + */ + public static final class StaticBuilder extends AbstractBuilder + { + private StaticBuilder( @Nonnull final String uri ) + { + property(DestinationProperty.URI, uri); + } + + @Override + protected StaticBuilder getThis() + { + return this; + } + } + + /** + * Builder for creating a "dynamic" {@link TransparentProxyDestination}. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/dynamic-lookup-of-destinations + */ + public static final class DynamicBuilder extends AbstractBuilder + { + private DynamicBuilder( @Nonnull final String destinationName, @Nonnull final String uri ) + { + if( destinationName.isEmpty() ) { + throw new IllegalArgumentException( + "The 'destinationName' property is required for dynamic destinations but was not set."); + } + + this.header(DESTINATION_NAME_HEADER_KEY, destinationName); + property(DestinationProperty.URI, uri); + } + + @Override + protected DynamicBuilder getThis() + { + return this; + } + + /** + * Sets the fragment name for the dynamic destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/dynamic-lookup-of-destinations + * + * @param fragmentName + * The name of the fragment to use. + * @return This builder instance for method chaining. + */ + @Nonnull + public DynamicBuilder fragmentName( @Nonnull final String fragmentName ) + { + return header(new Header(FRAGMENT_NAME_HEADER_KEY, fragmentName)); + } + + /** + * Sets the fragment optional flag for the dynamic destination. See + * https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/dynamic-lookup-of-destinations + * + * @param fragmentOptional + * The value indicating if the fragment is optional. + * @return This builder instance for method chaining. + */ + @Nonnull + public DynamicBuilder fragmentOptional( final boolean fragmentOptional ) + { + return header(new Header(FRAGMENT_OPTIONAL_HEADER_KEY, Boolean.toString(fragmentOptional))); + } + } +} diff --git a/cloudplatform/connectivity-destination-service/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/TransparentProxyDestinationTest.java b/cloudplatform/connectivity-destination-service/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/TransparentProxyDestinationTest.java new file mode 100644 index 000000000..e10eb4c89 --- /dev/null +++ b/cloudplatform/connectivity-destination-service/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/TransparentProxyDestinationTest.java @@ -0,0 +1,264 @@ +package com.sap.cloud.sdk.cloudplatform.connectivity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.net.URI; +import java.util.Collection; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.auth0.jwt.interfaces.DecodedJWT; +import com.sap.cloud.sdk.cloudplatform.security.AuthToken; +import com.sap.cloud.sdk.cloudplatform.security.AuthTokenAccessor; +import com.sap.cloud.sdk.cloudplatform.tenant.DefaultTenant; +import com.sap.cloud.sdk.cloudplatform.tenant.Tenant; +import com.sap.cloud.sdk.cloudplatform.tenant.TenantAccessor; + +import io.vavr.control.Option; + +class TransparentProxyDestinationTest +{ + private static final URI VALID_URI = URI.create("https://www.sap.de"); + private static final String TRANSPARENT_PROXY_GATEWAY = "http://destination-gateway:80"; + private static final String TEST_KEY = "someKey"; + private static final String TEST_VALUE = "someValue"; + private static final String TEST_DEST_NAME = "testDest"; + private static final String TEST_TENANT_SUBDOMAIN = "subdomainValue"; + private static final String TEST_TENANT_ID = "tenantIdValue"; + private static final String TEST_AUTHORIZATION_HEADER = "dummy-jwt-token"; + + @Test + void testGetDelegationDestination() + { + final TransparentProxyDestination destination = + TransparentProxyDestination + .dynamicDestination(TEST_DEST_NAME, VALID_URI.toString()) + .property(TEST_KEY, TEST_VALUE) + .build(); + + assertThat(destination.get(TEST_KEY)).isEqualTo(Option.some(TEST_VALUE)); + } + + @Test + void testGetUriSuccessfully() + { + final TransparentProxyDestination destination = + TransparentProxyDestination.dynamicDestination(TEST_DEST_NAME, VALID_URI.toString()).build(); + + Assertions.assertThat(destination.getUri()).isEqualTo(VALID_URI); + } + + @Test + void testHeaders() + { + final Header header1 = new Header("foo", "bar"); + final Header header2 = new Header("baz", "qux"); + final Header header3 = new Header(TransparentProxyDestination.DESTINATION_NAME_HEADER_KEY, TEST_DEST_NAME); + + final TransparentProxyDestination destination = + TransparentProxyDestination + .dynamicDestination(TEST_DEST_NAME, VALID_URI.toString()) + .header(header1) + .header(header2) + .build(); + + final Collection

headers = destination.getHeaders(VALID_URI); + assertThat(headers).containsExactlyInAnyOrder(header1, header2, header3); + } + + @Test + void testDynamicDestinationHeaders() + { + final TransparentProxyDestination destination = + TransparentProxyDestination + .dynamicDestination(TEST_DEST_NAME, VALID_URI.toString()) + .fragmentName("fragName") + .fragmentOptional(true) + .build(); + + assertThat(destination.getHeaders(VALID_URI)) + .contains( + new Header(TransparentProxyDestination.DESTINATION_NAME_HEADER_KEY, TEST_DEST_NAME), + new Header(TransparentProxyDestination.FRAGMENT_NAME_HEADER_KEY, "fragName"), + new Header(TransparentProxyDestination.FRAGMENT_OPTIONAL_HEADER_KEY, "true")); + } + + @Test + void testTenantHeaders() + { + final TransparentProxyDestination destWithTenantId = + TransparentProxyDestination.staticDestination(VALID_URI.toString()).tenantId("tenantId").build(); + + assertThat(destWithTenantId.getHeaders(VALID_URI)) + .contains(new Header(TransparentProxyDestination.TENANT_ID_HEADER_KEY, "tenantId")); + + final TransparentProxyDestination destWithSubdomain = + TransparentProxyDestination.staticDestination(VALID_URI.toString()).tenantSubdomain("subdomain").build(); + + assertThat(destWithSubdomain.getHeaders(VALID_URI)) + .contains(new Header(TransparentProxyDestination.TENANT_SUBDOMAIN_HEADER_KEY, "subdomain")); + } + + @Test + void testCommonHeaders() + { + final TransparentProxyDestination destination = + TransparentProxyDestination + .staticDestination(VALID_URI.toString()) + .tokenServiceTenant("tokenTenant") + .clientAssertion("clientAssert") + .clientAssertionType("clientAssertType") + .clientAssertionDestinationName("clientAssertDest") + .authorization("authValue") + .subjectTokenType("subjectTokenType") + .actorToken("actorToken") + .actorTokenType("actorTokenType") + .redirectUri("redirectUri") + .codeVerifier("codeVerifier") + .chainName("chainName") + .chainVarSubjectToken("chainVarSubjectToken") + .chainVarSubjectTokenType("chainVarSubjectTokenType") + .chainVarSamlProviderDestinationName("chainVarSamlProviderDestName") + .build(); + + assertThat(destination.getHeaders(VALID_URI)) + .contains( + new Header(TransparentProxyDestination.TOKEN_SERVICE_TENANT_HEADER_KEY, "tokenTenant"), + new Header(TransparentProxyDestination.CLIENT_ASSERTION_HEADER_KEY, "clientAssert"), + new Header(TransparentProxyDestination.CLIENT_ASSERTION_TYPE_HEADER_KEY, "clientAssertType"), + new Header( + TransparentProxyDestination.CLIENT_ASSERTION_DESTINATION_NAME_HEADER_KEY, + "clientAssertDest"), + new Header(TransparentProxyDestination.AUTHORIZATION_HEADER_KEY, "authValue"), + new Header(TransparentProxyDestination.SUBJECT_TOKEN_TYPE_HEADER_KEY, "subjectTokenType"), + new Header(TransparentProxyDestination.ACTOR_TOKEN_HEADER_KEY, "actorToken"), + new Header(TransparentProxyDestination.ACTOR_TOKEN_TYPE_HEADER_KEY, "actorTokenType"), + new Header(TransparentProxyDestination.REDIRECT_URI_HEADER_KEY, "redirectUri"), + new Header(TransparentProxyDestination.CODE_VERIFIER_HEADER_KEY, "codeVerifier"), + new Header(TransparentProxyDestination.CHAIN_NAME_HEADER_KEY, "chainName"), + new Header(TransparentProxyDestination.CHAIN_VAR_SUBJECT_TOKEN_HEADER_KEY, "chainVarSubjectToken"), + new Header( + TransparentProxyDestination.CHAIN_VAR_SUBJECT_TOKEN_TYPE_HEADER_KEY, + "chainVarSubjectTokenType"), + new Header( + TransparentProxyDestination.CHAIN_VAR_SAML_PROVIDER_DESTINATION_NAME_HEADER_KEY, + "chainVarSamlProviderDestName")); + } + + @Test + void testTenantIdIsAddedPerRequest() + { + Tenant tenant = new DefaultTenant(TEST_TENANT_ID, TEST_TENANT_SUBDOMAIN); + TransparentProxyDestination destination = + TransparentProxyDestination.dynamicDestination(TEST_DEST_NAME, TRANSPARENT_PROXY_GATEWAY).build(); + TenantAccessor.executeWithTenant(tenant, () -> { + assertThat(destination.getHeaders(URI.create(TRANSPARENT_PROXY_GATEWAY))) + .contains(new Header(TransparentProxyDestination.TENANT_ID_HEADER_KEY, TEST_TENANT_ID)); + }); + } + + @Test + void testAuthorizationHeaderIsAddedPerRequest() + { + DecodedJWT mockJwt = Mockito.mock(DecodedJWT.class); + Mockito.when(mockJwt.getToken()).thenReturn(TEST_AUTHORIZATION_HEADER); + AuthToken token = new AuthToken(mockJwt); + + TransparentProxyDestination destination = + TransparentProxyDestination.dynamicDestination(TEST_DEST_NAME, TRANSPARENT_PROXY_GATEWAY).build(); + assertNotNull(destination); + AuthTokenAccessor.executeWithAuthToken(token, () -> { + assertThat(destination.getHeaders(URI.create(TRANSPARENT_PROXY_GATEWAY))) + .contains(new Header(TransparentProxyDestination.AUTHORIZATION_HEADER_KEY, TEST_AUTHORIZATION_HEADER)); + }); + } + + @Test + void testBuildThrowsExceptionWhenDestinationNameMissing() + { + Assertions + .assertThatThrownBy( + () -> TransparentProxyDestination.dynamicDestination("", TRANSPARENT_PROXY_GATEWAY).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "The 'destinationName' property is required for dynamic destinations but was not set."); + } + + @Test + void testEqualDynamicDestinations() + { + final TransparentProxyDestination destination1 = + TransparentProxyDestination.dynamicDestination(TEST_DEST_NAME, VALID_URI.toString()).build(); + + final TransparentProxyDestination destination2 = + TransparentProxyDestination.dynamicDestination(TEST_DEST_NAME, VALID_URI.toString()).build(); + assertThat(destination1).isEqualTo(destination2); + } + + @Test + void testEqualStaticDestinations() + { + final TransparentProxyDestination destination1 = + TransparentProxyDestination.staticDestination(VALID_URI.toString()).build(); + + final TransparentProxyDestination destination2 = + TransparentProxyDestination.staticDestination(VALID_URI.toString()).build(); + + assertThat(destination1).isEqualTo(destination2); + } + + @Test + void testTenantIdAndTenantSubdomainCannotBePassedTogether() + { + Assertions + .assertThatThrownBy( + () -> TransparentProxyDestination + .staticDestination(VALID_URI.toString()) + .tenantId("tenantId") + .tenantSubdomain("tenantSubdomain") + .build()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(TransparentProxyDestination.TENANT_ID_AND_TENANT_SUBDOMAIN_BOTH_PASSED_ERROR_MESSAGE); + + Assertions + .assertThatThrownBy( + () -> TransparentProxyDestination + .staticDestination(VALID_URI.toString()) + .tenantSubdomain("tenantSubdomain") + .tenantId("tenantId") + .build()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(TransparentProxyDestination.TENANT_ID_AND_TENANT_SUBDOMAIN_BOTH_PASSED_ERROR_MESSAGE); + } + + @Test + void testNoTenantHeaderWhenNoTenantPresent() + { + TransparentProxyDestination destination = + TransparentProxyDestination.dynamicDestination(TEST_DEST_NAME, TRANSPARENT_PROXY_GATEWAY).build(); + Collection
headers = destination.getHeaders(URI.create(TRANSPARENT_PROXY_GATEWAY)); + assertThat( + headers + .stream() + .noneMatch( + header -> header.getName().equalsIgnoreCase(TransparentProxyDestination.TENANT_ID_HEADER_KEY))) + .isTrue(); + } + + @Test + void testNoAuthorizationHeaderWhenNoAuthTokenPresent() + { + TransparentProxyDestination destination = + TransparentProxyDestination.dynamicDestination(TEST_DEST_NAME, TRANSPARENT_PROXY_GATEWAY).build(); + Collection
headers = destination.getHeaders(URI.create(TRANSPARENT_PROXY_GATEWAY)); + assertThat( + headers + .stream() + .noneMatch( + header -> header.getName().equalsIgnoreCase(TransparentProxyDestination.AUTHORIZATION_HEADER_KEY))) + .isTrue(); + } +} diff --git a/release_notes.md b/release_notes.md index 3112514a9..4fa7a5167 100644 --- a/release_notes.md +++ b/release_notes.md @@ -12,7 +12,7 @@ ### ✨ New Functionality -- +- Introducing Transparent Proxy Destination - https://sap.github.io/cloud-sdk/docs/java/features/connectivity/transparent-proxy ### 📈 Improvements