diff --git a/edc-extensions/business-partner-validation/src/main/java/org/eclipse/tractusx/edc/validation/businesspartner/functions/AbstractBusinessPartnerValidation.java b/edc-extensions/business-partner-validation/src/main/java/org/eclipse/tractusx/edc/validation/businesspartner/functions/AbstractBusinessPartnerValidation.java index cd124684b..e31f16511 100644 --- a/edc-extensions/business-partner-validation/src/main/java/org/eclipse/tractusx/edc/validation/businesspartner/functions/AbstractBusinessPartnerValidation.java +++ b/edc-extensions/business-partner-validation/src/main/java/org/eclipse/tractusx/edc/validation/businesspartner/functions/AbstractBusinessPartnerValidation.java @@ -25,8 +25,8 @@ import org.eclipse.edc.policy.model.Operator; import org.eclipse.edc.spi.agent.ParticipantAgent; import org.eclipse.edc.spi.monitor.Monitor; +import org.jetbrains.annotations.Nullable; -import java.util.Map; import java.util.Objects; import static java.lang.String.format; @@ -102,20 +102,8 @@ protected boolean evaluate( monitor.debug(message); return false; } - - final ParticipantAgent participantAgent = policyContext.getParticipantAgent(); - final Map claims = participantAgent.getClaims(); - - if (!claims.containsKey(REFERRING_CONNECTOR_CLAIM)) { - return false; - } - - Object referringConnectorClaimObject = claims.get(REFERRING_CONNECTOR_CLAIM); - String referringConnectorClaim = null; - - if (referringConnectorClaimObject instanceof String) { - referringConnectorClaim = (String) referringConnectorClaimObject; - } + + var referringConnectorClaim = getReferringConnectorClaim(policyContext.getParticipantAgent()); if (referringConnectorClaim == null || referringConnectorClaim.isEmpty()) { return false; @@ -131,6 +119,24 @@ protected boolean evaluate( } } + @Nullable + private String getReferringConnectorClaim(ParticipantAgent participantAgent) { + Object referringConnectorClaimObject = null; + String referringConnectorClaim = null; + var claims = participantAgent.getClaims(); + + referringConnectorClaimObject = claims.get(REFERRING_CONNECTOR_CLAIM); + + if (referringConnectorClaimObject instanceof String) { + referringConnectorClaim = (String) referringConnectorClaimObject; + } + if (referringConnectorClaim == null) { + referringConnectorClaim = participantAgent.getIdentity(); + } + + return referringConnectorClaim; + } + private boolean isBusinessPartnerNumber(String referringConnectorClaim, Object businessPartnerNumber, PolicyContext policyContext) { if (businessPartnerNumber == null) { final String message = format(FAIL_EVALUATION_BECAUSE_RIGHT_VALUE_NOT_STRING, "null"); diff --git a/edc-extensions/ssi/ssi-identity-core/README.md b/edc-extensions/ssi/ssi-identity-core/README.md new file mode 100644 index 000000000..c483f2605 --- /dev/null +++ b/edc-extensions/ssi/ssi-identity-core/README.md @@ -0,0 +1,21 @@ +# SSI Core Identity Service Module + +This module contains an implementation of the EDC identity service for SSI. +The SsiIdentityService contains a `SsiTokenValidationService` for validating the `JWT` token, +that uses an implementation of `SsiCredentialClient` for validating the JWT token and then check custom rules registered in the `SsiValidationRuleRegistry` + +For obtaining the `JWT` token, the identity service also delegate to the `SsiCredentialClient` . + +The default implementation according to the first milestone [here](https://github.com/eclipse-tractusx/ssi-docu/tree/main/docs/architecture/cx-3-2) +will rely on an MIW and the implementations in available in the module `:edc-extensions:ssi:ssi-miw-credential-client`. + +The implementation also provide a rule registry `SsiValidationRuleRegistry` where custom rule can be registered for validating the `ClaimToken` extracted from the `JWT` token. + +Custom rule could be like: + +- Audience validation +- VP/VC validation +- Expiration +- ..etc + +This module it's still in development, but it will likely to contain also the Identity extractor from the `ClaimToken` diff --git a/edc-extensions/ssi/ssi-identity-core/build.gradle.kts b/edc-extensions/ssi/ssi-identity-core/build.gradle.kts new file mode 100644 index 000000000..4a7a3da37 --- /dev/null +++ b/edc-extensions/ssi/ssi-identity-core/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + implementation(project(":spi:ssi-spi")) + implementation(libs.edc.spi.core) + implementation(libs.edc.spi.jwt) + implementation(libs.edc.jwt.core) + implementation(libs.nimbus.jwt) + testImplementation(testFixtures(libs.edc.junit)) +} diff --git a/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityService.java b/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityService.java new file mode 100644 index 000000000..199600929 --- /dev/null +++ b/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityService.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.identity; + +import org.eclipse.edc.jwt.spi.TokenValidationService; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiCredentialClient; + +public class SsiIdentityService implements IdentityService { + + private final TokenValidationService tokenValidationService; + + private final SsiCredentialClient client; + + public SsiIdentityService(TokenValidationService tokenValidationService, SsiCredentialClient client) { + this.tokenValidationService = tokenValidationService; + this.client = client; + } + + @Override + public Result obtainClientCredentials(TokenParameters parameters) { + return client.obtainClientCredentials(parameters); + } + + @Override + public Result verifyJwtToken(TokenRepresentation tokenRepresentation, String audience) { + return tokenValidationService.validate(tokenRepresentation); + } +} diff --git a/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityServiceExtension.java b/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityServiceExtension.java new file mode 100644 index 000000000..da318196a --- /dev/null +++ b/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityServiceExtension.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.identity; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiCredentialClient; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiValidationRuleRegistry; + +@Provides({IdentityService.class, SsiValidationRuleRegistry.class}) +@Extension(SsiIdentityServiceExtension.EXTENSION_NAME) +public class SsiIdentityServiceExtension implements ServiceExtension { + + public static final String EXTENSION_NAME = "SSI Identity Service"; + + @Inject + private SsiCredentialClient credentialClient; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var validationRulesRegistry = new SsiValidationRulesRegistryImpl(); + context.registerService(SsiValidationRuleRegistry.class, validationRulesRegistry); + + var identityService = new SsiIdentityService(new SsiTokenValidationService(validationRulesRegistry, credentialClient), credentialClient); + + context.registerService(IdentityService.class, identityService); + } + +} diff --git a/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiTokenValidationService.java b/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiTokenValidationService.java new file mode 100644 index 000000000..80a99d915 --- /dev/null +++ b/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiTokenValidationService.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.identity; + +import org.eclipse.edc.jwt.spi.TokenValidationRulesRegistry; +import org.eclipse.edc.jwt.spi.TokenValidationService; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiCredentialClient; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +public class SsiTokenValidationService implements TokenValidationService { + + private final TokenValidationRulesRegistry rulesRegistry; + private final SsiCredentialClient credentialClient; + + public SsiTokenValidationService(TokenValidationRulesRegistry rulesRegistry, SsiCredentialClient credentialClient) { + this.rulesRegistry = rulesRegistry; + this.credentialClient = credentialClient; + } + + @Override + public Result validate(TokenRepresentation tokenRepresentation) { + return credentialClient.validate(tokenRepresentation) + .compose(claimToken -> checkRules(claimToken, tokenRepresentation.getAdditional())); + } + + private Result checkRules(ClaimToken claimToken, @Nullable Map additional) { + var errors = rulesRegistry.getRules().stream() + .map(r -> r.checkRule(claimToken, additional)) + .filter(Result::failed) + .map(Result::getFailureMessages) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + + if (!errors.isEmpty()) { + return Result.failure(errors); + } + return Result.success(claimToken); + } +} diff --git a/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiValidationRulesRegistryImpl.java b/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiValidationRulesRegistryImpl.java new file mode 100644 index 000000000..077c35862 --- /dev/null +++ b/edc-extensions/ssi/ssi-identity-core/src/main/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiValidationRulesRegistryImpl.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.identity; + +import org.eclipse.edc.jwt.TokenValidationRulesRegistryImpl; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiValidationRuleRegistry; + +public class SsiValidationRulesRegistryImpl extends TokenValidationRulesRegistryImpl implements SsiValidationRuleRegistry { +} diff --git a/edc-extensions/ssi/ssi-identity-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-extensions/ssi/ssi-identity-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..28d3cef1c --- /dev/null +++ b/edc-extensions/ssi/ssi-identity-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# 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 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation +# +# + +org.eclipse.tractusx.edc.iam.ssi.identity.SsiIdentityServiceExtension \ No newline at end of file diff --git a/edc-extensions/ssi/ssi-identity-core/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityServiceExtensionTest.java b/edc-extensions/ssi/ssi-identity-core/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityServiceExtensionTest.java new file mode 100644 index 000000000..94e9a08c5 --- /dev/null +++ b/edc-extensions/ssi/ssi-identity-core/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityServiceExtensionTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.identity; + +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.injection.ObjectFactory; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiCredentialClient; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiValidationRuleRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +@ExtendWith(DependencyInjectionExtension.class) +public class SsiIdentityServiceExtensionTest { + + SsiIdentityServiceExtension extension; + + ServiceExtensionContext context; + + @BeforeEach + void setup(ObjectFactory factory, ServiceExtensionContext context) { + this.context = spy(context); + context.registerService(SsiCredentialClient.class, mock(SsiCredentialClient.class)); + extension = factory.constructInstance(SsiIdentityServiceExtension.class); + } + + @Test + void initialize() { + extension.initialize(context); + + assertThat(context.getService(IdentityService.class)).isNotNull().isInstanceOf(SsiIdentityService.class); + assertThat(context.getService(SsiValidationRuleRegistry.class)).isNotNull().isInstanceOf(SsiValidationRulesRegistryImpl.class); + } +} diff --git a/edc-extensions/ssi/ssi-identity-core/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityServiceTest.java b/edc-extensions/ssi/ssi-identity-core/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityServiceTest.java new file mode 100644 index 000000000..7ce0d53f3 --- /dev/null +++ b/edc-extensions/ssi/ssi-identity-core/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiIdentityServiceTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.identity; + +import org.eclipse.edc.jwt.spi.TokenValidationService; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiCredentialClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SsiIdentityServiceTest { + + SsiCredentialClient credentialClient = mock(SsiCredentialClient.class); + TokenValidationService tokenValidationService = mock(TokenValidationService.class); + + SsiIdentityService identityService; + + @BeforeEach + void setup() { + identityService = new SsiIdentityService(tokenValidationService, credentialClient); + } + + @Test + void verifyJwtToken_success() { + var token = TokenRepresentation.Builder.newInstance().token("test").build(); + var claim = ClaimToken.Builder.newInstance().build(); + + when(tokenValidationService.validate(token)).thenReturn(Result.success(claim)); + + var result = identityService.verifyJwtToken(token, "audience"); + + assertThat(result).isNotNull().extracting(Result::getContent).isEqualTo(claim); + } + + @Test + void verifyJwtToken_failed() { + var token = TokenRepresentation.Builder.newInstance().token("test").build(); + var claim = ClaimToken.Builder.newInstance().build(); + + when(tokenValidationService.validate(token)).thenReturn(Result.failure("fail")); + + var result = identityService.verifyJwtToken(token, "audience"); + + assertThat(result).isNotNull().matches(Result::failed); + } + + + @Test + void obtainClientCredentials_success() { + var tokenParameters = TokenParameters.Builder.newInstance().audience("audience").build(); + var tokenRepresentation = TokenRepresentation.Builder.newInstance().token("test").build(); + + when(credentialClient.obtainClientCredentials(tokenParameters)).thenReturn(Result.success(tokenRepresentation)); + + var result = identityService.obtainClientCredentials(tokenParameters); + + assertThat(result).isNotNull().extracting(Result::getContent).isEqualTo(tokenRepresentation); + } + + @Test + void obtainClientCredentials_fail() { + var tokenParameters = TokenParameters.Builder.newInstance().audience("audience").build(); + + when(credentialClient.obtainClientCredentials(tokenParameters)).thenReturn(Result.failure("fail")); + + var result = identityService.obtainClientCredentials(tokenParameters); + + assertThat(result).isNotNull().matches(Result::failed); + } +} diff --git a/edc-extensions/ssi/ssi-identity-core/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiTokenValidationServiceTest.java b/edc-extensions/ssi/ssi-identity-core/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiTokenValidationServiceTest.java new file mode 100644 index 000000000..56f30c3eb --- /dev/null +++ b/edc-extensions/ssi/ssi-identity-core/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/SsiTokenValidationServiceTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.identity; + +import org.eclipse.edc.jwt.spi.TokenValidationRule; +import org.eclipse.edc.jwt.spi.TokenValidationRulesRegistry; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiCredentialClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public class SsiTokenValidationServiceTest { + + SsiCredentialClient credentialClient = mock(SsiCredentialClient.class); + TokenValidationRulesRegistry validationRulesRegistry = mock(TokenValidationRulesRegistry.class); + + SsiTokenValidationService validationService; + + @BeforeEach + void setup() { + validationService = new SsiTokenValidationService(validationRulesRegistry, credentialClient); + } + + @Test + void validate_success() { + var token = TokenRepresentation.Builder.newInstance().token("test").build(); + var rule = mock(TokenValidationRule.class); + var claim = ClaimToken.Builder.newInstance().build(); + + when(validationRulesRegistry.getRules()).thenReturn(List.of(rule)); + when(credentialClient.validate(token)).thenReturn(Result.success(claim)); + when(rule.checkRule(any(), any())).thenReturn(Result.success()); + + var result = validationService.validate(token); + + assertThat(result).isNotNull().extracting(Result::getContent).isEqualTo(claim); + + verify(credentialClient).validate(token); + verify(rule).checkRule(eq(claim), any()); + } + + @Test + void validate_fail_whenClientFails() { + var token = TokenRepresentation.Builder.newInstance().token("test").build(); + var rule = mock(TokenValidationRule.class); + + when(validationRulesRegistry.getRules()).thenReturn(List.of(rule)); + when(credentialClient.validate(token)).thenReturn(Result.failure("failure")); + when(rule.checkRule(any(), any())).thenReturn(Result.success()); + + var result = validationService.validate(token); + + assertThat(result).isNotNull().matches(Result::failed); + + verify(credentialClient).validate(token); + verifyNoInteractions(rule); + } + + @Test + void validate_fail_whenRuleFails() { + var token = TokenRepresentation.Builder.newInstance().token("test").build(); + var rule = mock(TokenValidationRule.class); + var claim = ClaimToken.Builder.newInstance().build(); + + + when(validationRulesRegistry.getRules()).thenReturn(List.of(rule)); + when(credentialClient.validate(token)).thenReturn(Result.success(claim)); + when(rule.checkRule(any(), any())).thenReturn(Result.failure("failure")); + + var result = validationService.validate(token); + + assertThat(result).isNotNull().matches(Result::failed); + + verify(credentialClient).validate(token); + verify(rule).checkRule(eq(claim), any()); + } +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/README.md b/edc-extensions/ssi/ssi-miw-credential-client/README.md new file mode 100644 index 000000000..0b9b73ad2 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/README.md @@ -0,0 +1,14 @@ +# MIW Client Credential Module + +This module contains an implementation of the `SsiCredentialClient` interface for SSI. +It basically narrow down to two operations: + +- obtaining a token for protocol communication +- validating the token + +For validating the token accordingly to the first milestone [here](https://github.com/eclipse-tractusx/ssi-docu/tree/main/docs/architecture/cx-3-2), the implemetation +just call the MIW for checking that the token and the VP claim inside are correct. Then extract the `JWT` claims into the `ClaimToken` for further checks. + +For obtaining a `JWT` token also it reaches the MIW, that will create a token with the `VP` claim inside. + +The MIW interaction in this first implementation is still WIP, since the MIW interface it's not stable or complete yet. diff --git a/edc-extensions/ssi/ssi-miw-credential-client/build.gradle.kts b/edc-extensions/ssi/ssi-miw-credential-client/build.gradle.kts new file mode 100644 index 000000000..8416d8712 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/build.gradle.kts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + implementation(project(":spi:ssi-spi")) + implementation(libs.edc.spi.core) + implementation(libs.edc.spi.http) + implementation(libs.nimbus.jwt) + testImplementation(testFixtures(libs.edc.junit)) +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtension.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtension.java new file mode 100644 index 000000000..92dfcc555 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtension.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.miw; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +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.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient; +import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClientImpl; + + +@Extension(SsiMiwApiClientExtension.EXTENSION_NAME) +public class SsiMiwApiClientExtension implements ServiceExtension { + + public static final String EXTENSION_NAME = "SSI MIW Api Client"; + + @Setting(value = "MIW API base url") + public static final String MIW_BASE_URL = "tx.ssi.miw.url"; + + @Inject + private EdcHttpClient httpClient; + + @Inject + private TypeManager typeManager; + + @Inject + private Monitor monitor; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Provider + public MiwApiClient apiClient(ServiceExtensionContext context) { + var baseUrl = context.getConfig().getString(MIW_BASE_URL); + + return new MiwApiClientImpl(httpClient, baseUrl, typeManager.getMapper(), monitor); + } + +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwCredentialClientExtension.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwCredentialClientExtension.java new file mode 100644 index 000000000..563874a62 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwCredentialClientExtension.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.miw; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient; +import org.eclipse.tractusx.edc.iam.ssi.miw.credentials.SsiMiwCredentialClient; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiCredentialClient; + +@Extension(SsiMiwCredentialClientExtension.EXTENSION_NAME) +public class SsiMiwCredentialClientExtension implements ServiceExtension { + + public static final String EXTENSION_NAME = "SSI MIW Credential Client"; + + @Inject + private MiwApiClient apiClient; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Provider + public SsiCredentialClient credentialVerifier() { + return new SsiMiwCredentialClient(apiClient); + } + +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/api/MiwApiClient.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/api/MiwApiClient.java new file mode 100644 index 000000000..ddccfa3ac --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/api/MiwApiClient.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.miw.api; + +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.result.Result; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@ExtensionPoint +public interface MiwApiClient { + String VP = "vp"; + + Result>> getCredentials(Set types, String holderIdentifier); + + Result> createPresentation(List> credentials, String holderIdentifier); + + Result verifyPresentation(String jwtPresentation); + +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/api/MiwApiClientImpl.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/api/MiwApiClientImpl.java new file mode 100644 index 000000000..734f17910 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/api/MiwApiClientImpl.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.miw.api; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static java.lang.String.format; + +public class MiwApiClientImpl implements MiwApiClient { + + public static final MediaType TYPE_JSON = MediaType.parse("application/json"); + private static final String CREDENTIAL_PATH = "/api/credentials"; + private static final String PRESENTATIONS_PATH = "/api/presentations"; + + private final EdcHttpClient httpClient; + private final String baseUrl; + private final ObjectMapper mapper; + private final Monitor monitor; + + public MiwApiClientImpl(EdcHttpClient httpClient, String baseUrl, ObjectMapper mapper, Monitor monitor) { + this.httpClient = httpClient; + this.baseUrl = baseUrl; + this.mapper = mapper; + this.monitor = monitor; + } + + @Override + public Result>> getCredentials(Set types, String holderIdentifier) { + + var params = new ArrayList(); + params.add(format("holderIdentifier=%s", holderIdentifier)); + + if (!types.isEmpty()) { + params.add(format("type=%s", String.join(",", types))); + } + + var queryParams = "?" + String.join("&", params); + var url = baseUrl + CREDENTIAL_PATH + queryParams; + var request = new Request.Builder().get().url(url).build(); + + return executeRequest(request, new TypeReference<>() { + }); + } + + @Override + public Result> createPresentation(List> credentials, String holderIdentifier) { + try { + var body = Map.of("holderIdentifier", holderIdentifier, "verifiableCredentials", credentials); + var url = baseUrl + PRESENTATIONS_PATH + "?asJwt=true"; + var requestBody = RequestBody.create(mapper.writeValueAsString(body), TYPE_JSON); + var request = new Request.Builder().post(requestBody).url(url).build(); + + return executeRequest(request, new TypeReference<>() { + }); + } catch (JsonProcessingException e) { + return Result.failure(e.getMessage()); + } + } + + private Result executeRequest(Request request, TypeReference typeReference) { + try (var response = httpClient.execute(request)) { + return handleResponse(response, typeReference); + } catch (IOException e) { + return Result.failure(e.getMessage()); + } + } + + @Override + public Result verifyPresentation(String jwtPresentation) { + return Result.success(); + } + + private Result handleResponse(Response response, TypeReference tr) { + if (response.isSuccessful()) { + return handleSuccess(response, tr); + } else { + return handleError(response); + } + } + + private Result handleSuccess(Response response, TypeReference tr) { + try { + var body = Objects.requireNonNull(response.body()).string(); + return Result.success(mapper.readValue(body, tr)); + } catch (IOException e) { + monitor.debug("Failed to parse response from MIW"); + return Result.failure(e.getMessage()); + } + } + + private Result handleError(Response response) { + var msg = format("MIW API returned %s", response.code()); + monitor.debug(msg); + return Result.failure(msg); + } + +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/credentials/SsiMiwCredentialClient.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/credentials/SsiMiwCredentialClient.java new file mode 100644 index 000000000..d8f8f0712 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/credentials/SsiMiwCredentialClient.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.miw.credentials; + +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiCredentialClient; + +import java.text.ParseException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient.VP; + +public class SsiMiwCredentialClient implements SsiCredentialClient { + + private final MiwApiClient apiClient; + + public SsiMiwCredentialClient(MiwApiClient apiClient) { + this.apiClient = apiClient; + } + + @Override + public Result obtainClientCredentials(TokenParameters parameters) { + // TODO will need to take from the TokenParameters which are the credentials needed, REF https://github.com/eclipse-edc/Connector/pull/3150 + return apiClient.getCredentials(Set.of(), parameters.getAudience()) + .compose(credentials -> createPresentation(credentials, parameters)) + .compose(this::createToken); + } + + @Override + public Result validate(TokenRepresentation tokenRepresentation) { + return extractClaims(tokenRepresentation) + .compose(claimToken -> validatePresentation(claimToken, tokenRepresentation)); + } + + private Result createToken(Map presentationResponse) { + var vp = presentationResponse.get(VP); + if (vp instanceof String) { + return Result.success(TokenRepresentation.Builder.newInstance().token((String) vp).build()); + } else { + return Result.failure("Missing or invalid format for Verifiable Presentation"); + } + } + + private Result> createPresentation(List> credentials, TokenParameters tokenParameters) { + if (!credentials.isEmpty()) { + return apiClient.createPresentation(credentials, tokenParameters.getAudience()); + } else { + return Result.failure("Cannot create a presentation from an empty credentials list"); + } + } + + private Result validatePresentation(ClaimToken claimToken, TokenRepresentation tokenRepresentation) { + return apiClient.verifyPresentation(tokenRepresentation.getToken()) + .compose(v -> Result.success(claimToken)); + } + + private Result extractClaims(TokenRepresentation tokenRepresentation) { + try { + var jwt = SignedJWT.parse(tokenRepresentation.getToken()); + + var tokenBuilder = ClaimToken.Builder.newInstance(); + jwt.getJWTClaimsSet().getClaims().entrySet().stream() + .filter(entry -> entry.getValue() != null) + .forEach(entry -> tokenBuilder.claim(entry.getKey(), entry.getValue())); + + return Result.success(tokenBuilder.build()); + } catch (ParseException e) { + return Result.failure(e.getMessage()); + } + } + +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-extensions/ssi/ssi-miw-credential-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..d254b0d87 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,16 @@ +# +# Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# 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 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation +# +# + +org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwCredentialClientExtension +org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwApiClientExtension \ No newline at end of file diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtensionTest.java b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtensionTest.java new file mode 100644 index 000000000..8d85661ab --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtensionTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.miw; + +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.edc.spi.system.injection.ObjectFactory; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient; +import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClientImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwApiClientExtension.MIW_BASE_URL; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(DependencyInjectionExtension.class) +public class SsiMiwApiClientExtensionTest { + + SsiMiwApiClientExtension extension; + + ServiceExtensionContext context; + + @BeforeEach + void setup(ObjectFactory factory, ServiceExtensionContext context) { + this.context = spy(context); + context.registerService(MiwApiClient.class, mock(MiwApiClient.class)); + context.registerService(TypeManager.class, new TypeManager()); + extension = factory.constructInstance(SsiMiwApiClientExtension.class); + } + + @Test + void initialize() { + var config = mock(Config.class); + when(context.getConfig()).thenReturn(config); + when(config.getString(MIW_BASE_URL)).thenReturn("url"); + + assertThat(extension.apiClient(context)).isInstanceOf(MiwApiClientImpl.class); + verify(config).getString(MIW_BASE_URL); + } +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwCredentialClientExtensionTest.java b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwCredentialClientExtensionTest.java new file mode 100644 index 000000000..f156ae0f1 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwCredentialClientExtensionTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.miw; + +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.injection.ObjectFactory; +import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient; +import org.eclipse.tractusx.edc.iam.ssi.miw.credentials.SsiMiwCredentialClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@ExtendWith(DependencyInjectionExtension.class) +public class SsiMiwCredentialClientExtensionTest { + + SsiMiwCredentialClientExtension extension; + + @BeforeEach + void setup(ObjectFactory factory, ServiceExtensionContext context) { + context.registerService(MiwApiClient.class, mock(MiwApiClient.class)); + extension = factory.constructInstance(SsiMiwCredentialClientExtension.class); + } + + @Test + void initialize() { + assertThat(extension.credentialVerifier()).isInstanceOf(SsiMiwCredentialClient.class); + } + +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/credentials/SsiMiwCredentialClientTest.java b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/credentials/SsiMiwCredentialClientTest.java new file mode 100644 index 000000000..d0d36f15e --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/credentials/SsiMiwCredentialClientTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.miw.credentials; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.security.PrivateKey; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient.VP; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public class SsiMiwCredentialClientTest { + + SsiMiwCredentialClient credentialClient; + MiwApiClient apiClient = mock(MiwApiClient.class); + private RSAKey key; + + @BeforeEach + void setup() throws JOSEException { + credentialClient = new SsiMiwCredentialClient(apiClient); + key = testKey(); + } + + @Test + void validate_success() throws JOSEException { + var claims = createClaims(Instant.now()); + var jwt = createJwt(UUID.randomUUID().toString(), claims, key.toPrivateKey()); + when(apiClient.verifyPresentation(jwt)).thenReturn(Result.success()); + + var result = credentialClient.validate(TokenRepresentation.Builder.newInstance().token(jwt).build()); + + assertThat(result).isNotNull().matches(Result::succeeded); + verify(apiClient).verifyPresentation(jwt); + } + + @Test + void validate_success_whenClientFails() throws JOSEException { + var claims = createClaims(Instant.now()); + var jwt = createJwt(UUID.randomUUID().toString(), claims, key.toPrivateKey()); + when(apiClient.verifyPresentation(jwt)).thenReturn(Result.failure("fail")); + + var result = credentialClient.validate(TokenRepresentation.Builder.newInstance().token(jwt).build()); + + assertThat(result).isNotNull().matches(Result::failed); + verify(apiClient).verifyPresentation(jwt); + } + + @Test + void validate_fail_whenInvalidToken() throws JOSEException { + + var result = credentialClient.validate(TokenRepresentation.Builder.newInstance().token("invalid").build()); + + assertThat(result).isNotNull().matches(Result::failed); + verifyNoInteractions(apiClient); + } + + @Test + void obtainCredentials_success() { + + var audience = "test"; + var jwt = "serialized"; + Map credential = Map.of(); + Map presentation = Map.of(VP, jwt); + + var credentials = List.of(credential); + + when(apiClient.getCredentials(Set.of(), audience)).thenReturn(Result.success(credentials)); + when(apiClient.createPresentation(credentials, audience)).thenReturn(Result.success(presentation)); + var result = credentialClient.obtainClientCredentials(TokenParameters.Builder.newInstance().audience(audience).build()); + + assertThat(result).isNotNull() + .extracting(Result::getContent) + .extracting(TokenRepresentation::getToken) + .isEqualTo(jwt); + + verify(apiClient).getCredentials(Set.of(), audience); + } + + private JWTClaimsSet createClaims(Instant exp) { + return new JWTClaimsSet.Builder() + .claim("foo", "bar") + .expirationTime(Date.from(exp)) + .build(); + } + + private String createJwt(String publicKeyId, JWTClaimsSet claimsSet, PrivateKey pk) { + var header = new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(publicKeyId).build(); + try { + SignedJWT jwt = new SignedJWT(header, claimsSet); + jwt.sign(new RSASSASigner(pk)); + return jwt.serialize(); + } catch (JOSEException e) { + throw new AssertionError(e); + } + } + + private RSAKey testKey() throws JOSEException { + return new RSAKeyGenerator(2048) + .keyUse(KeyUse.SIGNATURE) // indicate the intended use of the key + .keyID(UUID.randomUUID().toString()) // give the key a unique ID + .generate(); + } +} diff --git a/edc-tests/e2e-tests/build.gradle.kts b/edc-tests/e2e-tests/build.gradle.kts index 63181a7ca..4b4c9deea 100644 --- a/edc-tests/e2e-tests/build.gradle.kts +++ b/edc-tests/e2e-tests/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { testImplementation(project(":edc-extensions:control-plane-adapter-api")) testImplementation(libs.okhttp.mockwebserver) testImplementation(libs.restAssured) + testImplementation(libs.nimbus.jwt) testImplementation(libs.postgres) testImplementation(libs.awaitility) testImplementation(libs.aws.s3) @@ -38,8 +39,10 @@ dependencies { testImplementation(libs.edc.dsp) testImplementation(testFixtures(libs.edc.sql.core)) + testCompileOnly(project(":edc-tests:runtime:extensions")) testCompileOnly(project(":edc-tests:runtime:runtime-memory")) + testCompileOnly(project(":edc-tests:runtime:runtime-memory-ssi")) testCompileOnly(project(":edc-tests:runtime:runtime-postgresql")) } diff --git a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/ParticipantRuntime.java b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/ParticipantRuntime.java index 4f6c9f0ff..9feae6c68 100644 --- a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/ParticipantRuntime.java +++ b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/ParticipantRuntime.java @@ -34,7 +34,9 @@ public class ParticipantRuntime extends EdcRuntimeExtension implements BeforeAll public ParticipantRuntime(String moduleName, String runtimeName, String bpn, Map properties) { super(moduleName, runtimeName, properties); - this.registerServiceMock(IdentityService.class, new MockDapsService(bpn)); + if (!properties.containsKey("tx.ssi.miw.url")) { + this.registerServiceMock(IdentityService.class, new MockDapsService(bpn)); + } } @Override diff --git a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/TestRuntimeConfiguration.java b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/TestRuntimeConfiguration.java index ba0a1b20a..22a201050 100644 --- a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/TestRuntimeConfiguration.java +++ b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/TestRuntimeConfiguration.java @@ -31,6 +31,7 @@ public class TestRuntimeConfiguration { public static final String PLATO_NAME = "PLATO"; public static final String PLATO_BPN = PLATO_NAME + BPN_SUFFIX; public static final Integer PLATO_PROXIED_AAS_BACKEND_PORT = getFreePort(); + public static final int MIW_PORT = getFreePort(); static final String DSP_PATH = "/api/v1/dsp"; static final int PLATO_CONNECTOR_PORT = getFreePort(); static final int PLATO_MANAGEMENT_PORT = getFreePort(); @@ -49,9 +50,9 @@ public class TestRuntimeConfiguration { static final String PLATO_DATAPLANE_CONTROL_PORT = String.valueOf(getFreePort()); static final String PLATO_DATAPLANE_PROXY_PORT = String.valueOf(getFreePort()); static final String SOKRATES_DATAPLANE_CONTROL_PORT = String.valueOf(getFreePort()); - static final String SOKRATES_DATAPLANE_PROXY_PORT = String.valueOf(getFreePort()); - + static final String MIW_URL = "http://localhost:" + MIW_PORT; + public static Map sokratesPostgresqlConfiguration() { var baseConfiguration = sokratesConfiguration(); var postgresConfiguration = postgresqlConfiguration(SOKRATES_NAME.toLowerCase()); @@ -98,6 +99,17 @@ public static Map postgresqlConfiguration(String name) { }; } + public static Map sokratesSsiConfiguration() { + var ssiConfiguration = new HashMap() { + { + put("tx.ssi.miw.url", MIW_URL); + } + }; + var baseConfiguration = sokratesConfiguration(); + ssiConfiguration.putAll(baseConfiguration); + return ssiConfiguration; + } + public static Map sokratesConfiguration() { return new HashMap<>() { { @@ -162,6 +174,17 @@ public static Map platoConfiguration() { }; } + public static Map platoSsiConfiguration() { + var ssiConfiguration = new HashMap() { + { + put("tx.ssi.miw.url", MIW_URL); + } + }; + var baseConfiguration = platoConfiguration(); + ssiConfiguration.putAll(baseConfiguration); + return ssiConfiguration; + } + @NotNull public static String jdbcUrl(String name) { return PostgresqlLocalInstance.JDBC_URL_PREFIX + name; diff --git a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/catalog/SsiCatalogInMemoryTest.java b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/catalog/SsiCatalogInMemoryTest.java new file mode 100644 index 000000000..0a683c0db --- /dev/null +++ b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/catalog/SsiCatalogInMemoryTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.tests.catalog; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.tractusx.edc.lifecycle.ParticipantRuntime; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import static java.lang.String.format; +import static org.eclipse.tractusx.edc.lifecycle.TestRuntimeConfiguration.MIW_PORT; +import static org.eclipse.tractusx.edc.lifecycle.TestRuntimeConfiguration.PLATO_BPN; +import static org.eclipse.tractusx.edc.lifecycle.TestRuntimeConfiguration.PLATO_NAME; +import static org.eclipse.tractusx.edc.lifecycle.TestRuntimeConfiguration.SOKRATES_BPN; +import static org.eclipse.tractusx.edc.lifecycle.TestRuntimeConfiguration.SOKRATES_NAME; +import static org.eclipse.tractusx.edc.lifecycle.TestRuntimeConfiguration.platoSsiConfiguration; +import static org.eclipse.tractusx.edc.lifecycle.TestRuntimeConfiguration.sokratesSsiConfiguration; + +@EndToEndTest +public class SsiCatalogInMemoryTest extends AbstractCatalogTest { + + @RegisterExtension + protected static final ParticipantRuntime SOKRATES_RUNTIME = new ParticipantRuntime( + ":edc-tests:runtime:runtime-memory-ssi", + SOKRATES_NAME, + SOKRATES_BPN, + sokratesSsiConfiguration() + ); + @RegisterExtension + protected static final ParticipantRuntime PLATO_RUNTIME = new ParticipantRuntime( + ":edc-tests:runtime:runtime-memory-ssi", + PLATO_NAME, + PLATO_BPN, + platoSsiConfiguration() + ); + MockWebServer server = new MockWebServer(); + + @BeforeEach + void setup() throws IOException { + server.start(MIW_PORT); + server.setDispatcher(new MiwDispatcher(PLATO_BPN)); + } + + @AfterEach + void teardown() throws IOException { + server.shutdown(); + } + + private static final class MiwDispatcher extends Dispatcher { + + private static final TypeManager MAPPER = new TypeManager(); + + private static final String SUMMARY_JSON; + + static { + + var classloader = Thread.currentThread().getContextClassLoader(); + + try (var jsonStream = classloader.getResourceAsStream("summary-vc.json")) { + Objects.requireNonNull(jsonStream); + SUMMARY_JSON = new String(jsonStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private final String bpn; + + private final Map summaryVc; + + private MiwDispatcher(String bpn) { + this.bpn = bpn; + var json = format(SUMMARY_JSON, bpn); + summaryVc = MAPPER.readValue(json, new TypeReference<>() { + }); + } + + @NotNull + @Override + public MockResponse dispatch(@NotNull RecordedRequest recordedRequest) throws InterruptedException { + return switch (recordedRequest.getPath().split("\\?")[0]) { + case "/api/credentials" -> credentialResponse(); + case "/api/presentations" -> presentationResponse(); + default -> new MockResponse().setResponseCode(404); + }; + } + + private MockResponse credentialResponse() { + return new MockResponse().setBody(MAPPER.writeValueAsString(List.of(summaryVc))); + } + + private MockResponse presentationResponse() { + try { + var jwt = createJwt(UUID.randomUUID().toString(), createClaims(Instant.now(), Map.of("verifiableCredential", List.of(summaryVc))), testKey().toPrivateKey()); + return new MockResponse().setBody(MAPPER.writeValueAsString(Map.of("vp", jwt))); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + + private JWTClaimsSet createClaims(Instant exp, Map presentation) { + return new JWTClaimsSet.Builder() + .claim("vp", presentation) + .expirationTime(Date.from(exp)) + .build(); + } + + private String createJwt(String publicKeyId, JWTClaimsSet claimsSet, PrivateKey pk) { + var header = new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(publicKeyId).build(); + try { + SignedJWT jwt = new SignedJWT(header, claimsSet); + jwt.sign(new RSASSASigner(pk)); + return jwt.serialize(); + } catch (JOSEException e) { + throw new AssertionError(e); + } + } + + private RSAKey testKey() throws JOSEException { + return new RSAKeyGenerator(2048) + .keyUse(KeyUse.SIGNATURE) // indicate the intended use of the key + .keyID(UUID.randomUUID().toString()) // give the key a unique ID + .generate(); + } + } +} diff --git a/edc-tests/e2e-tests/src/test/resources/summary-vc.json b/edc-tests/e2e-tests/src/test/resources/summary-vc.json new file mode 100644 index 000000000..5d47eb5ce --- /dev/null +++ b/edc-tests/e2e-tests/src/test/resources/summary-vc.json @@ -0,0 +1,38 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "urn:uuid:12345678-1234-1234-1234-123456789abc", + "type": [ + "VerifiableCredential", + "SummaryCredential" + ], + "issuer": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000", + "issuanceDate": "2023-06-02T12:00:00Z", + "expirationDate": "2022-06-16T18:56:59Z", + "credentialSubject": { + "id": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000", + "holderIdentifier": "%s", + "type": "Summary-List", + "name": "CX-Credentials", + "items": [ + "cx-active-member", + "cx-dismantler", + "cx-pcf", + "cx-sustainability", + "cx-quality", + "cx-traceability", + "cx-behavior-twin", + "cx-bpn" + ], + "contract-templates": "https://public.catena-x.org/contracts/" + }, + "proof": { + "type": "Ed25519Signature2018", + "created": "2023-06-02T12:00:00Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:web:example.com#key-1", + "jws": "eyJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2MjM1NzA3NDEsImV4cCI6MTYyMzU3NDM0MSwianRpIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5YWJjIiwicHJvb2YiOnsiaWQiOiJkaWQ6d2ViOmV4YW1wbGUuY29tIiwibmFtZSI6IkJlaXNwaWVsLU9yZ2FuaXNhdGlvbiJ9fQ.SignedExampleSignature" + } +} \ No newline at end of file diff --git a/edc-tests/runtime/extensions/build.gradle.kts b/edc-tests/runtime/extensions/build.gradle.kts index b9554f001..613db089b 100644 --- a/edc-tests/runtime/extensions/build.gradle.kts +++ b/edc-tests/runtime/extensions/build.gradle.kts @@ -18,8 +18,9 @@ plugins { dependencies { - + implementation(libs.edc.core.controlplane) + implementation(libs.edc.util) // for the controller implementation(libs.jakarta.rsApi) } diff --git a/edc-tests/runtime/extensions/src/main/java/org/eclipse/tractusx/edc/lifecycle/ConsumerServicesExtension.java b/edc-tests/runtime/extensions/src/main/java/org/eclipse/tractusx/edc/lifecycle/ConsumerServicesExtension.java index a1a2ab29c..12c926b18 100644 --- a/edc-tests/runtime/extensions/src/main/java/org/eclipse/tractusx/edc/lifecycle/ConsumerServicesExtension.java +++ b/edc-tests/runtime/extensions/src/main/java/org/eclipse/tractusx/edc/lifecycle/ConsumerServicesExtension.java @@ -15,6 +15,7 @@ package org.eclipse.tractusx.edc.lifecycle; import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.agent.ParticipantAgentService; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.web.spi.WebService; @@ -23,8 +24,12 @@ public class ConsumerServicesExtension implements ServiceExtension { @Inject private WebService webService; + @Inject + private ParticipantAgentService participantAgentService; + @Override public void initialize(ServiceExtensionContext context) { webService.registerResource("default", new ConsumerEdrHandlerController(context.getMonitor())); + participantAgentService.register(new SsiParticipantExtractor()); } } diff --git a/edc-tests/runtime/extensions/src/main/java/org/eclipse/tractusx/edc/lifecycle/SsiParticipantExtractor.java b/edc-tests/runtime/extensions/src/main/java/org/eclipse/tractusx/edc/lifecycle/SsiParticipantExtractor.java new file mode 100644 index 000000000..e10e2055f --- /dev/null +++ b/edc-tests/runtime/extensions/src/main/java/org/eclipse/tractusx/edc/lifecycle/SsiParticipantExtractor.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.lifecycle; + +import org.eclipse.edc.spi.agent.ParticipantAgentServiceExtension; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Optional; + +import static org.eclipse.edc.spi.agent.ParticipantAgent.PARTICIPANT_IDENTITY; +import static org.eclipse.edc.util.reflection.ReflectionUtil.getFieldValue; + +public class SsiParticipantExtractor implements ParticipantAgentServiceExtension { + + private static final String EXTRACTING_KEY = "verifiableCredential[0].credentialSubject.holderIdentifier"; + + @Override + public @NotNull Map attributesFor(ClaimToken token) { + var vp = (Map) token.getClaim("vp"); + return Optional.ofNullable(vp) + .flatMap(this::extractIdentity) + .map(this::identityMap) + .orElse(Map.of()); + } + + private Optional extractIdentity(Map vp) { + return Optional.ofNullable(getFieldValue(EXTRACTING_KEY, vp)); + } + + private Map identityMap(String identity) { + return Map.of(PARTICIPANT_IDENTITY, identity); + } + +} diff --git a/edc-tests/runtime/runtime-memory-ssi/README.md b/edc-tests/runtime/runtime-memory-ssi/README.md new file mode 100644 index 000000000..2f9593a75 --- /dev/null +++ b/edc-tests/runtime/runtime-memory-ssi/README.md @@ -0,0 +1,3 @@ +# In-Memory Runtime for Testing Purposes + +This module provides a very small, purely in-mem runtime to execute tests against. Not intended for anything other than testing! diff --git a/edc-tests/runtime/runtime-memory-ssi/build.gradle.kts b/edc-tests/runtime/runtime-memory-ssi/build.gradle.kts new file mode 100644 index 000000000..0a69659c3 --- /dev/null +++ b/edc-tests/runtime/runtime-memory-ssi/build.gradle.kts @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` + id("application") +} + + +dependencies { + + // use basic (all in-mem) control plane + implementation(project(":edc-controlplane:edc-controlplane-base")) { + exclude("org.eclipse.edc", "oauth2-core") + exclude("org.eclipse.edc", "oauth2-daps") + exclude(module = "data-encryption") + } + + implementation(project(":edc-extensions:ssi:ssi-identity-core")) + implementation(project(":edc-extensions:ssi:ssi-miw-credential-client")); + + implementation(project(":edc-tests:runtime:extensions")) + + // use basic (all in-mem) data plane + runtimeOnly(project(":edc-dataplane:edc-dataplane-base")) { + exclude("org.eclipse.edc", "api-observability") + } + + + implementation(libs.edc.core.controlplane) + // for the controller + implementation(libs.jakarta.rsApi) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +// do not publish +edcBuild { + publish.set(false) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 3cd6fd9e5..c4738535e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,6 +23,8 @@ rootProject.name = "tractusx-edc" include(":spi:control-plane-adapter-spi") include(":spi:edr-cache-spi") include(":spi:core-spi") +include(":spi:ssi-spi") + // core modules include(":core:edr-cache-core") @@ -42,12 +44,15 @@ include(":edc-extensions:transferprocess-sftp-provisioner") include(":edc-extensions:control-plane-adapter-api") include(":edc-extensions:control-plane-adapter-callback") include(":edc-extensions:edr-cache-sql") +include("edc-extensions:ssi:ssi-identity-core") +include("edc-extensions:ssi:ssi-miw-credential-client") include(":edc-tests:e2e-tests") include(":edc-tests:runtime:extensions") include(":edc-tests:runtime:runtime-memory") +include(":edc-tests:runtime:runtime-memory-ssi") include(":edc-tests:runtime:runtime-postgresql") include(":edc-tests:cucumber") diff --git a/spi/ssi-spi/build.gradle.kts b/spi/ssi-spi/build.gradle.kts new file mode 100644 index 000000000..d37a21733 --- /dev/null +++ b/spi/ssi-spi/build.gradle.kts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` + `java-test-fixtures` +} + +dependencies { + implementation(libs.edc.spi.core) + implementation(libs.edc.spi.jwt) +} diff --git a/spi/ssi-spi/src/main/java/org/eclipse/tractusx/edc/iam/ssi/spi/SsiCredentialClient.java b/spi/ssi-spi/src/main/java/org/eclipse/tractusx/edc/iam/ssi/spi/SsiCredentialClient.java new file mode 100644 index 000000000..44e8313de --- /dev/null +++ b/spi/ssi-spi/src/main/java/org/eclipse/tractusx/edc/iam/ssi/spi/SsiCredentialClient.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.spi; + +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; + +/** + * Obtains client security tokens from an identity provider. + * Providers may implement different authorization protocols such as OAuth2. + */ + +@ExtensionPoint +public interface SsiCredentialClient { + + /** + * Obtains a client token encoded as a JWT. + * + * @param parameters parameter object defining the token properties. + * @return generated client token. + */ + + Result obtainClientCredentials(TokenParameters parameters); + + /** + * Verifies a JWT bearer token. + * + * @param tokenRepresentation A token representation including the token to verify. + * @return Result of the validation. + */ + + Result validate(TokenRepresentation tokenRepresentation); +} diff --git a/spi/ssi-spi/src/main/java/org/eclipse/tractusx/edc/iam/ssi/spi/SsiValidationRuleRegistry.java b/spi/ssi-spi/src/main/java/org/eclipse/tractusx/edc/iam/ssi/spi/SsiValidationRuleRegistry.java new file mode 100644 index 000000000..95bff34e3 --- /dev/null +++ b/spi/ssi-spi/src/main/java/org/eclipse/tractusx/edc/iam/ssi/spi/SsiValidationRuleRegistry.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.iam.ssi.spi; + +import org.eclipse.edc.jwt.spi.TokenValidationRulesRegistry; +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; + +@ExtensionPoint +public interface SsiValidationRuleRegistry extends TokenValidationRulesRegistry { +}