diff --git a/DEPENDENCIES b/DEPENDENCIES index ec11fe8d..92ded5c5 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -10,9 +10,11 @@ maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.15.4, maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.4, Apache-2.0, approved, #7930 maven/mavencentral/com.fasterxml.jackson.module/jackson-module-parameter-names/2.15.4, Apache-2.0, approved, #8803 maven/mavencentral/com.fasterxml/classmate/1.6.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.github.ben-manes.caffeine/caffeine/3.1.8, Apache-2.0, approved, clearlydefined maven/mavencentral/com.github.stephenc.jcip/jcip-annotations/1.0-1, Apache-2.0, approved, CQ21949 maven/mavencentral/com.google.code.findbugs/jsr305/3.0.2, Apache-2.0, approved, #20 maven/mavencentral/com.google.errorprone/error_prone_annotations/2.18.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.errorprone/error_prone_annotations/2.21.1, Apache-2.0, approved, #9834 maven/mavencentral/com.google.guava/failureaccess/1.0.1, Apache-2.0, approved, CQ22654 maven/mavencentral/com.google.guava/guava/32.1.1-jre, Apache-2.0 AND CC0-1.0 AND LicenseRef-Public-Domain, approved, #9229 maven/mavencentral/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava, Apache-2.0, approved, CQ22657 @@ -20,7 +22,12 @@ maven/mavencentral/com.google.j2objc/j2objc-annotations/2.8, Apache-2.0, approve maven/mavencentral/com.h2database/h2/2.2.220, (EPL-1.0 OR MPL-2.0) AND (LGPL-3.0-or-later OR EPL-1.0 OR MPL-2.0), approved, #9322 maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.24.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.opencsv/opencsv/5.7.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.squareup.okhttp3/okhttp/4.12.0, Apache-2.0, approved, #11156 +maven/mavencentral/com.squareup.okio/okio-jvm/3.6.0, Apache-2.0, approved, #11158 +maven/mavencentral/com.squareup.okio/okio/3.6.0, Apache-2.0, approved, #11155 maven/mavencentral/com.zaxxer/HikariCP/5.0.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/dev.failsafe/failsafe-okhttp/3.3.2, Apache-2.0, approved, #9178 +maven/mavencentral/dev.failsafe/failsafe/3.3.2, Apache-2.0, approved, #9268 maven/mavencentral/io.github.classgraph/classgraph/4.8.149, MIT, approved, CQ22530 maven/mavencentral/io.micrometer/micrometer-commons/1.12.3, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #11679 maven/mavencentral/io.micrometer/micrometer-core/1.12.3, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #11678 @@ -32,9 +39,9 @@ maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.7, Apache-2.0, ap maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.7, Apache-2.0, approved, #5919 maven/mavencentral/jakarta.activation/jakarta.activation-api/2.1.2, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf maven/mavencentral/jakarta.annotation/jakarta.annotation-api/2.1.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.ca -maven/mavencentral/jakarta.persistence/jakarta.persistence-api/3.1.0, EPL-2.0 OR BSD-3-Clause AND (EPL-2.0 OR BSD-3-Clause AND BSD-3-Clause), approved, #7696 +maven/mavencentral/jakarta.persistence/jakarta.persistence-api/3.1.0, EPL-2.0 OR BSD-3-Clause, approved, ee4j.jpa maven/mavencentral/jakarta.servlet/jakarta.servlet-api/6.0.0, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.servlet -maven/mavencentral/jakarta.transaction/jakarta.transaction-api/2.0.1, EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0, approved, #7697 +maven/mavencentral/jakarta.transaction/jakarta.transaction-api/2.0.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jta maven/mavencentral/jakarta.validation/jakarta.validation-api/3.0.2, Apache-2.0, approved, ee4j.validation maven/mavencentral/jakarta.websocket/jakarta.websocket-api/2.1.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.websocket maven/mavencentral/jakarta.websocket/jakarta.websocket-client-api/2.1.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.websocket @@ -48,7 +55,22 @@ maven/mavencentral/org.apache.commons/commons-text/1.10.0, Apache-2.0, approved, maven/mavencentral/org.apache.logging.log4j/log4j-api/2.21.1, Apache-2.0 AND (Apache-2.0 AND LGPL-2.0-or-later), approved, #11079 maven/mavencentral/org.apache.logging.log4j/log4j-to-slf4j/2.21.1, Apache-2.0, approved, #11919 maven/mavencentral/org.aspectj/aspectjweaver/1.9.21, Apache-2.0 AND BSD-3-Clause AND EPL-1.0 AND BSD-3-Clause AND Apache-1.1, approved, #7695 +maven/mavencentral/org.checkerframework/checker-qual/3.37.0, MIT, approved, clearlydefined maven/mavencentral/org.checkerframework/checker-qual/3.42.0, MIT, approved, clearlydefined +maven/mavencentral/org.eclipse.edc/connector-core/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/core-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/data-address-http-data-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/data-plane-http-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/data-plane-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/http-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/keys-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/policy-engine-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/policy-model/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/runtime-metamodel/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/transaction-datasource-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/transaction-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/transform-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/validator-spi/0.5.2-20240327-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.jetty.ee10.websocket/jetty-ee10-websocket-jakarta-client/12.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.eclipse.jetty.ee10.websocket/jetty-ee10-websocket-jakarta-common/12.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.eclipse.jetty.ee10.websocket/jetty-ee10-websocket-jakarta-server/12.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty @@ -79,6 +101,11 @@ maven/mavencentral/org.glassfish/jakarta.json/2.0.1, EPL-2.0 OR GPL-2.0-only wit maven/mavencentral/org.hibernate.orm/hibernate-core/6.4.4.Final, LGPL-2.1-or-later AND (EPL-2.0 OR BSD-3-Clause) AND MIT, approved, #12490 maven/mavencentral/org.hibernate.validator/hibernate-validator/8.0.1.Final, Apache-2.0, approved, clearlydefined maven/mavencentral/org.jboss.logging/jboss-logging/3.5.3.Final, Apache-2.0, approved, #9471 +maven/mavencentral/org.jetbrains.kotlin/kotlin-stdlib-common/1.9.22, Apache-2.0, approved, #14186 +maven/mavencentral/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.9.22, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.9.22, None, restricted, #14185 +maven/mavencentral/org.jetbrains.kotlin/kotlin-stdlib/1.9.22, Apache-2.0, approved, #11827 +maven/mavencentral/org.jetbrains/annotations/24.1.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.liquibase/liquibase-core/4.19.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.mapstruct/mapstruct/1.5.3.Final, Apache-2.0, approved, #6277 maven/mavencentral/org.openapitools/jackson-databind-nullable/0.1.0, Apache-2.0, approved, clearlydefined diff --git a/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationClient.java b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationClient.java new file mode 100644 index 00000000..1807c906 --- /dev/null +++ b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationClient.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.client; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.AccessControlServiceException; +import org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.HttpAccessControlCheckClientConfig; +import org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.HttpAccessControlCheckDtrClientConfig; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class DtrAccessVerificationClient implements HttpAccessVerificationClient { + public static final String HEADER_EDC_BPN = "Edc-Bpn"; + public static final String AUTHORIZATION = "Authorization"; + public static final String ACCEPT = "Accept"; + public static final String CONTENT_TYPE = "Content-Type"; + private static final String APPLICATION_JSON = "application/json"; + private final Monitor monitor; + private final EdcHttpClient httpClient; + private final TypeManager typeManager; + private final HttpAccessControlCheckClientConfig config; + private final HttpAccessControlCheckDtrClientConfig dtrConfig; + private final LoadingCache tokenCache; + private final Cache accessControlDecisionCache; + + public DtrAccessVerificationClient( + final Monitor monitor, + final EdcHttpClient httpClient, + final Oauth2TokenClient tokenClient, + final TypeManager typeManager, + final HttpAccessControlCheckClientConfig config, + final HttpAccessControlCheckDtrClientConfig dtrConfig ) { + this.monitor = monitor; + this.httpClient = httpClient; + this.typeManager = typeManager; + this.config = config; + this.dtrConfig = dtrConfig; + this.tokenCache = Caffeine.newBuilder() + .maximumSize( config.getDtrClientConfigMap().size() ) + .expireAfterWrite( Duration.ofMinutes( 5 ) ) + .refreshAfterWrite( Duration.ofMinutes( 4 ) ) + .build( tokenClient::getBearerToken ); + final long cacheDurationMinutes = dtrConfig.getDecisionCacheDurationMinutes(); + if ( cacheDurationMinutes <= 0 ) { + this.accessControlDecisionCache = null; + } else { + this.accessControlDecisionCache = Caffeine.newBuilder() + .maximumSize( 10000 ) + .expireAfterWrite( Duration.ofMinutes( cacheDurationMinutes ) ) + .build(); + } + } + + @Override + public boolean isAspectModelCall( final String url ) { + return urlMatchesPattern( url, dtrConfig.getAspectModelUrlPattern() ); + } + + @Override + public boolean shouldAllowAccess( + final String requestedUriPath, + @Nullable final String requestedQueryString, + final Map additionalHeaders ) throws AccessControlServiceException { + final String requestedUrl = getTargetUrl( requestedUriPath, requestedQueryString ); + final String bpn = Optional.ofNullable( additionalHeaders.get( HEADER_EDC_BPN ) ) + .orElseThrow( () -> new AccessControlServiceException( "Null BPN found." ) ); + final RequestKey key = new RequestKey( bpn, requestedUrl ); + if ( accessControlDecisionCache == null ) { + return this.callDtr( key ); + } else { + return accessControlDecisionCache.get( + key, this::callDtr ); + } + } + + private boolean callDtr( final RequestKey requestKey ) { + final Request dtrRequest = getDtrRequest( requestKey ); + try ( Response response = httpClient.execute( dtrRequest ) ) { + return response.isSuccessful(); + } catch ( final IOException exception ) { + monitor.severe( "Failed to execute DTR access verification request.", exception ); + throw new AccessControlServiceException( exception ); + } + } + + private boolean urlMatchesPattern( final String url, final String urlPattern ) { + final Pattern pattern = Pattern.compile( urlPattern, Pattern.CASE_INSENSITIVE ); + final Matcher matcher = pattern.matcher( url ); + return matcher.find(); + } + + @NotNull + private String getTargetUrl( + final String requestedUriPath, + final String requestedQueryString ) { + final var edcBaseUrl = config.getEdcDataPlaneBaseUrl(); + final var query = Optional.ofNullable( requestedQueryString ) + .filter( s -> !s.isBlank() ) + .map( s -> "?" + s ) + .orElse( "" ); + return edcBaseUrl + requestedUriPath + query; + } + + @NotNull + private Request getDtrRequest( final RequestKey requestKey ) { + final String token = tokenCache.get( dtrConfig.getOauth2TokenScope() ); + if ( token == null ) { + throw new AccessControlServiceException( "Token is null." ); + } + final RequestBody body = RequestBody.create( + createRequest( requestKey ), + MediaType.get( APPLICATION_JSON ) ); + return new Request.Builder() + .url( dtrConfig.getDtrAccessVerificationUrl() ) + .addHeader( HEADER_EDC_BPN, requestKey.bpn() ) + .addHeader( AUTHORIZATION, "Bearer " + token ) + .addHeader( ACCEPT, APPLICATION_JSON ) + .addHeader( CONTENT_TYPE, APPLICATION_JSON ) + .post( body ) + .build(); + } + + @NotNull + private String createRequest( RequestKey requestKey ) { + return typeManager.writeValueAsString( new DtrAccessVerificationRequest( requestKey.requestedUrl() ) ); + } +} diff --git a/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationRequest.java b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationRequest.java new file mode 100644 index 00000000..5924d7a3 --- /dev/null +++ b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationRequest.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.client; + +public record DtrAccessVerificationRequest(String submodelEndpointUrl) { +} diff --git a/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/HttpAccessVerificationClient.java b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/HttpAccessVerificationClient.java new file mode 100644 index 00000000..8547b5c4 --- /dev/null +++ b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/HttpAccessVerificationClient.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.client; + +import java.util.Map; + +import org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.AccessControlServiceException; + +public interface HttpAccessVerificationClient { + + boolean isAspectModelCall( String url ); + + boolean shouldAllowAccess( + final String requestedUriPath, final String requestedQueryString, Map additionalHeaders ) throws AccessControlServiceException; + +} diff --git a/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/RequestKey.java b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/RequestKey.java new file mode 100644 index 00000000..6bad659b --- /dev/null +++ b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/RequestKey.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.client; + +public record RequestKey(String bpn, String requestedUrl) { +} diff --git a/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationClientTest.java b/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationClientTest.java new file mode 100644 index 00000000..2ab2707a --- /dev/null +++ b/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationClientTest.java @@ -0,0 +1,274 @@ +/******************************************************************************* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.client; + +import static org.assertj.core.api.Assertions.*; +import static org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.client.DtrAccessVerificationClient.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.Map; +import java.util.stream.Stream; + +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.AccessControlServiceException; +import org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.HttpAccessControlCheckClientConfig; +import org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.HttpAccessControlCheckDtrClientConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okio.Buffer; + +class DtrAccessVerificationClientTest { + static final String BPN = "BPNL000000000001"; + static final String POST = "POST"; + static final String SCOPE = "aud:dtr"; + static final String DUMMY_TOKEN = "dummy_token_value"; + static final String BEARER_PREFIX = "Bearer "; + static final String LOCALHOST_ACCESS_VERIFICATION = "https://localhost/access-verification"; + static final String LOCALHOST_EDC_DATA_PROXY = "http://localhost/edc-data/proxy"; + static final String REQUEST_FORMAT = "{\"submodelEndpointUrl\":\"%s\"}"; + static final String REQUESTED_URI_PATH = "/request/path"; + static final String REQUESTED_QUERY_STRING = "queryParam=true"; + + @Mock + private Response httpResponse; + @Mock + private Monitor monitor; + @Mock + private EdcHttpClient httpClient; + @Mock + private Oauth2TokenClient tokenClient; + @Mock + private TypeManager typeManager; + @Mock + private HttpAccessControlCheckClientConfig config; + @Mock + private HttpAccessControlCheckDtrClientConfig dtrConfig; + private AutoCloseable mocks; + + @BeforeEach + void setUp() { + mocks = MockitoAnnotations.openMocks( this ); + } + + @AfterEach + void tearDown() throws Exception { + mocks.close(); + } + + public static Stream urlMatcherSourceProvider() { + return Stream. builder() + .add( Arguments.of( "^abcd$", "abcd", true ) ) + .add( Arguments.of( "^abcd$", "ABCDabcd0123", false ) ) + .add( Arguments.of( "abcd", "ABCDabcd0123", true ) ) + .add( Arguments.of( "^[a-dA-D]+$", "Abcd", true ) ) + .build(); + } + + @ParameterizedTest + @MethodSource( "urlMatcherSourceProvider" ) + void test_IsAspectModelCall_ShouldReturnTrue_WhenUrlIsMatchingPattern( + final String pattern, final String url, final boolean expected ) { + //given + when( dtrConfig.getAspectModelUrlPattern() ).thenReturn( pattern ); + final var underTest = new DtrAccessVerificationClient( monitor, httpClient, tokenClient, typeManager, config, dtrConfig ); + //when + final boolean actual = underTest.isAspectModelCall( url ); + //then + assertThat( actual ).isEqualTo( expected ); + } + + @SuppressWarnings( "resource" ) + @ParameterizedTest + @ValueSource( ints = { 0, 1 } ) + void test_ShouldAllowAccess_ShouldReturnTrue_WhenDtrResponseIsSuccessful( + final int cacheForMinutes ) throws IOException { + //given + final Map additionalHeaders = Map.of( HEADER_EDC_BPN, BPN ); + when( dtrConfig.getDtrAccessVerificationUrl() ).thenReturn( LOCALHOST_ACCESS_VERIFICATION ); + when( dtrConfig.getDecisionCacheDurationMinutes() ).thenReturn( cacheForMinutes ); + when( config.getEdcDataPlaneBaseUrl() ).thenReturn( LOCALHOST_EDC_DATA_PROXY ); + when( dtrConfig.getOauth2TokenScope() ).thenReturn( SCOPE ); + when( tokenClient.getBearerToken( SCOPE ) ).thenReturn( DUMMY_TOKEN ); + final var requestMatcher = new RequestMatcher( + LOCALHOST_ACCESS_VERIFICATION, BPN, BEARER_PREFIX + DUMMY_TOKEN, + LOCALHOST_EDC_DATA_PROXY + REQUESTED_URI_PATH + "?" + REQUESTED_QUERY_STRING ); + when( typeManager.writeValueAsString( any( DtrAccessVerificationRequest.class ) ) ) + .thenReturn( REQUEST_FORMAT.formatted( LOCALHOST_EDC_DATA_PROXY + REQUESTED_URI_PATH + "?" + REQUESTED_QUERY_STRING ) ); + when( httpClient.execute( argThat( requestMatcher ) ) ).thenReturn( httpResponse ); + when( httpResponse.isSuccessful() ).thenReturn( true ); + final var underTest = new DtrAccessVerificationClient( monitor, httpClient, tokenClient, typeManager, config, dtrConfig ); + //when + final boolean actual = underTest.shouldAllowAccess( REQUESTED_URI_PATH, REQUESTED_QUERY_STRING, additionalHeaders ); + //then + assertThat( actual ).isTrue(); + verify( tokenClient ).getBearerToken( SCOPE ); + verify( httpClient ).execute( argThat( requestMatcher ) ); + } + + @SuppressWarnings( "resource" ) + @ParameterizedTest + @ValueSource( ints = { 0, 1 } ) + void test_ShouldAllowAccess_ShouldReturnTrue_WhenDtrResponseIsSuccessfulWithNoQueryParameter( + final int cacheForMinutes ) throws IOException { + //given + final Map additionalHeaders = Map.of( HEADER_EDC_BPN, BPN ); + when( dtrConfig.getDtrAccessVerificationUrl() ).thenReturn( LOCALHOST_ACCESS_VERIFICATION ); + when( dtrConfig.getDecisionCacheDurationMinutes() ).thenReturn( cacheForMinutes ); + when( config.getEdcDataPlaneBaseUrl() ).thenReturn( LOCALHOST_EDC_DATA_PROXY ); + when( dtrConfig.getOauth2TokenScope() ).thenReturn( SCOPE ); + when( tokenClient.getBearerToken( SCOPE ) ).thenReturn( DUMMY_TOKEN ); + final var requestMatcher = new RequestMatcher( + LOCALHOST_ACCESS_VERIFICATION, BPN, BEARER_PREFIX + DUMMY_TOKEN, + LOCALHOST_EDC_DATA_PROXY + REQUESTED_URI_PATH ); + when( typeManager.writeValueAsString( any( DtrAccessVerificationRequest.class ) ) ) + .thenReturn( REQUEST_FORMAT.formatted( LOCALHOST_EDC_DATA_PROXY + REQUESTED_URI_PATH ) ); + when( httpClient.execute( argThat( requestMatcher ) ) ).thenReturn( httpResponse ); + when( httpResponse.isSuccessful() ).thenReturn( true ); + final var underTest = new DtrAccessVerificationClient( monitor, httpClient, tokenClient, typeManager, config, dtrConfig ); + //when + final boolean actual = underTest.shouldAllowAccess( REQUESTED_URI_PATH, null, additionalHeaders ); + //then + assertThat( actual ).isTrue(); + verify( tokenClient ).getBearerToken( SCOPE ); + verify( httpClient ).execute( argThat( requestMatcher ) ); + } + + @SuppressWarnings( "resource" ) + @ParameterizedTest + @ValueSource( ints = { 0, 1 } ) + void test_ShouldAllowAccess_ShouldReturnFalse_WhenDtrResponseIsNotSuccessful( + final int cacheForMinutes ) throws IOException { + //given + final Map additionalHeaders = Map.of( HEADER_EDC_BPN, BPN ); + when( dtrConfig.getDtrAccessVerificationUrl() ).thenReturn( LOCALHOST_ACCESS_VERIFICATION ); + when( dtrConfig.getDecisionCacheDurationMinutes() ).thenReturn( cacheForMinutes ); + when( config.getEdcDataPlaneBaseUrl() ).thenReturn( LOCALHOST_EDC_DATA_PROXY ); + when( dtrConfig.getOauth2TokenScope() ).thenReturn( SCOPE ); + when( tokenClient.getBearerToken( SCOPE ) ).thenReturn( DUMMY_TOKEN ); + final var requestMatcher = new RequestMatcher( + LOCALHOST_ACCESS_VERIFICATION, BPN, BEARER_PREFIX + DUMMY_TOKEN, + LOCALHOST_EDC_DATA_PROXY + REQUESTED_URI_PATH + "?" + REQUESTED_QUERY_STRING ); + when( typeManager.writeValueAsString( any( DtrAccessVerificationRequest.class ) ) ) + .thenReturn( REQUEST_FORMAT.formatted( LOCALHOST_EDC_DATA_PROXY + REQUESTED_URI_PATH + "?" + REQUESTED_QUERY_STRING ) ); + when( httpClient.execute( argThat( requestMatcher ) ) ).thenReturn( httpResponse ); + when( httpResponse.isSuccessful() ).thenReturn( false ); + final var underTest = new DtrAccessVerificationClient( monitor, httpClient, tokenClient, typeManager, config, dtrConfig ); + //when + final boolean actual = underTest.shouldAllowAccess( REQUESTED_URI_PATH, REQUESTED_QUERY_STRING, additionalHeaders ); + //then + assertThat( actual ).isFalse(); + verify( tokenClient ).getBearerToken( SCOPE ); + verify( httpClient ).execute( argThat( requestMatcher ) ); + } + + @SuppressWarnings( "resource" ) + @ParameterizedTest + @ValueSource( ints = { 0, 1 } ) + void test_ShouldDenyAccess_ShouldThrowException_WhenTokenCannotBeObtained( + final int cacheForMinutes ) throws IOException { + //given + final Map additionalHeaders = Map.of( HEADER_EDC_BPN, BPN ); + when( dtrConfig.getDtrAccessVerificationUrl() ).thenReturn( LOCALHOST_ACCESS_VERIFICATION ); + when( dtrConfig.getDecisionCacheDurationMinutes() ).thenReturn( cacheForMinutes ); + when( config.getEdcDataPlaneBaseUrl() ).thenReturn( LOCALHOST_EDC_DATA_PROXY ); + when( dtrConfig.getOauth2TokenScope() ).thenReturn( SCOPE ); + when( tokenClient.getBearerToken( SCOPE ) ).thenReturn( null ); + final var underTest = new DtrAccessVerificationClient( monitor, httpClient, tokenClient, typeManager, config, dtrConfig ); + //when + assertThatExceptionOfType( AccessControlServiceException.class ) + .isThrownBy( () -> underTest.shouldAllowAccess( REQUESTED_URI_PATH, REQUESTED_QUERY_STRING, additionalHeaders ) ); + //then + verify( tokenClient ).getBearerToken( SCOPE ); + verify( httpClient, never() ).execute( any() ); + } + + @SuppressWarnings( "resource" ) + @ParameterizedTest + @ValueSource( ints = { 0, 1 } ) + void test_ShouldDenyAccess_ShouldThrowException_WhenDtrRequestResultsInException( + final int cacheForMinutes ) throws IOException { + //given + final Map additionalHeaders = Map.of( HEADER_EDC_BPN, BPN ); + when( dtrConfig.getDtrAccessVerificationUrl() ).thenReturn( LOCALHOST_ACCESS_VERIFICATION ); + when( dtrConfig.getDecisionCacheDurationMinutes() ).thenReturn( cacheForMinutes ); + when( config.getEdcDataPlaneBaseUrl() ).thenReturn( LOCALHOST_EDC_DATA_PROXY ); + when( dtrConfig.getOauth2TokenScope() ).thenReturn( SCOPE ); + when( tokenClient.getBearerToken( SCOPE ) ).thenReturn( DUMMY_TOKEN ); + final var requestMatcher = new RequestMatcher( + LOCALHOST_ACCESS_VERIFICATION, BPN, BEARER_PREFIX + DUMMY_TOKEN, + LOCALHOST_EDC_DATA_PROXY + REQUESTED_URI_PATH + "?" + REQUESTED_QUERY_STRING ); + when( typeManager.writeValueAsString( any( DtrAccessVerificationRequest.class ) ) ) + .thenReturn( REQUEST_FORMAT.formatted( LOCALHOST_EDC_DATA_PROXY + REQUESTED_URI_PATH + "?" + REQUESTED_QUERY_STRING ) ); + when( httpClient.execute( argThat( requestMatcher ) ) ).thenThrow( IOException.class ); + final var underTest = new DtrAccessVerificationClient( monitor, httpClient, tokenClient, typeManager, config, dtrConfig ); + //when + assertThatExceptionOfType( AccessControlServiceException.class ) + .isThrownBy( () -> underTest.shouldAllowAccess( REQUESTED_URI_PATH, REQUESTED_QUERY_STRING, additionalHeaders ) ); + //then + verify( tokenClient ).getBearerToken( SCOPE ); + verify( httpClient ).execute( any() ); + verify( httpResponse, never() ).isSuccessful(); + } + + private record RequestMatcher(String url, String bpn, String authorization, String targetUrl) implements ArgumentMatcher { + @SuppressWarnings( "DataFlowIssue" ) + @Override + public boolean matches( final Request request ) { + final boolean methodMatched = POST.equals( request.method() ); + final boolean urlMatched = url.equals( request.url().url().toString() ); + final boolean bpnHeaderFound = bpn.equals( request.header( HEADER_EDC_BPN ) ); + final boolean authHeaderFound = authorization.equals( request.header( AUTHORIZATION ) ); + final RequestBody body = request.body(); + final boolean contentTypeHeaderFound = (body.contentType().type() + "/" + body.contentType().subtype()).equals( request.header( CONTENT_TYPE ) ); + final String submodelEndpointUrl = bodyAsObject( body ).submodelEndpointUrl(); + final boolean bodyMatched = targetUrl.equals( submodelEndpointUrl ); + return methodMatched && urlMatched && bpnHeaderFound && authHeaderFound && contentTypeHeaderFound && bodyMatched; + } + + private DtrAccessVerificationRequest bodyAsObject( RequestBody body ) { + try { + final Buffer buffer = new Buffer(); + body.writeTo( buffer ); + final String string = buffer.readUtf8(); + return new ObjectMapper().readValue( string, DtrAccessVerificationRequest.class ); + } catch ( IOException e ) { + throw new RuntimeException( e ); + } + } + } +} diff --git a/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationRequestTest.java b/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationRequestTest.java new file mode 100644 index 00000000..c9ff10d6 --- /dev/null +++ b/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrAccessVerificationRequestTest.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.edc.spi.types.TypeManager; +import org.junit.jupiter.api.Test; + +class DtrAccessVerificationRequestTest { + + private final TypeManager typeManager = new TypeManager(); + + @Test + void test_JsonDeserialization_ShouldGetBackOriginalRequest_WhenCalledWithSerializedValidData() { + //given + final var request = new DtrAccessVerificationRequest( "http://edc-data-plane/url" ); + final var asString = typeManager.writeValueAsString( request ); + //when + final var actual = typeManager.readValue( asString, DtrAccessVerificationRequest.class ); + //then + assertThat( actual ).isEqualTo( request ); + } + + @Test + void test_JsonSerialization_ShouldProduceExpectedJsonFormat_WhenCalledWithValidData() { + //given + final var request = new DtrAccessVerificationRequest( "http://edc-data-plane/url" ); + //when + final var actual = typeManager.writeValueAsString( request ); + //then + assertThat( actual ).isEqualTo( "{\"submodelEndpointUrl\":\"http://edc-data-plane/url\"}" ); + } +} diff --git a/pom.xml b/pom.xml index e57beac7..742dfc8c 100644 --- a/pom.xml +++ b/pom.xml @@ -96,7 +96,7 @@ 4.19.1 - 0.5.2-20240321-SNAPSHOT + 0.5.2-20240327-SNAPSHOT 3.24.2