diff --git a/libraries/edc-extension/pom.xml b/libraries/edc-extension/pom.xml new file mode 100644 index 00000000..f3ac2e7b --- /dev/null +++ b/libraries/edc-extension/pom.xml @@ -0,0 +1,112 @@ + + + + + 4.0.0 + + org.eclipse.tractusx + digital-twin-registry + DEV-SNAPSHOT + ../../pom.xml + + + org.eclipse.tractusx.digital_twin_registry + dtr-edc-access-control-extension + Tractus-X Semantic Layer Digital Twin Registry Access Control Extension for Eclipse Dataspace Connector Dataplane + Module contains the EDC extension triggering access control calls to the Semantic Layer Digital Twin Registry Service's relevant API endpoint. + jar + + + ${organization} + ${url} + + + + + ${licence_name} + ${licence_url} + ${licence_distribution} + ${licence_comments} + + + + + + org.eclipse.edc + connector-core + + + org.eclipse.edc + data-plane-spi + + + org.eclipse.edc + data-plane-http-spi + + + org.apache.commons + commons-lang3 + + + com.github.ben-manes.caffeine + caffeine + + + org.junit.jupiter + junit-jupiter + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.github.git-commit-id + git-commit-id-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.plugins + maven-deploy-plugin + + + + diff --git a/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/AccessControlServiceException.java b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/AccessControlServiceException.java new file mode 100644 index 00000000..cf856e73 --- /dev/null +++ b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/AccessControlServiceException.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * 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; + +public class AccessControlServiceException extends RuntimeException { + + public AccessControlServiceException( final String message ) { + super( message ); + } + + public AccessControlServiceException( final Throwable cause ) { + super( cause ); + } +} diff --git a/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/DtrDataPlaneAccessControlServiceExtension.java b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/DtrDataPlaneAccessControlServiceExtension.java new file mode 100644 index 00000000..72e2837b --- /dev/null +++ b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/DtrDataPlaneAccessControlServiceExtension.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * 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; + +import org.eclipse.edc.connector.dataplane.spi.iam.DataPlaneAccessTokenService; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; + +@Extension( value = "Data Plane HTTP Access Control" ) +public class DtrDataPlaneAccessControlServiceExtension implements ServiceExtension { + + @Setting( value = "Contains the base URL of the EDC data plane endpoint where the data plane requests are sent by the end users." ) + public static final String EDC_DATA_PLANE_BASE_URL = "edc.granular.access.verification.edc.data.plane.baseUrl"; + @Setting( value = "Comma separated list of DTR configuration names used as keys for DTR clients." ) + public static final String EDC_DTR_CONFIG_NAMES = "edc.granular.access.verification.dtr.names"; + /** + * Prefix for individual DTR configurations. + */ + public static final String EDC_DTR_CONFIG_PREFIX = "edc.granular.access.verification.dtr.config."; + /** + * Configuration property suffix for the configuration of DTR decision cache. The cache is turned off if set to 0. + */ + public static final String DTR_DECISION_CACHE_MINUTES = "dtr.decision.cache.duration.minutes"; + /** + * Configuration property suffix for the pattern to allow for the recognition of aspect model requests which need + * to be handled by DTR access control. + */ + public static final String ASPECT_MODEL_URL_PATTERN = "aspect.model.url.pattern"; + /** + * Configuration property suffix for the URL where DTR can be reached. + */ + public static final String DTR_ACCESS_VERIFICATION_URL = "dtr.access.verification.endpoint.url"; + /** + * Configuration property suffix for the URL where OAUTH2 tokens can be obtained for the DTR requests. + */ + public static final String OAUTH2_TOKEN_ENDPOINT_URL = "oauth2.token.endpoint.url"; + /** + * Configuration property suffix for the scope we need to use for OAUTH2 token requests when we need to access DTR. + */ + public static final String OAUTH2_TOKEN_SCOPE = "oauth2.token.scope"; + /** + * Configuration property suffix for the client id we need to use for OAUTH2 token requests when we need to access DTR. + */ + public static final String OAUTH2_TOKEN_CLIENT_ID = "oauth2.token.clientId"; + + /** + * Configuration property suffix for the path where we can find the client secret in vault for the OAUTH2 token requests when we need to access DTR. + */ + public static final String OAUTH2_TOKEN_CLIENT_SECRET_PATH = "oauth2.token.clientSecret.path"; + @Inject + private Monitor monitor; + @Inject + private EdcHttpClient httpClient; + @Inject + private TypeManager typeManager; + @Inject + private Vault vault; + @Inject + private DataPlaneAccessTokenService dataPlaneAccessTokenService; + private HttpAccessControlCheckClientConfig config; + + @Override + public String name() { + return "DTR Data Plane Access Control Service"; + } + + @Override + public void initialize( final ServiceExtensionContext context ) { + monitor.info( "Initializing " + name() ); + config = new HttpAccessControlCheckClientConfig( context ); + } +} diff --git a/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckClientConfig.java b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckClientConfig.java new file mode 100644 index 00000000..547821ee --- /dev/null +++ b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckClientConfig.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * 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; + +import static org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.DtrDataPlaneAccessControlServiceExtension.*; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +public class HttpAccessControlCheckClientConfig { + + private final Map dtrClientConfigMap; + private final String edcDataPlaneBaseUrl; + + public HttpAccessControlCheckClientConfig( final ServiceExtensionContext context ) { + dtrClientConfigMap = Arrays.stream( context.getSetting( EDC_DTR_CONFIG_NAMES, "" ).split( "," ) ) + .filter( StringUtils::isNotBlank ) + .collect( Collectors.toUnmodifiableMap( Function.identity(), + name -> new HttpAccessControlCheckDtrClientConfig( + context.getConfig( EDC_DTR_CONFIG_PREFIX + name ) ) ) ); + edcDataPlaneBaseUrl = context.getSetting( EDC_DATA_PLANE_BASE_URL, null ); + } + + public Map getDtrClientConfigMap() { + return dtrClientConfigMap; + } + + public String getEdcDataPlaneBaseUrl() { + return edcDataPlaneBaseUrl; + } +} diff --git a/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckDtrClientConfig.java b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckDtrClientConfig.java new file mode 100644 index 00000000..d1cf2ca1 --- /dev/null +++ b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckDtrClientConfig.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * 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; + +import static org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.DtrDataPlaneAccessControlServiceExtension.*; + +import org.eclipse.edc.spi.system.configuration.Config; + +public class HttpAccessControlCheckDtrClientConfig { + + private final String aspectModelUrlPattern; + private final String dtrAccessVerificationUrl; + private final String oauth2TokenEndpointUrl; + private final String oauth2TokenScope; + private final String oauth2ClientId; + private final String oauth2ClientSecretPath; + private final int decisionCacheDurationMinutes; + + public HttpAccessControlCheckDtrClientConfig( final Config context ) { + aspectModelUrlPattern = context.getString( ASPECT_MODEL_URL_PATTERN, null ); + dtrAccessVerificationUrl = context.getString( DTR_ACCESS_VERIFICATION_URL, null ); + oauth2TokenEndpointUrl = context.getString( OAUTH2_TOKEN_ENDPOINT_URL, null ); + oauth2TokenScope = context.getString( OAUTH2_TOKEN_SCOPE, null ); + oauth2ClientId = context.getString( OAUTH2_TOKEN_CLIENT_ID, null ); + oauth2ClientSecretPath = context.getString( OAUTH2_TOKEN_CLIENT_SECRET_PATH, null ); + decisionCacheDurationMinutes = context.getInteger( DTR_DECISION_CACHE_MINUTES, 0 ); + } + + public String getAspectModelUrlPattern() { + return aspectModelUrlPattern; + } + + public String getDtrAccessVerificationUrl() { + return dtrAccessVerificationUrl; + } + + public String getOauth2TokenEndpointUrl() { + return oauth2TokenEndpointUrl; + } + + public String getOauth2TokenScope() { + return oauth2TokenScope; + } + + public String getOauth2ClientId() { + return oauth2ClientId; + } + + public String getOauth2ClientSecretPath() { + return oauth2ClientSecretPath; + } + + public int getDecisionCacheDurationMinutes() { + return decisionCacheDurationMinutes; + } +} diff --git a/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrOauth2TokenClient.java b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrOauth2TokenClient.java new file mode 100644 index 00000000..17e9980c --- /dev/null +++ b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrOauth2TokenClient.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * 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.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; + +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.security.Vault; +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.HttpAccessControlCheckDtrClientConfig; +import org.jetbrains.annotations.NotNull; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class DtrOauth2TokenClient implements Oauth2TokenClient { + + static final TypeReference> MAP_TYPE_REFERENCE = new TypeReference<>() { + }; + static final String CONTENT_TYPE = "Content-Type"; + static final String ACCESS_TOKEN = "access_token"; + private final Monitor monitor; + private final EdcHttpClient httpClient; + private final HttpAccessControlCheckDtrClientConfig dtrConfig; + private final TypeManager typeManager; + private final LoadingCache secretCache; + + public DtrOauth2TokenClient( + final Monitor monitor, + final EdcHttpClient httpClient, + final TypeManager typeManager, + final Vault vault, + final HttpAccessControlCheckDtrClientConfig dtrConfig ) { + this.monitor = monitor; + this.httpClient = httpClient; + this.typeManager = typeManager; + this.dtrConfig = dtrConfig; + this.secretCache = Caffeine.newBuilder() + .maximumSize( 1 ) + .expireAfterWrite( Duration.ofMinutes( 30 ) ) + .refreshAfterWrite( Duration.ofMinutes( 15 ) ) + .build( vault::resolveSecret ); + } + + @Override + public String getBearerToken( final String scope ) { + final Request request = createTokenRequest( scope ); + try ( Response response = httpClient.execute( request ) ) { + if ( !response.isSuccessful() ) { + throw new IllegalStateException( "OAuth2 authentication error. Response code=" + response.code() ); + } + final ResponseBody body = response.body(); + if ( body == null ) { + throw new IllegalStateException( "OAuth2 response body is empty." ); + } + final var map = typeManager.readValue( body.string(), MAP_TYPE_REFERENCE ); + if ( !map.containsKey( ACCESS_TOKEN ) ) { + throw new IllegalStateException( "OAuth2 response body had no token." ); + } + return map.get( ACCESS_TOKEN ); + } catch ( final Exception e ) { + monitor.severe( "Failed to obtain Bearer token: " + e.getMessage() ); + return null; + } + } + + @NotNull + private String fetchClientSecret() { + final String secret = secretCache.get( dtrConfig.getOauth2ClientSecretPath() ); + if ( secret == null ) { + throw new AccessControlServiceException( "Cannot resolve authentication credentials." ); + } + return secret; + } + + @NotNull + private Request createTokenRequest( final String scope ) { + final String secret = fetchClientSecret(); + final FormBody formBody = new FormBody( + List.of( "grant_type", "client_id", "client_secret", "scope" ), + List.of( "client_credentials", + URLEncoder.encode( dtrConfig.getOauth2ClientId(), StandardCharsets.UTF_8 ), + URLEncoder.encode( secret, StandardCharsets.UTF_8 ), + URLEncoder.encode( scope, StandardCharsets.UTF_8 ) ) ); + return new Request.Builder() + .url( dtrConfig.getOauth2TokenEndpointUrl() ) + .addHeader( "Accept", "application/json" ) + .addHeader( "Content-Type", formBody.contentType().toString() ) + .post( formBody ) + .build(); + } +} diff --git a/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/Oauth2TokenClient.java b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/Oauth2TokenClient.java new file mode 100644 index 00000000..4e5440e4 --- /dev/null +++ b/libraries/edc-extension/src/main/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/Oauth2TokenClient.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * 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 interface Oauth2TokenClient { + + String getBearerToken( String scope ); +} diff --git a/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckClientConfigTest.java b/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckClientConfigTest.java new file mode 100644 index 00000000..57cda353 --- /dev/null +++ b/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckClientConfigTest.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.DtrDataPlaneAccessControlServiceExtension.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.util.Map; + +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class HttpAccessControlCheckClientConfigTest { + private ServiceExtensionContext serviceExtensionContext; + private HttpAccessControlCheckClientConfig underTest; + + @BeforeEach + void setUp() { + serviceExtensionContext = mock(); + } + + @Test + void test_GetDtrClientConfigMap_ShouldReturnEmptyMap_WhenConfigurationIsNotSet() { + //given + final int expected = 1; + when( serviceExtensionContext.getSetting( EDC_DTR_CONFIG_NAMES, "" ) ).thenReturn( "" ); + underTest = new HttpAccessControlCheckClientConfig( serviceExtensionContext ); + //when + final var actual = underTest.getDtrClientConfigMap(); + //then + assertThat( actual ).isEqualTo( Map.of() ); + verify( serviceExtensionContext, never() ).getConfig( anyString() ); + } + + @Test + void test_GetEdcDataPlaneBaseUrl_ShouldReturnExpectedValue_WhenConfigurationWasSet() { + //given + final String expected = "http://edc-data-plane/proxy"; + when( serviceExtensionContext.getSetting( EDC_DTR_CONFIG_NAMES, "" ) ).thenReturn( "name" ); + when( serviceExtensionContext.getSetting( eq( EDC_DATA_PLANE_BASE_URL ), isNull() ) ).thenReturn( expected ); + when( serviceExtensionContext.getConfig( EDC_DTR_CONFIG_PREFIX + "name" ) ).thenReturn( mock() ); + underTest = new HttpAccessControlCheckClientConfig( serviceExtensionContext ); + //when + final String actual = underTest.getEdcDataPlaneBaseUrl(); + //then + assertThat( actual ).isEqualTo( expected ); + } +} diff --git a/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckDtrClientConfigTest.java b/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckDtrClientConfigTest.java new file mode 100644 index 00000000..3916a022 --- /dev/null +++ b/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/HttpAccessControlCheckDtrClientConfigTest.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.semantics.edc.dataplane.http.accesscontrol.DtrDataPlaneAccessControlServiceExtension.*; +import static org.mockito.Mockito.*; + +import org.eclipse.edc.spi.system.configuration.Config; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class HttpAccessControlCheckDtrClientConfigTest { + private Config config; + private HttpAccessControlCheckDtrClientConfig underTest; + + @BeforeEach + void setUp() { + config = mock(); + } + + @Test + void test_GetAspectModelUrlPattern_ShouldReturnExpectedValue_WhenConfigurationWasSet() { + //given + final String expected = "http://aspec-model/api"; + when( config.getString( eq( ASPECT_MODEL_URL_PATTERN ), isNull() ) ).thenReturn( expected ); + underTest = new HttpAccessControlCheckDtrClientConfig( config ); + //when + final String actual = underTest.getAspectModelUrlPattern(); + //then + assertThat( actual ).isEqualTo( expected ); + } + + @Test + void test_GetDtrAccessVerificationUrl_ShouldReturnExpectedValue_WhenConfigurationWasSet() { + //given + final String expected = "http://dtr/submodel-descriptor/authorized"; + when( config.getString( eq( DTR_ACCESS_VERIFICATION_URL ), isNull() ) ).thenReturn( expected ); + underTest = new HttpAccessControlCheckDtrClientConfig( config ); + //when + final String actual = underTest.getDtrAccessVerificationUrl(); + //then + assertThat( actual ).isEqualTo( expected ); + } + + @Test + void test_GetOauth2TokenEndpointUrl_ShouldReturnExpectedValue_WhenConfigurationWasSet() { + //given + final String expected = "http://oauth2/token"; + when( config.getString( eq( OAUTH2_TOKEN_ENDPOINT_URL ), isNull() ) ).thenReturn( expected ); + underTest = new HttpAccessControlCheckDtrClientConfig( config ); + //when + final String actual = underTest.getOauth2TokenEndpointUrl(); + //then + assertThat( actual ).isEqualTo( expected ); + } + + @Test + void test_GetOauth2TokenScope_ShouldReturnExpectedValue_WhenConfigurationWasSet() { + //given + final String expected = "aud:dtr-id"; + when( config.getString( eq( OAUTH2_TOKEN_SCOPE ), isNull() ) ).thenReturn( expected ); + underTest = new HttpAccessControlCheckDtrClientConfig( config ); + //when + final String actual = underTest.getOauth2TokenScope(); + //then + assertThat( actual ).isEqualTo( expected ); + } + + @Test + void test_GetOauth2ClientId_ShouldReturnExpectedValue_WhenConfigurationWasSet() { + //given + final String expected = "edc-client-id"; + when( config.getString( eq( OAUTH2_TOKEN_CLIENT_ID ), isNull() ) ).thenReturn( expected ); + underTest = new HttpAccessControlCheckDtrClientConfig( config ); + //when + final String actual = underTest.getOauth2ClientId(); + //then + assertThat( actual ).isEqualTo( expected ); + } + + @Test + void test_GetOauth2ClientSecretPath_ShouldReturnExpectedValue_WhenConfigurationWasSet() { + //given + final String expected = "edc-client-secret-path"; + when( config.getString( eq( OAUTH2_TOKEN_CLIENT_SECRET_PATH ), isNull() ) ).thenReturn( expected ); + underTest = new HttpAccessControlCheckDtrClientConfig( config ); + //when + final String actual = underTest.getOauth2ClientSecretPath(); + //then + assertThat( actual ).isEqualTo( expected ); + } +} diff --git a/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrOauth2TokenClientTest.java b/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrOauth2TokenClientTest.java new file mode 100644 index 00000000..0f1ae482 --- /dev/null +++ b/libraries/edc-extension/src/test/java/org/eclipse/tractusx/semantics/edc/dataplane/http/accesscontrol/client/DtrOauth2TokenClientTest.java @@ -0,0 +1,251 @@ +/******************************************************************************* + * 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.DtrOauth2TokenClient.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.security.Vault; +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.HttpAccessControlCheckDtrClientConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +class DtrOauth2TokenClientTest { + static final String POST = "POST"; + static final String SCOPE = "aud:dtr"; + static final String ENCODED_SCOPE = URLEncoder.encode( SCOPE, StandardCharsets.UTF_8 ); + static final String CLIENT_ID = "client:id"; + static final String ENCODED_CLIENT_ID = URLEncoder.encode( CLIENT_ID, StandardCharsets.UTF_8 ); + static final String CLIENT_SECRET_VAULT_PATH = "client_secret"; + static final String CLIENT_SECRET = "client:secret"; + static final String ENCODED_CLIENT_SECRET = URLEncoder.encode( CLIENT_SECRET, StandardCharsets.UTF_8 ); + static final String DUMMY_TOKEN = "dummy_token_value"; + static final String RESPONSE_BODY = "{\"" + ACCESS_TOKEN + "\": \"" + DUMMY_TOKEN + "\"}"; + static final String LOCALHOST_TOKEN_ENDPOINT = "https://localhost/token"; + @Mock + private Response httpResponse; + @Mock + private ResponseBody responseBody; + @Mock + private Monitor monitor; + @Mock + private EdcHttpClient httpClient; + @Mock + private Vault vault; + @Mock + private TypeManager typeManager; + @Mock + private HttpAccessControlCheckDtrClientConfig dtrConfig; + @InjectMocks + private DtrOauth2TokenClient underTest; + private AutoCloseable mocks; + + @BeforeEach + void setUp() { + mocks = MockitoAnnotations.openMocks( this ); + } + + @AfterEach + void tearDown() throws Exception { + mocks.close(); + } + + @SuppressWarnings( { "resource", "ResultOfMethodCallIgnored" } ) + @Test + void test_GetBearerToken_ShouldReturnToken_WhenOauth2ResponseContainsOne() throws IOException { + //given + when( dtrConfig.getOauth2ClientId() ).thenReturn( CLIENT_ID ); + when( dtrConfig.getOauth2TokenScope() ).thenReturn( SCOPE ); + when( dtrConfig.getOauth2ClientSecretPath() ).thenReturn( CLIENT_SECRET_VAULT_PATH ); + when( dtrConfig.getOauth2TokenEndpointUrl() ).thenReturn( LOCALHOST_TOKEN_ENDPOINT ); + when( vault.resolveSecret( CLIENT_SECRET_VAULT_PATH ) ).thenReturn( CLIENT_SECRET ); + final var requestMatcher = new RequestMatcher( + LOCALHOST_TOKEN_ENDPOINT, ENCODED_CLIENT_ID, ENCODED_CLIENT_SECRET, ENCODED_SCOPE ); + when( httpClient.execute( argThat( requestMatcher ) ) ).thenReturn( httpResponse ); + when( httpResponse.isSuccessful() ).thenReturn( true ); + when( httpResponse.body() ).thenReturn( responseBody ); + when( responseBody.string() ).thenReturn( RESPONSE_BODY ); + when( typeManager.readValue( RESPONSE_BODY, MAP_TYPE_REFERENCE ) ) + .thenReturn( new HashMap<>( Map.of( ACCESS_TOKEN, DUMMY_TOKEN ) ) ); + //when + final String actual = underTest.getBearerToken( SCOPE ); + //then + assertThat( actual ).isEqualTo( DUMMY_TOKEN ); + final var inOrder = inOrder( vault, httpClient, httpResponse, responseBody, typeManager, monitor ); + inOrder.verify( vault ).resolveSecret( CLIENT_SECRET_VAULT_PATH ); + inOrder.verify( httpClient ).execute( argThat( requestMatcher ) ); + inOrder.verify( httpResponse ).isSuccessful(); + inOrder.verify( httpResponse, atLeastOnce() ).body(); + inOrder.verify( responseBody ).string(); + inOrder.verify( typeManager ).readValue( anyString(), eq( MAP_TYPE_REFERENCE ) ); + } + + @SuppressWarnings( { "resource", "ResultOfMethodCallIgnored" } ) + @Test + void test_GetBearerToken_ShouldReturnNull_WhenOauth2ResponseIsNotSuccessful() throws IOException { + //given + when( dtrConfig.getOauth2ClientId() ).thenReturn( CLIENT_ID ); + when( dtrConfig.getOauth2TokenScope() ).thenReturn( SCOPE ); + when( dtrConfig.getOauth2ClientSecretPath() ).thenReturn( CLIENT_SECRET_VAULT_PATH ); + when( dtrConfig.getOauth2TokenEndpointUrl() ).thenReturn( LOCALHOST_TOKEN_ENDPOINT ); + when( vault.resolveSecret( CLIENT_SECRET_VAULT_PATH ) ).thenReturn( CLIENT_SECRET ); + final var requestMatcher = new RequestMatcher( + LOCALHOST_TOKEN_ENDPOINT, ENCODED_CLIENT_ID, ENCODED_CLIENT_SECRET, ENCODED_SCOPE ); + when( httpClient.execute( argThat( requestMatcher ) ) ).thenReturn( httpResponse ); + when( httpResponse.isSuccessful() ).thenReturn( false ); + //when + final String actual = underTest.getBearerToken( SCOPE ); + //then + assertThat( actual ).isNull(); + final var inOrder = inOrder( vault, httpClient, httpResponse, responseBody, typeManager, monitor ); + inOrder.verify( vault ).resolveSecret( CLIENT_SECRET_VAULT_PATH ); + inOrder.verify( httpClient ).execute( argThat( requestMatcher ) ); + inOrder.verify( httpResponse ).isSuccessful(); + inOrder.verify( monitor ).severe( anyString() ); + inOrder.verify( httpResponse, never() ).body(); + inOrder.verify( responseBody, never() ).string(); + inOrder.verify( typeManager, never() ).readValue( anyString(), eq( MAP_TYPE_REFERENCE ) ); + } + + @SuppressWarnings( { "resource", "ResultOfMethodCallIgnored" } ) + @Test + void test_GetBearerToken_ShouldReturnNull_WhenOauth2ResponseHasNoBody() throws IOException { + //given + when( dtrConfig.getOauth2ClientId() ).thenReturn( CLIENT_ID ); + when( dtrConfig.getOauth2TokenScope() ).thenReturn( SCOPE ); + when( dtrConfig.getOauth2ClientSecretPath() ).thenReturn( CLIENT_SECRET_VAULT_PATH ); + when( dtrConfig.getOauth2TokenEndpointUrl() ).thenReturn( LOCALHOST_TOKEN_ENDPOINT ); + when( vault.resolveSecret( CLIENT_SECRET_VAULT_PATH ) ).thenReturn( CLIENT_SECRET ); + final var requestMatcher = new RequestMatcher( + LOCALHOST_TOKEN_ENDPOINT, ENCODED_CLIENT_ID, ENCODED_CLIENT_SECRET, ENCODED_SCOPE ); + when( httpClient.execute( argThat( requestMatcher ) ) ).thenReturn( httpResponse ); + when( httpResponse.isSuccessful() ).thenReturn( true ); + when( httpResponse.body() ).thenReturn( null ); + //when + final String actual = underTest.getBearerToken( SCOPE ); + //then + assertThat( actual ).isNull(); + final var inOrder = inOrder( vault, httpClient, httpResponse, responseBody, typeManager, monitor ); + inOrder.verify( vault ).resolveSecret( CLIENT_SECRET_VAULT_PATH ); + inOrder.verify( httpClient ).execute( argThat( requestMatcher ) ); + inOrder.verify( httpResponse ).isSuccessful(); + inOrder.verify( httpResponse ).body(); + inOrder.verify( monitor ).severe( anyString() ); + inOrder.verify( responseBody, never() ).string(); + inOrder.verify( typeManager, never() ).readValue( anyString(), eq( MAP_TYPE_REFERENCE ) ); + } + + @SuppressWarnings( { "resource", "ResultOfMethodCallIgnored" } ) + @Test + void test_GetBearerToken_ShouldReturnNull_WhenOauth2ResponseHasNoTokenBody() throws IOException { + //given + when( dtrConfig.getOauth2ClientId() ).thenReturn( CLIENT_ID ); + when( dtrConfig.getOauth2TokenScope() ).thenReturn( SCOPE ); + when( dtrConfig.getOauth2ClientSecretPath() ).thenReturn( CLIENT_SECRET_VAULT_PATH ); + when( dtrConfig.getOauth2TokenEndpointUrl() ).thenReturn( LOCALHOST_TOKEN_ENDPOINT ); + when( vault.resolveSecret( CLIENT_SECRET_VAULT_PATH ) ).thenReturn( CLIENT_SECRET ); + final var requestMatcher = new RequestMatcher( + LOCALHOST_TOKEN_ENDPOINT, ENCODED_CLIENT_ID, ENCODED_CLIENT_SECRET, ENCODED_SCOPE ); + when( httpClient.execute( argThat( requestMatcher ) ) ).thenReturn( httpResponse ); + when( httpResponse.isSuccessful() ).thenReturn( true ); + when( httpResponse.body() ).thenReturn( responseBody ); + when( responseBody.string() ).thenReturn( "{}" ); + when( typeManager.readValue( "{}", MAP_TYPE_REFERENCE ) ) + .thenReturn( new HashMap<>() ); + //when + final String actual = underTest.getBearerToken( SCOPE ); + //then + assertThat( actual ).isNull(); + final var inOrder = inOrder( vault, httpClient, httpResponse, responseBody, typeManager, monitor ); + inOrder.verify( vault ).resolveSecret( CLIENT_SECRET_VAULT_PATH ); + inOrder.verify( httpClient ).execute( argThat( requestMatcher ) ); + inOrder.verify( httpResponse ).isSuccessful(); + inOrder.verify( httpResponse ).body(); + inOrder.verify( responseBody ).string(); + inOrder.verify( typeManager ).readValue( anyString(), eq( MAP_TYPE_REFERENCE ) ); + inOrder.verify( monitor ).severe( anyString() ); + } + + @SuppressWarnings( { "resource", "ResultOfMethodCallIgnored" } ) + @Test + void test_GetBearerToken_ShouldThrowException_WhenSecretCannotBeFetchedFromVault() throws IOException { + //given + when( dtrConfig.getOauth2ClientId() ).thenReturn( CLIENT_ID ); + when( dtrConfig.getOauth2TokenScope() ).thenReturn( SCOPE ); + when( dtrConfig.getOauth2ClientSecretPath() ).thenReturn( CLIENT_SECRET_VAULT_PATH ); + when( dtrConfig.getOauth2TokenEndpointUrl() ).thenReturn( LOCALHOST_TOKEN_ENDPOINT ); + when( vault.resolveSecret( CLIENT_SECRET_VAULT_PATH ) ).thenReturn( null ); + //when + assertThatExceptionOfType( AccessControlServiceException.class ).isThrownBy( () -> underTest.getBearerToken( SCOPE ) ); + //then + final var inOrder = inOrder( vault, httpClient, httpResponse, responseBody, typeManager, monitor ); + inOrder.verify( vault ).resolveSecret( CLIENT_SECRET_VAULT_PATH ); + inOrder.verify( httpClient, never() ).execute( any() ); + inOrder.verify( httpResponse, never() ).isSuccessful(); + inOrder.verify( httpResponse, never() ).body(); + inOrder.verify( responseBody, never() ).string(); + inOrder.verify( typeManager, never() ).readValue( anyString(), eq( MAP_TYPE_REFERENCE ) ); + } + + private record RequestMatcher(String url, String clientId, String clientSecret, String scope) 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 FormBody body = (FormBody) request.body(); + final boolean contentTypeHeaderFound = body.contentType().toString().equals( request.header( CONTENT_TYPE ) ); + final Map expectedParameterMap = Map.of( + "client_id", clientId, + "client_secret", clientSecret, + "scope", scope, + "grant_type", "client_credentials" ); + boolean bodyMatched = body.size() == expectedParameterMap.size(); + for ( int i = 0; i < body.size(); i++ ) { + bodyMatched &= expectedParameterMap.get( body.name( i ) ).equals( body.encodedValue( i ) ); + } + return methodMatched && urlMatched && contentTypeHeaderFound && bodyMatched; + } + } + +} diff --git a/pom.xml b/pom.xml index 2e9ac1dd..383985cb 100644 --- a/pom.xml +++ b/pom.xml @@ -95,6 +95,9 @@ 2.2.220 4.19.1 + + 0.5.2-20240321-SNAPSHOT + 3.24.2 5.9.3 @@ -107,6 +110,7 @@ backend access-control-service-interface access-control-service-sql-impl + libraries/edc-extension @@ -280,6 +284,23 @@ ${snakeyaml.version} + + + org.eclipse.edc + connector-core + ${edc.version} + + + org.eclipse.edc + data-plane-spi + ${edc.version} + + + org.eclipse.edc + data-plane-http-spi + ${edc.version} + + org.postgresql @@ -413,6 +434,11 @@ maven-central https://repo1.maven.org/maven2/ + + Maven Snapshots + maven-snapshots + https://oss.sonatype.org/content/repositories/snapshots/ +