Skip to content

[Destinations] Custom Destination Service Parameters and Chaining #895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -36,6 +37,18 @@ public Option<Object> get( @Nonnull final String key )
return Option.of(parameters.get(key));
}

/**
* Get all defined options.
*
* @return A set of all option keys.
* @since 5.22.0
*/
@Nonnull
public Set<String> getOptionKeys()
{
return Set.copyOf(parameters.keySet());
}

/**
* Creates a builder for instantiating {@link DestinationOptions} objects.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationRetrievalStrategy.TokenForwarding.REFRESH_TOKEN;
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationRetrievalStrategy.TokenForwarding.USER_TOKEN;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

Expand All @@ -30,6 +33,8 @@ final class DestinationRetrievalStrategy
private final String token;
@Nullable
private String fragment;
@Nonnull
private final List<Header> additionalHeaders = new ArrayList<>();

static DestinationRetrievalStrategy withoutToken( @Nonnull final OnBehalfOf behalf )
{
Expand Down Expand Up @@ -68,6 +73,12 @@ DestinationRetrievalStrategy withFragmentName( @Nonnull final String fragmentNam
return this;
}

DestinationRetrievalStrategy withAdditionalHeaders( @Nonnull final List<Header> additionalHeaders )
{
this.additionalHeaders.addAll(additionalHeaders);
return this;
}

enum TokenForwarding
{
USER_TOKEN,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,15 @@ DestinationRetrieval prepareSupplier( @Nonnull final DestinationOptions options
.getFragmentName(options)
.peek(it -> log.debug("Found fragment name '{}'.", it))
.getOrNull();
final List<Header> additionalHeaders = DestinationServiceOptionsAugmenter.getAdditionalHeaders(options);
additionalHeaders.forEach(it -> log.debug("Found additional header: {}.", it));

log
.debug(
"Loading destination from reuse-destination-service with retrieval strategy {} and token exchange strategy {}.",
retrievalStrategy,
tokenExchangeStrategy);
return prepareSupplier(retrievalStrategy, tokenExchangeStrategy, refreshToken, fragmentName);
return prepareSupplier(retrievalStrategy, tokenExchangeStrategy, refreshToken, fragmentName, additionalHeaders);
}

/**
Expand Down Expand Up @@ -161,7 +163,8 @@ DestinationRetrieval prepareSupplier(
@Nonnull final DestinationServiceRetrievalStrategy retrievalStrategy,
@Nonnull final DestinationServiceTokenExchangeStrategy tokenExchangeStrategy,
@Nullable final String refreshToken,
@Nullable final String fragmentName )
@Nullable final String fragmentName,
@Nonnull final List<Header> additionalHeaders )
throws DestinationAccessException
{
log
Expand Down Expand Up @@ -198,6 +201,9 @@ DestinationRetrieval prepareSupplier(
if( fragmentName != null ) {
strategy.withFragmentName(fragmentName);
}
if( !additionalHeaders.isEmpty() ) {
strategy.withAdditionalHeaders(additionalHeaders);
}
return new DestinationRetrieval(() -> destinationRetriever.apply(strategy), strategy.behalf());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,17 +185,24 @@ private HttpUriRequest prepareRequest( final String servicePath, final Destinati
log.debug("Querying Destination Service via URI {}.", requestUri);
final HttpUriRequest request = new HttpGet(requestUri);

final String headerName = switch( strategy.tokenForwarding() ) {
case USER_TOKEN -> "x-user-token";
case REFRESH_TOKEN -> "x-refresh-token";
case NONE -> null;
};
if( headerName != null ) {
request.addHeader(headerName, strategy.token());
if( !servicePath.startsWith("/v1/destinations") && !servicePath.startsWith("/v2/destinations") ) {
// additional headers and settings are only needed for single destination requests
return request;
}

switch( strategy.tokenForwarding() ) {
case USER_TOKEN -> request.addHeader("x-user-token", strategy.token());
case REFRESH_TOKEN -> request.addHeader("x-refresh-token", strategy.token());
case NONE -> {
/* nothing to do in this case */ }
}
if( strategy.fragment() != null ) {
request.addHeader("x-fragment-name", strategy.fragment());
}
for( final Header h : strategy.additionalHeaders() ) {
request.addHeader(h.getName(), h.getValue());
}

return request;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sap.cloud.sdk.cloudplatform.connectivity;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Nonnull;
Expand All @@ -23,7 +24,8 @@ public class DestinationServiceOptionsAugmenter implements DestinationOptionsAug
static final String DESTINATION_TOKEN_EXCHANGE_STRATEGY_KEY = "scp.cf.destinationTokenExchangeStrategy";
static final String X_REFRESH_TOKEN_KEY = "x-refresh-token";
static final String X_FRAGMENT_KEY = "X-fragment-name";
static final String CROSS_LEVEL_SETTING = "crossLevelSetting";
static final String CROSS_LEVEL_SETTING_KEY = "crossLevelSetting";
static final String CUSTOM_HEADER_KEY = "customHeader.";

private final Map<String, Object> parameters = new HashMap<>();

Expand Down Expand Up @@ -125,7 +127,7 @@ public DestinationServiceOptionsAugmenter fragmentName( @Nonnull final String fr
@Nonnull
public DestinationServiceOptionsAugmenter crossLevelConsumption( @Nonnull final CrossLevelScope scope )
{
parameters.put(CROSS_LEVEL_SETTING, scope);
parameters.put(CROSS_LEVEL_SETTING_KEY, scope);
return this;
}

Expand Down Expand Up @@ -168,6 +170,28 @@ String getSuffix()
}
}

/**
* Adds custom headers to the destination request. Use this to add parameters that are not directly supported by the
* SDK. For example, use this to achieve destination chaining.
* <p>
* <strong>Note:</strong> Any secret values added as custom headers here will <strong>not</strong> be masked in log
* output and may appear in plain text on debug log level.
*
* @param headers
* The headers to be added.
* @return The same augmenter that called this method.
* @since 5.22.0
*/
@Beta
@Nonnull
public DestinationServiceOptionsAugmenter customHeaders( @Nonnull final Header... headers )
{
for( final Header header : headers ) {
parameters.put(CUSTOM_HEADER_KEY + header.getName(), header.getValue());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This solution uses a prefix, setting one option per header. That ensures the headers are directly respected for the cache key computation. If we were to store a nested map or other form of object, we'd have to check for equals/hashcode implementations.

}
return this;
}

@Override
public void augmentBuilder( @Nonnull final DestinationOptions.Builder builder )
{
Expand Down Expand Up @@ -238,8 +262,28 @@ static Option<String> getFragmentName( @Nonnull final DestinationOptions options
static Option<CrossLevelScope> getCrossLevelScope( @Nonnull final DestinationOptions options )
{
return options
.get(CROSS_LEVEL_SETTING)
.get(CROSS_LEVEL_SETTING_KEY)
.filter(CrossLevelScope.class::isInstance)
.map(CrossLevelScope.class::cast);
}

@Nonnull
static List<Header> getAdditionalHeaders( @Nonnull final DestinationOptions options )
{
return options
.getOptionKeys()
.stream()
.filter(it -> it.startsWith(CUSTOM_HEADER_KEY))
.map(it -> it.replaceFirst(CUSTOM_HEADER_KEY, ""))
.map(headerName -> {
final String headerValue =
options
.get(CUSTOM_HEADER_KEY + headerName)
.filter(String.class::isInstance)
.map(String.class::cast)
.get();
return new Header(headerName, headerValue);
})
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceTokenExchangeStrategy.EXCHANGE_ONLY;
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceTokenExchangeStrategy.FORWARD_USER_TOKEN;
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceTokenExchangeStrategy.LOOKUP_ONLY;
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE;
import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.NAMED_USER_CURRENT_TENANT;
import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_CURRENT_TENANT;
import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_PROVIDER;
import static com.sap.cloud.sdk.cloudplatform.connectivity.XsuaaTokenMocker.mockXsuaaToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
Expand All @@ -26,6 +24,7 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;

Expand Down Expand Up @@ -209,7 +208,8 @@ void testExceptionsAreThrownOnIllegalCombinations()
.executeWithTenant(
c._3(),
() -> softly
.assertThatThrownBy(() -> sut.prepareSupplier(c._1(), c._2(), null, null))
.assertThatThrownBy(
() -> sut.prepareSupplier(c._1(), c._2(), null, null, Collections.emptyList()))
.as("Expecting '%s' with '%s' and '%s' to throw.", c._1(), c._2(), c._3())
.isInstanceOf(DestinationAccessException.class)));

Expand All @@ -235,7 +235,8 @@ void testExceptionsAreThrownForImpossibleTokenExchanges()
ALWAYS_PROVIDER,
DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE,
null,
null);
null,
Collections.emptyList());

TenantAccessor
.executeWithTenant(
Expand All @@ -260,7 +261,7 @@ void testDefaultStrategies()
sut.prepareSupplier(DestinationOptions.builder().build());
sut.prepareSupplierAllDestinations(DestinationOptions.builder().build());

verify(sut).prepareSupplier(CURRENT_TENANT, FORWARD_USER_TOKEN, null, null);
verify(sut).prepareSupplier(CURRENT_TENANT, FORWARD_USER_TOKEN, null, null, Collections.emptyList());
verify(sut).prepareSupplierAllDestinations(CURRENT_TENANT);
}

Expand All @@ -274,7 +275,12 @@ void testDefaultNonXsuaaTokenStrategy()
sut.prepareSupplierAllDestinations(DestinationOptions.builder().build());

verify(sut)
.prepareSupplier(CURRENT_TENANT, DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE, null, null);
.prepareSupplier(
CURRENT_TENANT,
DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE,
null,
null,
Collections.emptyList());
verify(sut).prepareSupplierAllDestinations(CURRENT_TENANT);
}

Expand Down Expand Up @@ -305,6 +311,6 @@ void testFragmentName()

sut.prepareSupplier(opts);

verify(sut).prepareSupplier(any(), any(), eq(null), eq(fragmentName));
verify(sut).prepareSupplier(any(), any(), eq(null), eq(fragmentName), any());
}
}
Loading