diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/DefaultTrustedIssuerRegistry.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/DefaultTrustedIssuerRegistry.java new file mode 100644 index 00000000000..cd3dcf9e751 --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/DefaultTrustedIssuerRegistry.java @@ -0,0 +1,44 @@ +/* + * 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.edc.iam.identitytrust.core; + +import org.eclipse.edc.identitytrust.TrustedIssuerRegistry; +import org.eclipse.edc.identitytrust.model.Issuer; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Simple, memory-based implementation of a {@link TrustedIssuerRegistry} + */ +public class DefaultTrustedIssuerRegistry implements TrustedIssuerRegistry { + private final Map store = new HashMap<>(); + + @Override + public void addIssuer(Issuer issuer) { + store.put(issuer.id(), issuer); + } + + @Override + public Issuer getById(String id) { + return store.get(id); + } + + @Override + public Collection getTrustedIssuers() { + return store.values(); + } +} diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtension.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtension.java index 4600a2d2625..b2b1246b49b 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtension.java @@ -16,6 +16,7 @@ import org.eclipse.edc.iam.identitytrust.sts.embedded.EmbeddedSecureTokenService; import org.eclipse.edc.identitytrust.SecureTokenService; +import org.eclipse.edc.identitytrust.TrustedIssuerRegistry; import org.eclipse.edc.jwt.TokenGenerationServiceImpl; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; @@ -66,6 +67,11 @@ public SecureTokenService createDefaultTokenService(ServiceExtensionContext cont return new EmbeddedSecureTokenService(new TokenGenerationServiceImpl(keyPair.getPrivate()), clock, TimeUnit.MINUTES.toSeconds(tokenExpiration)); } + @Provider(isDefault = true) + public TrustedIssuerRegistry createInMemoryIssuerRegistry() { + return new DefaultTrustedIssuerRegistry(); + } + private KeyPair keyPairFromConfig(ServiceExtensionContext context) { var pubKeyAlias = context.getSetting(STS_PUBLIC_KEY_ALIAS, null); var privKeyAlias = context.getSetting(STS_PRIVATE_KEY_ALIAS, null); diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java index bb70cd5311e..d854c476dee 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java @@ -21,6 +21,7 @@ import org.eclipse.edc.iam.identitytrust.verification.SelfIssuedIdTokenVerifier; import org.eclipse.edc.identitytrust.CredentialServiceClient; import org.eclipse.edc.identitytrust.SecureTokenService; +import org.eclipse.edc.identitytrust.TrustedIssuerRegistry; import org.eclipse.edc.identitytrust.validation.JwtValidator; import org.eclipse.edc.identitytrust.verification.JwtVerifier; import org.eclipse.edc.identitytrust.verification.PresentationVerifier; @@ -50,13 +51,16 @@ public class IdentityAndTrustExtension implements ServiceExtension { @Inject private DidResolverRegistry resolverRegistry; + @Inject + private TrustedIssuerRegistry registry; + private JwtValidator jwtValidator; private JwtVerifier jwtVerifier; @Provider public IdentityService createIdentityService(ServiceExtensionContext context) { return new IdentityAndTrustService(secureTokenService, getIssuerDid(context), context.getParticipantId(), presentationVerifier, - credentialServiceClient, getJwtValidator(), getJwtVerifier()); + credentialServiceClient, getJwtValidator(), getJwtVerifier(), registry); } @Provider diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/DefaultTrustedIssuerRegistryTest.java b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/DefaultTrustedIssuerRegistryTest.java new file mode 100644 index 00000000000..7a7c07bdf7e --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/DefaultTrustedIssuerRegistryTest.java @@ -0,0 +1,65 @@ +/* + * 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.edc.iam.identitytrust.core; + +import org.eclipse.edc.identitytrust.model.Issuer; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class DefaultTrustedIssuerRegistryTest { + + private final DefaultTrustedIssuerRegistry registry = new DefaultTrustedIssuerRegistry(); + + @Test + void addIssuer() { + var issuer = new Issuer("test-id", Map.of()); + registry.addIssuer(issuer); + assertThat(registry.getTrustedIssuers()).containsExactly(issuer); + } + + @Test + void addIssuer_exists_shouldReplace() { + var issuer = new Issuer("test-id", Map.of()); + var issuer2 = new Issuer("test-id", Map.of("new-key", "new-val")); + registry.addIssuer(issuer); + registry.addIssuer(issuer2); + assertThat(registry.getTrustedIssuers()).containsExactly(issuer2); + } + + @Test + void getById() { + var issuer = new Issuer("test-id", Map.of()); + registry.addIssuer(issuer); + assertThat(registry.getById("test-id")).isEqualTo(issuer); + } + + @Test + void getById_notFound() { + assertThat(registry.getById("nonexistent-id")).isNull(); + } + + @Test + void getTrustedIssuers() { + var issuer = new Issuer("test-id", Map.of()); + var issuer2 = new Issuer("test-id2", Map.of("new-key", "new-val")); + registry.addIssuer(issuer); + registry.addIssuer(issuer2); + + assertThat(registry.getTrustedIssuers()).containsExactlyInAnyOrder(issuer2, issuer); + } +} \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtensionTest.java b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtensionTest.java index b5c24a372de..495dfe78a4c 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtensionTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtensionTest.java @@ -23,6 +23,7 @@ import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.security.KeyPairFactory; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.injection.ObjectFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -93,4 +94,13 @@ void verify_defaultServiceWithWarning(ServiceExtensionContext context, IatpDefau verify(mockedMonitor).warning(anyString()); verify(keyPairFactory, times(1)).defaultKeyPair(); } + + @Test + void verify_defaultIssuerRegistry(ServiceExtensionContext context, ObjectFactory factory) { + Monitor mockedMonitor = mock(); + context.registerService(Monitor.class, mockedMonitor); + var ext = factory.constructInstance(IatpDefaultServicesExtension.class); + + assertThat(ext.createInMemoryIssuerRegistry()).isInstanceOf(DefaultTrustedIssuerRegistry.class); + } } \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityAndTrustService.java b/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityAndTrustService.java index 1ec143699ef..76065e5c3ad 100644 --- a/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityAndTrustService.java +++ b/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityAndTrustService.java @@ -19,6 +19,8 @@ import org.eclipse.edc.iam.identitytrust.validation.rules.IsRevoked; import org.eclipse.edc.identitytrust.CredentialServiceClient; import org.eclipse.edc.identitytrust.SecureTokenService; +import org.eclipse.edc.identitytrust.TrustedIssuerRegistry; +import org.eclipse.edc.identitytrust.model.Issuer; import org.eclipse.edc.identitytrust.model.VerifiableCredential; import org.eclipse.edc.identitytrust.validation.CredentialValidationRule; import org.eclipse.edc.identitytrust.validation.JwtValidator; @@ -61,6 +63,7 @@ public class IdentityAndTrustService implements IdentityService { private final CredentialServiceClient credentialServiceClient; private final JwtValidator jwtValidator; private final JwtVerifier jwtVerifier; + private final TrustedIssuerRegistry trustedIssuerRegistry; /** * Constructs a new instance of the {@link IdentityAndTrustService}. @@ -70,7 +73,7 @@ public class IdentityAndTrustService implements IdentityService { */ public IdentityAndTrustService(SecureTokenService secureTokenService, String myOwnDid, String participantId, PresentationVerifier presentationVerifier, CredentialServiceClient credentialServiceClient, - JwtValidator jwtValidator, JwtVerifier jwtVerifier) { + JwtValidator jwtValidator, JwtVerifier jwtVerifier, TrustedIssuerRegistry trustedIssuerRegistry) { this.secureTokenService = secureTokenService; this.myOwnDid = myOwnDid; this.participantId = participantId; @@ -78,6 +81,7 @@ public IdentityAndTrustService(SecureTokenService secureTokenService, String myO this.credentialServiceClient = credentialServiceClient; this.jwtValidator = jwtValidator; this.jwtVerifier = jwtVerifier; + this.trustedIssuerRegistry = trustedIssuerRegistry; } @Override @@ -88,7 +92,7 @@ public Result obtainClientCredentials(TokenParameters param if (scopeValidationResult.failed()) { return failure(scopeValidationResult.getFailureMessages()); } - + // create claims for the STS var claims = new HashMap(); parameters.getAdditional().forEach((k, v) -> claims.replace(k, v.toString())); @@ -131,7 +135,7 @@ public Result verifyJwtToken(TokenRepresentation tokenRepresentation var filters = new ArrayList<>(List.of( new HasValidSubjectIds(issuerResult.getContent()), new IsRevoked(null), - new HasValidIssuer(getAllowedIssuers()))); + new HasValidIssuer(getTrustedIssuerIds()))); filters.addAll(getAdditionalValidations()); var results = credentials.stream().map(c -> filters.stream().reduce(t -> Result.success(), CredentialValidationRule::and).apply(c)).reduce(Result::merge); @@ -152,8 +156,8 @@ private Collection getAdditionalValidations( return List.of(); } - private List getAllowedIssuers() { - return List.of(); + private List getTrustedIssuerIds() { + return trustedIssuerRegistry.getTrustedIssuers().stream().map(Issuer::id).toList(); } private Result validateScope(String scope) { diff --git a/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/validation/rules/HasValidIssuer.java b/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/validation/rules/HasValidIssuer.java index bdbf951a7f3..d61862c6abb 100644 --- a/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/validation/rules/HasValidIssuer.java +++ b/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/validation/rules/HasValidIssuer.java @@ -18,8 +18,7 @@ import org.eclipse.edc.identitytrust.validation.CredentialValidationRule; import org.eclipse.edc.spi.result.Result; -import java.util.List; -import java.util.Map; +import java.util.Collection; import static org.eclipse.edc.spi.result.Result.failure; import static org.eclipse.edc.spi.result.Result.success; @@ -31,30 +30,18 @@ * If the issuer object is neither a string nor an object containing an "id" field, a failure is returned. */ public class HasValidIssuer implements CredentialValidationRule { - private final List allowedIssuers; + private final Collection trustedIssuers; - public HasValidIssuer(List allowedIssuers) { - - this.allowedIssuers = allowedIssuers; + public HasValidIssuer(Collection trustedIssuers) { + this.trustedIssuers = trustedIssuers; } @Override public Result apply(VerifiableCredential credential) { - var issuerObject = credential.getIssuer(); - String issuer; - // issuers can be URLs, or Objects containing an "id" property - if (issuerObject instanceof String) { - issuer = issuerObject.toString(); - } else if (issuerObject instanceof Map) { - var id = ((Map) issuerObject).get("id"); - if (id == null) { - return failure("Issuer was an object, but did not contain an 'id' field"); - } - issuer = id.toString(); - } else { - return failure("VC Issuer must either be a String or an Object but was %s.".formatted(issuerObject.getClass())); + var issuer = credential.getIssuer(); + if (issuer.id() == null) { + return failure("Issuer did not contain an 'id' field."); } - - return allowedIssuers.contains(issuer) ? success() : failure("Issuer '%s' is not in the list of allowed issuers".formatted(issuer)); + return trustedIssuers.contains(issuer.id()) ? success() : failure("Issuer '%s' is not in the list of trusted issuers".formatted(issuer.id())); } } diff --git a/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/service/IdentityAndTrustServiceTest.java b/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/service/IdentityAndTrustServiceTest.java index c11372ab887..74c8f78e669 100644 --- a/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/service/IdentityAndTrustServiceTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/service/IdentityAndTrustServiceTest.java @@ -18,8 +18,10 @@ import org.eclipse.edc.iam.identitytrust.IdentityAndTrustService; import org.eclipse.edc.identitytrust.CredentialServiceClient; import org.eclipse.edc.identitytrust.SecureTokenService; +import org.eclipse.edc.identitytrust.TrustedIssuerRegistry; import org.eclipse.edc.identitytrust.model.CredentialFormat; import org.eclipse.edc.identitytrust.model.CredentialSubject; +import org.eclipse.edc.identitytrust.model.Issuer; import org.eclipse.edc.identitytrust.model.VerifiablePresentationContainer; import org.eclipse.edc.identitytrust.validation.JwtValidator; import org.eclipse.edc.identitytrust.verification.JwtVerifier; @@ -37,6 +39,7 @@ import org.junit.jupiter.params.provider.ValueSource; import java.util.List; +import java.util.Map; import static org.eclipse.edc.identitytrust.TestFunctions.createCredentialBuilder; import static org.eclipse.edc.identitytrust.TestFunctions.createJwt; @@ -65,7 +68,9 @@ class IdentityAndTrustServiceTest { private final CredentialServiceClient mockedClient = mock(); private final JwtValidator jwtValidatorMock = mock(); private final JwtVerifier jwtVerfierMock = mock(); - private final IdentityAndTrustService service = new IdentityAndTrustService(mockedSts, EXPECTED_OWN_DID, EXPECTED_PARTICIPANT_ID, mockedVerifier, mockedClient, jwtValidatorMock, jwtVerfierMock); + private final TrustedIssuerRegistry trustedIssuerRegistryMock = mock(); + private final IdentityAndTrustService service = new IdentityAndTrustService(mockedSts, EXPECTED_OWN_DID, EXPECTED_PARTICIPANT_ID, mockedVerifier, mockedClient, + jwtValidatorMock, jwtVerfierMock, trustedIssuerRegistryMock); @BeforeEach void setup() { @@ -179,7 +184,7 @@ void credentialHasInvalidIssuer_issuerIsUrl() { var presentation = createPresentationBuilder() .type("VerifiablePresentation") .credentials(List.of(createCredentialBuilder() - .issuer("invalid-issuer") + .issuer(new Issuer("invalid-issuer", Map.of())) .build())) .build(); var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.JSON_LD, presentation); @@ -189,7 +194,7 @@ void credentialHasInvalidIssuer_issuerIsUrl() { var result = service.verifyJwtToken(token, "test-audience"); assertThat(result).isFailed().messages() .hasSizeGreaterThanOrEqualTo(1) - .contains("Issuer 'invalid-issuer' is not in the list of allowed issuers"); + .contains("Issuer 'invalid-issuer' is not in the list of trusted issuers"); } @Test diff --git a/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/validation/rules/HasValidIssuerTest.java b/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/validation/rules/HasValidIssuerTest.java index 55eec5fe6af..95da5f7a9e0 100644 --- a/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/validation/rules/HasValidIssuerTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/validation/rules/HasValidIssuerTest.java @@ -14,6 +14,7 @@ package org.eclipse.edc.iam.identitytrust.validation.rules; +import org.eclipse.edc.identitytrust.model.Issuer; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -29,7 +30,7 @@ class HasValidIssuerTest { @Test void hasValidIssuer_string() { var vc = createCredentialBuilder() - .issuer("did:web:issuer2") + .issuer(new Issuer("did:web:issuer2", Map.of())) .build(); assertThat(new HasValidIssuer(List.of("did:web:issuer1", "did:web:issuer2")).apply(vc)).isSucceeded(); } @@ -38,7 +39,7 @@ void hasValidIssuer_string() { @Test void hasValidIssuer_object() { var vc = createCredentialBuilder() - .issuer(Map.of("id", "did:web:issuer1", "name", "test issuer company")) + .issuer(new Issuer("did:web:issuer1", Map.of("name", "test issuer company"))) .build(); assertThat(new HasValidIssuer(List.of("did:web:issuer1", "did:web:issuer2")).apply(vc)).isSucceeded(); } @@ -47,39 +48,19 @@ void hasValidIssuer_object() { @Test void invalidIssuer_string() { var vc = createCredentialBuilder() - .issuer("did:web:invalid") + .issuer(new Issuer("did:web:invalid", Map.of())) .build(); assertThat(new HasValidIssuer(List.of("did:web:issuer1", "did:web:issuer2")).apply(vc)).isFailed() - .detail().isEqualTo("Issuer 'did:web:invalid' is not in the list of allowed issuers"); + .detail().isEqualTo("Issuer 'did:web:invalid' is not in the list of trusted issuers"); } @DisplayName("Issuer (object) is not in the list of valid issuers") @Test void invalidIssuer_object() { var vc = createCredentialBuilder() - .issuer(Map.of("id", "did:web:invalid", "name", "test issuer company")) + .issuer(new Issuer("did:web:invalid", Map.of("id", "did:web:invalid", "name", "test issuer company"))) .build(); assertThat(new HasValidIssuer(List.of("did:web:issuer1", "did:web:issuer2")).apply(vc)).isFailed() - .detail().isEqualTo("Issuer 'did:web:invalid' is not in the list of allowed issuers"); - } - - @DisplayName("Issuer (object) does not have an 'id' property") - @Test - void issuerIsObject_noIdField() { - var vc = createCredentialBuilder() - .issuer(Map.of("name", "test issuer company")) - .build(); - assertThat(new HasValidIssuer(List.of("did:web:issuer1", "did:web:issuer2")).apply(vc)).isFailed() - .detail().isEqualTo("Issuer was an object, but did not contain an 'id' field"); - } - - @DisplayName("Issuer neither a string nor an object") - @Test - void issuerIsInvalidType() { - var vc = createCredentialBuilder() - .issuer(43L) - .build(); - assertThat(new HasValidIssuer(List.of("did:web:issuer1", "did:web:issuer2")).apply(vc)).isFailed() - .detail().isEqualTo("VC Issuer must either be a String or an Object but was class java.lang.Long."); + .detail().isEqualTo("Issuer 'did:web:invalid' is not in the list of trusted issuers"); } } \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToIssuerTransformer.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToIssuerTransformer.java new file mode 100644 index 00000000000..e34b02aefbd --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToIssuerTransformer.java @@ -0,0 +1,44 @@ +/* + * 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.edc.iam.identitytrust.transform.to; + +import jakarta.json.JsonObject; +import org.eclipse.edc.identitytrust.model.Issuer; +import org.eclipse.edc.jsonld.spi.JsonLdKeywords; +import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; + +public class JsonObjectToIssuerTransformer extends AbstractJsonLdTransformer { + protected JsonObjectToIssuerTransformer() { + super(JsonObject.class, Issuer.class); + } + + @Override + public @Nullable Issuer transform(@NotNull JsonObject jsonObject, @NotNull TransformerContext context) { + var id = nodeId(jsonObject); + var props = new HashMap(); + visitProperties(jsonObject, (key, jsonValue) -> { + if (!JsonLdKeywords.ID.equals(key)) { + var res = transformGenericProperty(jsonValue, context); + props.put(key, res); + } + }); + return new Issuer(id, props); + } +} diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiableCredentialTransformer.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiableCredentialTransformer.java index d7c27289e85..e2b8e226633 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiableCredentialTransformer.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiableCredentialTransformer.java @@ -18,6 +18,7 @@ import jakarta.json.JsonValue; import org.eclipse.edc.identitytrust.model.CredentialStatus; import org.eclipse.edc.identitytrust.model.CredentialSubject; +import org.eclipse.edc.identitytrust.model.Issuer; import org.eclipse.edc.identitytrust.model.VerifiableCredential; import org.eclipse.edc.jsonld.spi.JsonLdKeywords; import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; @@ -26,6 +27,7 @@ import org.jetbrains.annotations.Nullable; import java.time.Instant; +import java.util.Map; import static org.eclipse.edc.identitytrust.model.VerifiableCredential.Builder; import static org.eclipse.edc.identitytrust.model.VerifiableCredential.VERIFIABLE_CREDENTIAL_DESCRIPTION_PROPERTY; @@ -84,7 +86,21 @@ private Instant parseDate(JsonValue jsonValue, TransformerContext context) { return Instant.parse(str); } - private Object parseIssuer(JsonValue jsonValue, TransformerContext context) { - return transformString(jsonValue, context); //todo: handle the case where the issuer is an object + private Issuer parseIssuer(JsonValue jsonValue, TransformerContext context) { + if (jsonValue.getValueType() == JsonValue.ValueType.STRING) { + return new Issuer(transformString(jsonValue, context), Map.of()); + } else { + // issuers can be objects, that MUST contain an ID, and optional other properties + // an issuer is never an array with >1 elements + JsonObject issuer; + if (jsonValue.getValueType() == JsonValue.ValueType.ARRAY) { + issuer = jsonValue.asJsonArray().get(0).asJsonObject(); + } else if (jsonValue.getValueType() == JsonValue.ValueType.OBJECT) { + issuer = jsonValue.asJsonObject(); + } else { + throw new IllegalArgumentException("Unknown issuer type, expected ARRAY or OBJECT, was %s".formatted(jsonValue.getValueType())); + } + return transformObject(issuer, Issuer.class, context); + } } } diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/TestData.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/TestData.java index e6edb4b9f73..09c41d46209 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/TestData.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/TestData.java @@ -48,6 +48,34 @@ public interface TestData { } } """; + String EXAMPLE_VC_JSONLD_ISSUER_IS_URL = """ + { + "@context": [ + "https://www.w3.org/2018/credentials/v2" + ], + "id": "http://university.example/credentials/3732", + "type": ["VerifiableCredential", "ExampleDegreeCredential"], + "issuer": "https://university.example/issuers/565049", + "validFrom": "2015-05-10T12:30:00Z", + "validUntil":"2023-05-12T23:00:00Z", + "name": "Example University Degree", + "description": "2015 Bachelor of Science and Arts Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "degree": { + "degreetype": "ExampleBachelorDegree", + "subtype": "Bachelor of Science and Arts" + } + }, + "credentialStatus": { + "id": "https://university.example/credentials/status/3#94567", + "type": "StatusList2021Entry", + "statusPurpose": "revocation", + "statusListIndex": "94567", + "statusListCredential": "https://university.example/credentials/status/3" + } + } + """; String EXAMPLE_VP_JSONLD = """ { "@context": [ diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToIssuerTransformerTest.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToIssuerTransformerTest.java new file mode 100644 index 00000000000..8c40f8021b9 --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToIssuerTransformerTest.java @@ -0,0 +1,78 @@ +/* + * 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.edc.iam.identitytrust.transform.to; + +import jakarta.json.Json; +import jakarta.json.JsonString; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class JsonObjectToIssuerTransformerTest { + + private final TransformerContext context = mock(); + private JsonObjectToIssuerTransformer transformer; + + @BeforeEach + void setup() { + transformer = new JsonObjectToIssuerTransformer(); + } + + @DisplayName("Asserts the correct parsing of an issuer that is a URL") + @Test + void transform_issuerIsUrl() { + var url = "https://some-test.issuer.org"; + var jobj = Json.createObjectBuilder().add("@id", url).build(); + var issuer = transformer.transform(jobj, context); + assertThat(issuer).isNotNull(); + assertThat(issuer.id()).isEqualTo(url); + assertThat(issuer.additionalProperties()).isEmpty(); + } + + @DisplayName("Asserts the correct parsing of an issuer that is an object") + @Test + void transform_issuerIsObject() { + var url = "https://some-test.issuer.org"; + var issuerObj = Json.createObjectBuilder() + .add("@id", url) + .add("name", "test-name") + .add("desc", "test-desc") + .build(); + when(context.transform(any(), any())).thenAnswer(a -> ((JsonString) a.getArgument(0)).getString()); + + var issuer = transformer.transform(issuerObj, context); + assertThat(issuer).isNotNull(); + assertThat(issuer.id()).isEqualTo(url); + assertThat(issuer.additionalProperties()).containsEntry("name", "test-name") + .containsEntry("desc", "test-desc"); + } + + @DisplayName("Asserts the correct parsing of an issuer that is a number (invalid)") + @Test + void transform_issuerIsUnexpectedType() { + var jobj = Json.createObjectBuilder().add("@id", 42).build(); + when(context.transform(any(), any())).thenAnswer(a -> ((JsonString) a.getArgument(0)).getString()); + + assertThatThrownBy(() -> transformer.transform(jobj, context)).isInstanceOf(NullPointerException.class); + } + +} \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiableCredentialTransformerTest.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiableCredentialTransformerTest.java index f4c60816606..abaa1aef990 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiableCredentialTransformerTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiableCredentialTransformerTest.java @@ -20,6 +20,7 @@ import org.eclipse.edc.core.transform.TransformerContextImpl; import org.eclipse.edc.core.transform.TypeTransformerRegistryImpl; import org.eclipse.edc.core.transform.transformer.to.JsonValueToGenericTypeTransformer; +import org.eclipse.edc.identitytrust.model.Issuer; import org.eclipse.edc.jsonld.TitaniumJsonLd; import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.jsonld.util.JacksonJsonLd; @@ -31,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_VC_JSONLD; +import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_VC_JSONLD_ISSUER_IS_URL; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -50,6 +52,7 @@ void setUp() throws URISyntaxException { registry.register(new JsonObjectToCredentialSubjectTransformer()); registry.register(new JsonObjectToCredentialStatusTransformer()); registry.register(new JsonValueToGenericTypeTransformer(OBJECT_MAPPER)); + registry.register(new JsonObjectToIssuerTransformer()); registry.register(transformer); context = spy(new TransformerContextImpl(registry)); @@ -68,7 +71,23 @@ void transform() throws JsonProcessingException { assertThat(vc.getDescription()).isNotNull(); assertThat(vc.getName()).isNotNull(); assertThat(vc.getCredentialStatus()).isNotNull(); + assertThat(vc.getIssuer()).isNotNull().extracting(Issuer::id).isEqualTo("https://university.example/issuers/565049"); verify(context, never()).reportProblem(anyString()); + } + + @Test + void transform_issuerIsUrl() throws JsonProcessingException { + var jsonObj = OBJECT_MAPPER.readValue(EXAMPLE_VC_JSONLD_ISSUER_IS_URL, JsonObject.class); + var vc = transformer.transform(jsonLdService.expand(jsonObj).getContent(), context); + + assertThat(vc).isNotNull(); + assertThat(vc.getCredentialSubject()).isNotNull().hasSize(1); + assertThat(vc.getTypes()).hasSize(2); + assertThat(vc.getDescription()).isNotNull(); + assertThat(vc.getName()).isNotNull(); + assertThat(vc.getCredentialStatus()).isNotNull(); + assertThat(vc.getIssuer()).isNotNull().extracting(Issuer::id).isEqualTo("https://university.example/issuers/565049"); + verify(context, never()).reportProblem(anyString()); } } \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiablePresentationTransformerTest.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiablePresentationTransformerTest.java index 6f5000cfeb7..4482736e015 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiablePresentationTransformerTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToVerifiablePresentationTransformerTest.java @@ -48,6 +48,7 @@ void setup() throws URISyntaxException { registry.register(new JsonObjectToCredentialSubjectTransformer()); registry.register(new JsonObjectToCredentialStatusTransformer()); registry.register(new JsonObjectToVerifiableCredentialTransformer()); + registry.register(new JsonObjectToIssuerTransformer()); registry.register(new JsonValueToGenericTypeTransformer(OBJECT_MAPPER)); registry.register(transformer); diff --git a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/TrustedIssuerRegistry.java b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/TrustedIssuerRegistry.java new file mode 100644 index 00000000000..436517e081d --- /dev/null +++ b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/TrustedIssuerRegistry.java @@ -0,0 +1,32 @@ +/* + * 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.edc.identitytrust; + +import org.eclipse.edc.identitytrust.model.Issuer; + +import java.util.Collection; + +/** + * A list of trusted VC issuers + */ +public interface TrustedIssuerRegistry { + + void addIssuer(Issuer issuer); + + Issuer getById(String id); + + Collection getTrustedIssuers(); +} + diff --git a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/Issuer.java b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/Issuer.java new file mode 100644 index 00000000000..9e4311ec5e3 --- /dev/null +++ b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/Issuer.java @@ -0,0 +1,24 @@ +/* + * 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.edc.identitytrust.model; + +import java.util.Map; +import java.util.Objects; + +public record Issuer(String id, Map additionalProperties) { + public Issuer { + Objects.requireNonNull(id, "Issuer must be a URL or have an id!"); + } +} diff --git a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiableCredential.java b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiableCredential.java index f0778881360..fce8e1db05c 100644 --- a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiableCredential.java +++ b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiableCredential.java @@ -39,7 +39,7 @@ public class VerifiableCredential { private List credentialSubject = new ArrayList<>(); private String id; // must be URI, but URI is less efficient at runtime private List types = new ArrayList<>(); - private Object issuer; // can be URI or an object containing an ID + private Issuer issuer; // can be URI or an object containing an ID private Instant issuanceDate; // v2 of the spec renames this to "validFrom" private Instant expirationDate; // v2 of the spec renames this to "validUntil" private CredentialStatus credentialStatus; @@ -61,7 +61,7 @@ public List getTypes() { return types; } - public Object getIssuer() { + public Issuer getIssuer() { return issuer; } @@ -129,7 +129,7 @@ public Builder description(String desc) { /** * Issuers can be URIs or objects containing an ID */ - public Builder issuer(Object issuer) { + public Builder issuer(Issuer issuer) { this.instance.issuer = issuer; return this; } diff --git a/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiableCredentialTest.java b/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiableCredentialTest.java index 5e331ca9cdd..16b013e1a13 100644 --- a/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiableCredentialTest.java +++ b/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiableCredentialTest.java @@ -17,8 +17,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.net.URI; import java.util.ArrayList; +import java.util.Map; import static java.time.Instant.now; import static org.assertj.core.api.Assertions.assertThatNoException; @@ -34,7 +34,7 @@ void setUp() { void buildMinimalVc() { assertThatNoException().isThrownBy(() -> VerifiableCredential.Builder.newInstance() .credentialSubject(new CredentialSubject()) - .issuer(URI.create("http://test.issuer")) + .issuer(new Issuer("http://test.issuer", Map.of())) .issuanceDate(now()) .type("test-type") .build()); @@ -44,7 +44,7 @@ void buildMinimalVc() { void assertDefaultValues() { var vc = VerifiableCredential.Builder.newInstance() .credentialSubject(new CredentialSubject()) - .issuer(URI.create("http://test.issuer")) + .issuer(new Issuer("http://test.issuer", Map.of())) .issuanceDate(now()) .type("test-type") .build(); @@ -54,7 +54,7 @@ void assertDefaultValues() { void build_emptyContexts() { assertThatThrownBy(() -> VerifiableCredential.Builder.newInstance() .credentialSubject(new CredentialSubject()) - .issuer(URI.create("http://test.issuer")) + .issuer(new Issuer("http://test.issuer", Map.of())) .issuanceDate(now()) .build()) .isInstanceOf(IllegalArgumentException.class); @@ -64,7 +64,7 @@ void build_emptyContexts() { void build_emptyTypes() { assertThatThrownBy(() -> VerifiableCredential.Builder.newInstance() .credentialSubject(new CredentialSubject()) - .issuer(URI.create("http://test.issuer")) + .issuer(new Issuer("http://test.issuer", Map.of())) .issuanceDate(now()) .types(new ArrayList<>()) .build()) @@ -75,7 +75,7 @@ void build_emptyTypes() { void build_emptyProofs() { assertThatThrownBy(() -> VerifiableCredential.Builder.newInstance() .credentialSubject(new CredentialSubject()) - .issuer(URI.create("http://test.issuer")) + .issuer(new Issuer("http://test.issuer", Map.of())) .issuanceDate(now()) .build()) .isInstanceOf(IllegalArgumentException.class); diff --git a/spi/common/identity-trust-spi/src/testFixtures/java/org/eclipse/edc/identitytrust/TestFunctions.java b/spi/common/identity-trust-spi/src/testFixtures/java/org/eclipse/edc/identitytrust/TestFunctions.java index e5a0beab880..87323552877 100644 --- a/spi/common/identity-trust-spi/src/testFixtures/java/org/eclipse/edc/identitytrust/TestFunctions.java +++ b/spi/common/identity-trust-spi/src/testFixtures/java/org/eclipse/edc/identitytrust/TestFunctions.java @@ -25,12 +25,14 @@ import com.nimbusds.jwt.SignedJWT; import org.eclipse.edc.identitytrust.model.CredentialFormat; import org.eclipse.edc.identitytrust.model.CredentialSubject; +import org.eclipse.edc.identitytrust.model.Issuer; import org.eclipse.edc.identitytrust.model.VerifiableCredential; import org.eclipse.edc.identitytrust.model.VerifiablePresentation; import org.eclipse.edc.identitytrust.model.VerifiablePresentationContainer; import org.eclipse.edc.spi.iam.TokenRepresentation; import java.util.Date; +import java.util.Map; import static java.time.Instant.now; @@ -47,7 +49,7 @@ public static VerifiableCredential.Builder createCredentialBuilder() { .claim("test-claim", "test-value") .build()) .type("test-type") - .issuer("http://test.issuer") + .issuer(new Issuer("http://test.issuer", Map.of())) .issuanceDate(now()); }