Skip to content

Commit 6c84b1a

Browse files
MatKuhrJonas-Isrnewtork
authored
[Destinations] Custom Destination Service Parameters and Chaining (#895)
Co-authored-by: Jonas-Isr <[email protected]> Co-authored-by: Alexander Dümont <[email protected]>
1 parent b023be3 commit 6c84b1a

File tree

10 files changed

+198
-27
lines changed

10 files changed

+198
-27
lines changed

cloudplatform/cloudplatform-connectivity/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationOptions.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.HashMap;
44
import java.util.Map;
5+
import java.util.Set;
56

67
import javax.annotation.Nonnull;
78
import javax.annotation.Nullable;
@@ -36,6 +37,18 @@ public Option<Object> get( @Nonnull final String key )
3637
return Option.of(parameters.get(key));
3738
}
3839

40+
/**
41+
* Get all defined options.
42+
*
43+
* @return A set of all option keys.
44+
* @since 5.22.0
45+
*/
46+
@Nonnull
47+
public Set<String> getOptionKeys()
48+
{
49+
return Set.copyOf(parameters.keySet());
50+
}
51+
3952
/**
4053
* Creates a builder for instantiating {@link DestinationOptions} objects.
4154
*

cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationRetrievalStrategy.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationRetrievalStrategy.TokenForwarding.REFRESH_TOKEN;
55
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationRetrievalStrategy.TokenForwarding.USER_TOKEN;
66

7+
import java.util.ArrayList;
8+
import java.util.List;
9+
710
import javax.annotation.Nonnull;
811
import javax.annotation.Nullable;
912

@@ -30,6 +33,8 @@ final class DestinationRetrievalStrategy
3033
private final String token;
3134
@Nullable
3235
private String fragment;
36+
@Nonnull
37+
private final List<Header> additionalHeaders = new ArrayList<>();
3338

3439
static DestinationRetrievalStrategy withoutToken( @Nonnull final OnBehalfOf behalf )
3540
{
@@ -68,6 +73,12 @@ DestinationRetrievalStrategy withFragmentName( @Nonnull final String fragmentNam
6873
return this;
6974
}
7075

76+
DestinationRetrievalStrategy withAdditionalHeaders( @Nonnull final List<Header> additionalHeaders )
77+
{
78+
this.additionalHeaders.addAll(additionalHeaders);
79+
return this;
80+
}
81+
7182
enum TokenForwarding
7283
{
7384
USER_TOKEN,

cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationRetrievalStrategyResolver.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,15 @@ DestinationRetrieval prepareSupplier( @Nonnull final DestinationOptions options
116116
.getFragmentName(options)
117117
.peek(it -> log.debug("Found fragment name '{}'.", it))
118118
.getOrNull();
119+
final List<Header> additionalHeaders = DestinationServiceOptionsAugmenter.getAdditionalHeaders(options);
120+
additionalHeaders.forEach(it -> log.debug("Found additional header: {}.", it));
119121

120122
log
121123
.debug(
122124
"Loading destination from reuse-destination-service with retrieval strategy {} and token exchange strategy {}.",
123125
retrievalStrategy,
124126
tokenExchangeStrategy);
125-
return prepareSupplier(retrievalStrategy, tokenExchangeStrategy, refreshToken, fragmentName);
127+
return prepareSupplier(retrievalStrategy, tokenExchangeStrategy, refreshToken, fragmentName, additionalHeaders);
126128
}
127129

128130
/**
@@ -161,7 +163,8 @@ DestinationRetrieval prepareSupplier(
161163
@Nonnull final DestinationServiceRetrievalStrategy retrievalStrategy,
162164
@Nonnull final DestinationServiceTokenExchangeStrategy tokenExchangeStrategy,
163165
@Nullable final String refreshToken,
164-
@Nullable final String fragmentName )
166+
@Nullable final String fragmentName,
167+
@Nonnull final List<Header> additionalHeaders )
165168
throws DestinationAccessException
166169
{
167170
log
@@ -198,6 +201,9 @@ DestinationRetrieval prepareSupplier(
198201
if( fragmentName != null ) {
199202
strategy.withFragmentName(fragmentName);
200203
}
204+
if( !additionalHeaders.isEmpty() ) {
205+
strategy.withAdditionalHeaders(additionalHeaders);
206+
}
201207
return new DestinationRetrieval(() -> destinationRetriever.apply(strategy), strategy.behalf());
202208
}
203209

cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@
5151
@SuppressWarnings( "PMD.TooManyStaticImports" )
5252
public class DestinationService implements DestinationLoader
5353
{
54-
private static final String PATH_DEFAULT = "/v1/destinations/";
55-
private static final String PATH_V2 = "/v2/destinations/";
54+
static final String PATH_DEFAULT = "/v1/destinations/";
55+
static final String PATH_V2 = "/v2/destinations/";
5656
private static final String PATH_SERVICE_INSTANCE = "/v1/instanceDestinations";
5757
private static final String PATH_SUBACCOUNT = "/v1/subaccountDestinations";
5858

cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationServiceAdapter.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,17 +185,25 @@ private HttpUriRequest prepareRequest( final String servicePath, final Destinati
185185
log.debug("Querying Destination Service via URI {}.", requestUri);
186186
final HttpUriRequest request = new HttpGet(requestUri);
187187

188-
final String headerName = switch( strategy.tokenForwarding() ) {
189-
case USER_TOKEN -> "x-user-token";
190-
case REFRESH_TOKEN -> "x-refresh-token";
191-
case NONE -> null;
192-
};
193-
if( headerName != null ) {
194-
request.addHeader(headerName, strategy.token());
188+
if( !servicePath.startsWith(DestinationService.PATH_DEFAULT)
189+
&& !servicePath.startsWith(DestinationService.PATH_V2) ) {
190+
// additional headers and settings are only needed for single destination requests
191+
return request;
192+
}
193+
194+
switch( strategy.tokenForwarding() ) {
195+
case USER_TOKEN -> request.addHeader("x-user-token", strategy.token());
196+
case REFRESH_TOKEN -> request.addHeader("x-refresh-token", strategy.token());
197+
case NONE -> {
198+
/* nothing to do in this case */ }
195199
}
196200
if( strategy.fragment() != null ) {
197201
request.addHeader("x-fragment-name", strategy.fragment());
198202
}
203+
for( final Header h : strategy.additionalHeaders() ) {
204+
request.addHeader(h.getName(), h.getValue());
205+
}
206+
199207
return request;
200208
}
201209

cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationServiceOptionsAugmenter.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.sap.cloud.sdk.cloudplatform.connectivity;
22

33
import java.util.HashMap;
4+
import java.util.List;
45
import java.util.Map;
56

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

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

@@ -125,7 +127,7 @@ public DestinationServiceOptionsAugmenter fragmentName( @Nonnull final String fr
125127
@Nonnull
126128
public DestinationServiceOptionsAugmenter crossLevelConsumption( @Nonnull final CrossLevelScope scope )
127129
{
128-
parameters.put(CROSS_LEVEL_SETTING, scope);
130+
parameters.put(CROSS_LEVEL_SETTING_KEY, scope);
129131
return this;
130132
}
131133

@@ -168,6 +170,29 @@ String getSuffix()
168170
}
169171
}
170172

173+
/**
174+
* Adds custom headers to the destination request. Use this to add parameters that are not directly supported by the
175+
* SDK. For example, use this to achieve destination chaining.
176+
* <p>
177+
* <strong>Note:</strong> Any secret values added as custom headers here will <strong>not</strong> be masked in log
178+
* output and may appear in plain text on debug log level.
179+
* </p>
180+
*
181+
* @param headers
182+
* The headers to be added.
183+
* @return The same augmenter that called this method.
184+
* @since 5.22.0
185+
*/
186+
@Beta
187+
@Nonnull
188+
public DestinationServiceOptionsAugmenter customHeaders( @Nonnull final Header... headers )
189+
{
190+
for( final Header header : headers ) {
191+
parameters.put(CUSTOM_HEADER_KEY + header.getName(), header.getValue());
192+
}
193+
return this;
194+
}
195+
171196
@Override
172197
public void augmentBuilder( @Nonnull final DestinationOptions.Builder builder )
173198
{
@@ -238,8 +263,28 @@ static Option<String> getFragmentName( @Nonnull final DestinationOptions options
238263
static Option<CrossLevelScope> getCrossLevelScope( @Nonnull final DestinationOptions options )
239264
{
240265
return options
241-
.get(CROSS_LEVEL_SETTING)
266+
.get(CROSS_LEVEL_SETTING_KEY)
242267
.filter(CrossLevelScope.class::isInstance)
243268
.map(CrossLevelScope.class::cast);
244269
}
270+
271+
@Nonnull
272+
static List<Header> getAdditionalHeaders( @Nonnull final DestinationOptions options )
273+
{
274+
return options
275+
.getOptionKeys()
276+
.stream()
277+
.filter(it -> it.startsWith(CUSTOM_HEADER_KEY))
278+
.map(it -> it.replaceFirst(CUSTOM_HEADER_KEY, ""))
279+
.map(headerName -> {
280+
final String headerValue =
281+
options
282+
.get(CUSTOM_HEADER_KEY + headerName)
283+
.filter(String.class::isInstance)
284+
.map(String.class::cast)
285+
.get();
286+
return new Header(headerName, headerValue);
287+
})
288+
.toList();
289+
}
245290
}

cloudplatform/connectivity-destination-service/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationRetrievalStrategyResolverTest.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@
99
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceTokenExchangeStrategy.EXCHANGE_ONLY;
1010
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceTokenExchangeStrategy.FORWARD_USER_TOKEN;
1111
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceTokenExchangeStrategy.LOOKUP_ONLY;
12-
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE;
1312
import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.NAMED_USER_CURRENT_TENANT;
1413
import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_CURRENT_TENANT;
1514
import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_PROVIDER;
1615
import static com.sap.cloud.sdk.cloudplatform.connectivity.XsuaaTokenMocker.mockXsuaaToken;
17-
import static org.assertj.core.api.Assertions.assertThat;
1816
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1917
import static org.mockito.ArgumentMatchers.any;
2018
import static org.mockito.ArgumentMatchers.eq;
@@ -26,6 +24,7 @@
2624
import static org.mockito.Mockito.verifyNoMoreInteractions;
2725

2826
import java.util.ArrayList;
27+
import java.util.Collections;
2928
import java.util.List;
3029
import java.util.function.Function;
3130

@@ -209,7 +208,8 @@ void testExceptionsAreThrownOnIllegalCombinations()
209208
.executeWithTenant(
210209
c._3(),
211210
() -> softly
212-
.assertThatThrownBy(() -> sut.prepareSupplier(c._1(), c._2(), null, null))
211+
.assertThatThrownBy(
212+
() -> sut.prepareSupplier(c._1(), c._2(), null, null, Collections.emptyList()))
213213
.as("Expecting '%s' with '%s' and '%s' to throw.", c._1(), c._2(), c._3())
214214
.isInstanceOf(DestinationAccessException.class)));
215215

@@ -235,7 +235,8 @@ void testExceptionsAreThrownForImpossibleTokenExchanges()
235235
ALWAYS_PROVIDER,
236236
DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE,
237237
null,
238-
null);
238+
null,
239+
Collections.emptyList());
239240

240241
TenantAccessor
241242
.executeWithTenant(
@@ -260,7 +261,7 @@ void testDefaultStrategies()
260261
sut.prepareSupplier(DestinationOptions.builder().build());
261262
sut.prepareSupplierAllDestinations(DestinationOptions.builder().build());
262263

263-
verify(sut).prepareSupplier(CURRENT_TENANT, FORWARD_USER_TOKEN, null, null);
264+
verify(sut).prepareSupplier(CURRENT_TENANT, FORWARD_USER_TOKEN, null, null, Collections.emptyList());
264265
verify(sut).prepareSupplierAllDestinations(CURRENT_TENANT);
265266
}
266267

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

276277
verify(sut)
277-
.prepareSupplier(CURRENT_TENANT, DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE, null, null);
278+
.prepareSupplier(
279+
CURRENT_TENANT,
280+
DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE,
281+
null,
282+
null,
283+
Collections.emptyList());
278284
verify(sut).prepareSupplierAllDestinations(CURRENT_TENANT);
279285
}
280286

@@ -305,6 +311,6 @@ void testFragmentName()
305311

306312
sut.prepareSupplier(opts);
307313

308-
verify(sut).prepareSupplier(any(), any(), eq(null), eq(fragmentName));
314+
verify(sut).prepareSupplier(any(), any(), eq(null), eq(fragmentName), any());
309315
}
310316
}

0 commit comments

Comments
 (0)