Skip to content

Commit 5ef5e58

Browse files
committed
More stuff
1 parent 6278ca3 commit 5ef5e58

File tree

13 files changed

+346
-106
lines changed

13 files changed

+346
-106
lines changed

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

-14
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414

1515
import lombok.AccessLevel;
1616
import lombok.AllArgsConstructor;
17-
import lombok.Getter;
18-
import lombok.RequiredArgsConstructor;
1917
import lombok.Value;
2018

2119
/**
@@ -266,16 +264,4 @@ public IasCommunicationOptions getValue()
266264
}
267265
}
268266
}
269-
270-
// TODO: currently only public for our E2E test
271-
@Beta
272-
@Getter
273-
@RequiredArgsConstructor( staticName = "enhanceDestination" )
274-
public static final class ZeroTrustIdentityOptions
275-
implements
276-
ServiceBindingDestinationOptions.OptionsEnhancer<HttpDestination>
277-
{
278-
@Nonnull
279-
final HttpDestination value;
280-
}
281267
}

cloudplatform/connectivity-oauth/pom.xml

+10-5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@
4545
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
4646
<artifactId>cloudplatform-connectivity</artifactId>
4747
</dependency>
48+
<dependency>
49+
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
50+
<artifactId>connectivity-ztis</artifactId>
51+
<!-- ztis brings some dependencies we don't want for applications that don't need it -->
52+
<optional>true</optional>
53+
</dependency>
54+
<dependency>
55+
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
56+
<artifactId>connectivity-apache-httpclient4</artifactId>
57+
</dependency>
4858
<dependency>
4959
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
5060
<artifactId>security</artifactId>
@@ -139,11 +149,6 @@
139149
<artifactId>java-access-api</artifactId>
140150
<scope>test</scope>
141151
</dependency>
142-
<dependency>
143-
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
144-
<artifactId>connectivity-apache-httpclient4</artifactId>
145-
<scope>test</scope>
146-
</dependency>
147152
<dependency>
148153
<groupId>org.junit.jupiter</groupId>
149154
<artifactId>junit-jupiter-api</artifactId>

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

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.sap.cloud.sdk.cloudplatform.connectivity.BtpServiceOptions.BusinessLoggingOptions;
2323
import com.sap.cloud.sdk.cloudplatform.connectivity.BtpServiceOptions.BusinessRulesOptions;
2424
import com.sap.cloud.sdk.cloudplatform.connectivity.BtpServiceOptions.WorkflowOptions;
25+
import com.sap.cloud.sdk.cloudplatform.connectivity.SecurityLibWorkarounds.ZtisClientIdentity;
2526
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
2627
import com.sap.cloud.sdk.cloudplatform.tenant.Tenant;
2728
import com.sap.cloud.sdk.cloudplatform.tenant.TenantAccessor;
@@ -247,6 +248,9 @@ private void attachClientKeyStore( @Nonnull final OAuth2Options.Builder optionsB
247248
private KeyStore getClientKeyStore()
248249
{
249250
final ClientIdentity clientIdentity = getClientIdentity();
251+
if( clientIdentity instanceof ZtisClientIdentity ) {
252+
return ((ZtisClientIdentity) clientIdentity).keyStore();
253+
}
250254
if( !(clientIdentity instanceof ClientCertificate) ) {
251255
return null;
252256
}

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

+37
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
package com.sap.cloud.sdk.cloudplatform.connectivity;
66

7+
import static com.sap.cloud.sdk.cloudplatform.connectivity.SecurityLibWorkarounds.X509_ATTESTED;
8+
79
import java.net.URI;
810
import java.net.URISyntaxException;
11+
import java.security.KeyStore;
912
import java.util.ArrayList;
1013
import java.util.Arrays;
1114
import java.util.Collections;
@@ -16,7 +19,9 @@
1619

1720
import com.google.common.annotations.Beta;
1821
import com.sap.cloud.environment.servicebinding.api.TypedMapView;
22+
import com.sap.cloud.sdk.cloudplatform.connectivity.SecurityLibWorkarounds.ZtisClientIdentity;
1923
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
24+
import com.sap.cloud.sdk.cloudplatform.exception.CloudPlatformException;
2025
import com.sap.cloud.security.config.ClientCertificate;
2126
import com.sap.cloud.security.config.ClientCredentials;
2227
import com.sap.cloud.security.config.ClientIdentity;
@@ -132,11 +137,43 @@ protected List<String> getOAuthPropertyPath()
132137
ClientIdentity getCertificateIdentity()
133138
{
134139
final String clientid = getOAuthCredentialOrThrow(String.class, "clientid");
140+
141+
final String exactCredentialType = getOAuthCredentialOrThrow(String.class, "credential-type");
142+
if( exactCredentialType.equals(X509_ATTESTED) ) {
143+
return getZtisClientIdentity(clientid);
144+
}
145+
135146
final String cert = getOAuthCredentialOrThrow(String.class, "certificate");
136147
final String key = getOAuthCredentialOrThrow(String.class, "key");
137148
return new ClientCertificate(cert, key, clientid);
138149
}
139150

151+
@Nonnull
152+
private ZtisClientIdentity getZtisClientIdentity( @Nonnull final String clientid )
153+
{
154+
try {
155+
// sanity check: assert the connectivity-ztis module is present
156+
getClass()
157+
.getClassLoader()
158+
.loadClass("com.sap.cloud.sdk.cloudplatform.connectivity.ZeroTrustIdentityService");
159+
}
160+
catch( final ClassNotFoundException e ) {
161+
throw new CloudPlatformException(
162+
"Failed to load implementation for credential type X509_ATTESTED. Please ensure the 'connectivity-ztis' module is present.",
163+
e);
164+
}
165+
final ZeroTrustIdentityService ztis = ZeroTrustIdentityService.getInstance();
166+
167+
final KeyStore keyStore;
168+
try {
169+
keyStore = ztis.getOrCreateKeyStore();
170+
}
171+
catch( final Exception e ) {
172+
throw new CloudPlatformException("Failed to load X509 SVID from Zero Trust Identity Service.", e);
173+
}
174+
return new ZtisClientIdentity(clientid, keyStore);
175+
}
176+
140177
@Nonnull
141178
ClientIdentity getSecretIdentity()
142179
{

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

-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ public class IdentityAuthenticationServiceBindingDestinationLoader implements Se
4040
private static final String PROPERTY_TYPE_MISMATCH_WITH_FALLBACK_TEMPLATE =
4141
"The '{}' attribute of the IAS-based service binding is expected to be an instance of {}, which is not the case. The fallback value will be used instead.";
4242

43-
private static final ServiceIdentifier NULL_IDENTIFIER = ServiceIdentifier.of("unknown-service");
4443
@Nonnull
4544
private final ServiceBindingDestinationLoader delegateLoader;
4645

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

+30-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
import javax.annotation.Nonnull;
1616
import javax.annotation.Nullable;
1717

18+
import org.apache.http.impl.client.CloseableHttpClient;
19+
1820
import com.auth0.jwt.interfaces.DecodedJWT;
1921
import com.github.benmanes.caffeine.cache.Cache;
2022
import com.github.benmanes.caffeine.cache.Caffeine;
2123
import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier;
2224
import com.sap.cloud.sdk.cloudplatform.cache.CacheKey;
2325
import com.sap.cloud.sdk.cloudplatform.cache.CacheManager;
26+
import com.sap.cloud.sdk.cloudplatform.connectivity.SecurityLibWorkarounds.ZtisClientIdentity;
2427
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
2528
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationOAuthTokenException;
2629
import com.sap.cloud.sdk.cloudplatform.exception.CloudPlatformException;
@@ -55,7 +58,6 @@
5558
@Slf4j
5659
class OAuth2Service
5760
{
58-
5961
/**
6062
* Cache to reuse OAuth2TokenService and with that reuse the underlying response cache.
6163
* <p>
@@ -97,7 +99,33 @@ class OAuth2Service
9799
OAuth2TokenService getTokenService( @Nullable final String tenantId )
98100
{
99101
final CacheKey key = CacheKey.fromIds(tenantId, null).append(identity);
100-
return tokenServiceCache.get(key, x -> new DefaultOAuth2TokenService(HttpClientFactory.create(identity)));
102+
return tokenServiceCache.get(key, this::createTokenService);
103+
}
104+
105+
@Nonnull
106+
private OAuth2TokenService createTokenService( @Nonnull final CacheKey ignored )
107+
{
108+
if( !(identity instanceof ZtisClientIdentity) ) {
109+
return new DefaultOAuth2TokenService(HttpClientFactory.create(identity));
110+
}
111+
112+
final DefaultHttpDestination destination =
113+
DefaultHttpDestination
114+
.builder(tokenUri)
115+
.name("oauth-destination-ztis-" + identity.getId().hashCode())
116+
.keyStore(((ZtisClientIdentity) identity).keyStore())
117+
.build();
118+
final CloseableHttpClient httpClient;
119+
try {
120+
httpClient = ((CloseableHttpClient) HttpClientAccessor.getHttpClient(destination));
121+
}
122+
catch( final ClassCastException e ) {
123+
throw new DestinationAccessException(
124+
"For the X509_ATTESTED credential type the 'HttpClientAccessor' must return instances of 'CloseableHttpClient'.",
125+
e);
126+
}
127+
128+
return new DefaultOAuth2TokenService(httpClient);
101129
}
102130

103131
@Nonnull
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
package com.sap.cloud.sdk.cloudplatform.connectivity;
22

3+
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationKeyStoreComparator.resolveCertificatesOnly;
4+
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationKeyStoreComparator.resolveKeyStoreHashCode;
5+
6+
import java.security.KeyStore;
7+
38
import javax.annotation.Nonnull;
49
import javax.annotation.Nullable;
510

11+
import org.apache.commons.lang3.builder.EqualsBuilder;
12+
import org.apache.commons.lang3.builder.HashCodeBuilder;
13+
14+
import com.sap.cloud.security.config.ClientIdentity;
615
import com.sap.cloud.security.config.CredentialType;
716

817
final class SecurityLibWorkarounds
918
{
1019
private static final String X509_GENERATED = "X509_GENERATED";
20+
static final String X509_ATTESTED = "X509_ATTESTED";
1121

1222
private SecurityLibWorkarounds()
1323
{
@@ -17,11 +27,50 @@ private SecurityLibWorkarounds()
1727
@Nullable
1828
static CredentialType getCredentialType( @Nonnull final String rawType )
1929
{
20-
if( rawType.equals(X509_GENERATED) ) {
21-
// this particular credential type is currently (2024-01-31) NOT supported by the Security Client Lib.
30+
if( rawType.equals(X509_GENERATED) || rawType.equals(X509_ATTESTED) ) {
31+
// these particular credential types are only supported by the Security Client Lib > 3.3.5
2232
return CredentialType.X509;
2333
}
2434

2535
return CredentialType.from(rawType);
2636
}
27-
}
37+
38+
record ZtisClientIdentity( @Nonnull String id, @Nonnull KeyStore keyStore) implements ClientIdentity {
39+
40+
@Override
41+
public String getId()
42+
{
43+
return id;
44+
}
45+
46+
@Override
47+
public boolean isCertificateBased()
48+
{
49+
return true;
50+
}
51+
52+
// The identity will be used as cache key, so it's important we correctly implement equals/hashCode
53+
@Override
54+
public boolean equals( Object obj )
55+
{
56+
if( this == obj ) {
57+
return true;
58+
}
59+
60+
if( obj == null || getClass() != obj.getClass() ) {
61+
return false;
62+
}
63+
64+
final ZtisClientIdentity that = (ZtisClientIdentity) obj;
65+
return new EqualsBuilder()
66+
.append(id, that.id)
67+
.append(resolveCertificatesOnly(keyStore), resolveCertificatesOnly(that.keyStore))
68+
.isEquals();
69+
}
70+
71+
@Override
72+
public int hashCode()
73+
{
74+
return new HashCodeBuilder(41, 71).append(id).append(resolveKeyStoreHashCode(keyStore)).build();
75+
}
76+
}}

cloudplatform/connectivity-oauth/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/BtpServicePropertySuppliersTest.java

+27
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@
4141

4242
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
4343
import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier;
44+
import com.sap.cloud.environment.servicebinding.api.exception.ServiceBindingAccessException;
4445
import com.sap.cloud.sdk.cloudplatform.connectivity.BtpServiceOptions.BusinessLoggingOptions;
4546
import com.sap.cloud.sdk.cloudplatform.connectivity.BtpServiceOptions.BusinessRulesOptions;
4647
import com.sap.cloud.sdk.cloudplatform.connectivity.BtpServiceOptions.WorkflowOptions;
4748
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
49+
import com.sap.cloud.sdk.cloudplatform.exception.CloudPlatformException;
4850
import com.sap.cloud.sdk.cloudplatform.tenant.DefaultTenant;
4951
import com.sap.cloud.sdk.cloudplatform.tenant.TenantAccessor;
5052

@@ -268,6 +270,7 @@ void testAiCore()
268270

269271
final OAuth2PropertySupplier sut = AI_CORE.resolve(options);
270272

273+
assertThat(sut).isNotNull();
271274
assertThat(sut.getServiceUri())
272275
.isEqualTo(URI.create("https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com"));
273276
assertThat(sut.getClientIdentity().getId()).isEqualTo("client-id");
@@ -551,6 +554,30 @@ void testMutualTlsCanBeCombinedWithTokenRetrievalOptions()
551554
assertThat(tokenRetrievalOptions.getAdditionalTokenRetrievalParameters()).isNotEmpty();
552555
}
553556

557+
@Test
558+
@DisplayName( "Test the credential type X509_ATTESTED" )
559+
void testMutualTlsWithZeroTrustIdentityService()
560+
{
561+
final ServiceBinding binding =
562+
bindingWithCredentials(
563+
ServiceIdentifier.IDENTITY_AUTHENTICATION,
564+
entry("app_tid", PROVIDER_TENANT_ID),
565+
entry("url", PROVIDER_URL),
566+
entry("credential-type", "X509_ATTESTED"),
567+
entry("clientid", "ias-client-id"));
568+
569+
final ServiceBindingDestinationOptions options =
570+
ServiceBindingDestinationOptions.forService(binding).build();
571+
572+
final OAuth2PropertySupplier sut = IDENTITY_AUTHENTICATION.resolve(options);
573+
assertThat(sut).isNotNull();
574+
575+
assertThatThrownBy(sut::getClientIdentity)
576+
.isInstanceOf(CloudPlatformException.class)
577+
.describedAs("We are not mocking the ZTIS service here so this should fail")
578+
.hasCauseInstanceOf(ServiceBindingAccessException.class);
579+
}
580+
554581
@Test
555582
void testMutuallyExclusiveOptions()
556583
{

cloudplatform/connectivity-oauth/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/DefaultOAuth2PropertySupplierTest.java

+21
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import com.sap.cloud.environment.servicebinding.api.DefaultServiceBinding;
1919
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
2020
import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier;
21+
import com.sap.cloud.environment.servicebinding.api.exception.ServiceBindingAccessException;
2122
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
23+
import com.sap.cloud.sdk.cloudplatform.exception.CloudPlatformException;
2224
import com.sap.cloud.security.config.ClientCertificate;
2325
import com.sap.cloud.security.config.CredentialType;
2426

@@ -150,6 +152,25 @@ void testCredentialTypeX509()
150152
});
151153
}
152154

155+
@Test
156+
void testCredentialTypeX509_ATTESTED()
157+
{
158+
final ServiceBinding binding =
159+
new ServiceBindingBuilder(ServiceIdentifier.of("testX509_attested"))
160+
.with("credentials.uaa.credential-type", "X509_ATTESTED")
161+
.with("credentials.uaa.clientid", "id")
162+
.build();
163+
final ServiceBindingDestinationOptions options = ServiceBindingDestinationOptions.forService(binding).build();
164+
165+
sut = new DefaultOAuth2PropertySupplier(options);
166+
167+
assertThat(sut.getCredentialType()).isEqualTo(CredentialType.X509);
168+
assertThatThrownBy(sut::getClientIdentity)
169+
.isInstanceOf(CloudPlatformException.class)
170+
.describedAs("We are not mocking the Zero Trust Identity Service here, so this should be a failure")
171+
.hasCauseInstanceOf(ServiceBindingAccessException.class);
172+
}
173+
153174
@RequiredArgsConstructor
154175
private static final class ServiceBindingBuilder
155176
{

0 commit comments

Comments
 (0)