diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index b8d5aa5cd..ee3974796 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -22,8 +22,6 @@ jobs: run: ./gradlew checkstyleMain checkstyleTest checkstyleTestFixtures Verify-Launcher: - # disabled temporarily - if: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -35,8 +33,20 @@ jobs: - name: 'Build Docker image' run: docker build -t identity-hub ./launcher + - name: 'Create public key file' + run: | + mkdir -p keys + openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout keys/key.pem -out keys/cert.pem -subj="/CN=www.foo.bar" + - name: 'Start Identity Hub' - run: docker run -d --rm --name identity-hub identity-hub + run: | + docker run -d --rm --name identity-hub \ + -v $(pwd)/keys:/opt/keys \ + -e "EDC_IH_IAM_PUBLICKEY_PATH=/opt/keys/key.pem" \ + -e "EDC_IH_IAM_ID=did:web:test" \ + -e "WEB_HTTP_RESOLUTION_PORT=10001" \ + -e "WEB_HTTP_RESOLUTION_PATH=/api/v1/resolution/" \ + identity-hub:latest - name: 'Wait for Identity Hub to be healthy' uses: raschmitt/wait-for-healthy-container@v1 diff --git a/DEPENDENCIES b/DEPENDENCIES index e44e61910..0ba760b25 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -228,6 +228,7 @@ maven/mavencentral/org.eclipse.edc/http/0.3.2-SNAPSHOT, Apache-2.0, approved, te maven/mavencentral/org.eclipse.edc/identity-did-core/0.3.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/identity-did-crypto/0.3.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/identity-did-spi/0.3.2-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/identity-did-web/0.3.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/identity-trust-service/0.3.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/identity-trust-spi/0.3.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/jersey-core/0.3.2-SNAPSHOT, Apache-2.0, approved, technology.edc diff --git a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/validation/PresentationQueryValidator.java b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/validation/PresentationQueryValidator.java index 03cfb6e34..1095e652b 100644 --- a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/validation/PresentationQueryValidator.java +++ b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/validation/PresentationQueryValidator.java @@ -14,8 +14,11 @@ package org.eclipse.edc.identityservice.api.validation; +import jakarta.json.JsonArray; import jakarta.json.JsonObject; +import jakarta.json.JsonValue; import org.eclipse.edc.identityhub.spi.model.PresentationQuery; +import org.eclipse.edc.jsonld.spi.JsonLdKeywords; import org.eclipse.edc.validator.spi.ValidationResult; import org.eclipse.edc.validator.spi.Validator; @@ -30,18 +33,37 @@ public class PresentationQueryValidator implements Validator { @Override public ValidationResult validate(JsonObject input) { + if (input == null) { + return failure(violation("Presentation query was null", ".")); + } var scope = input.get(PresentationQuery.PRESENTATION_QUERY_SCOPE_PROPERTY); var presentationDef = input.get(PresentationQuery.PRESENTATION_QUERY_DEFINITION_PROPERTY); - if (scope == null && presentationDef == null) { + if (isNullObject(scope) && isNullObject(presentationDef)) { return failure(violation("Must contain either a 'scope' or a 'presentationDefinition' property.", null)); } - if (scope != null && presentationDef != null) { + if (!isNullObject(scope) && !isNullObject(presentationDef)) { return failure(violation("Must contain either a 'scope' or a 'presentationDefinition', not both.", null)); } return success(); } + + /** + * Checks if the given JsonValue object is the Null-Object. Due to JSON-LD expansion, a {@code "presentation_query": null} + * would get expanded to an array, thus a simple equals-null check is not sufficient. + * + * @param value the JsonValue to check + * @return true if the JsonValue object is either null, or its value type is NULL, false otherwise + */ + private boolean isNullObject(JsonValue value) { + if (value instanceof JsonArray) { + value = ((JsonArray) value).get(0).asJsonObject().get(JsonLdKeywords.VALUE); + + return value.getValueType() == JsonValue.ValueType.NULL; + } + return value == null; + } } diff --git a/core/identity-hub-core/build.gradle.kts b/core/identity-hub-core/build.gradle.kts index f6a88c71b..37a91c896 100644 --- a/core/identity-hub-core/build.gradle.kts +++ b/core/identity-hub-core/build.gradle.kts @@ -4,6 +4,8 @@ plugins { dependencies { api(project(":spi:identity-hub-spi")) + api(project(":spi:identity-hub-store-spi")) + implementation(libs.edc.core.connector) // for the CriterionToPredicateConverterImpl implementation(libs.edc.spi.jsonld) implementation(libs.edc.iatp.service) // JWT validator implementation(libs.edc.core.crypto) // JWT verifier diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java new file mode 100644 index 000000000..6e2f7829b --- /dev/null +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.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.edc.identityhub; + +import org.eclipse.edc.identityhub.defaults.InMemoryCredentialStore; +import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator; +import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; +import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +@Extension("Default Services Extension") +public class DefaultServicesExtension implements ServiceExtension { + @Provider(isDefault = true) + public CredentialStore createInMemStore() { + return new InMemoryCredentialStore(); + + } + + @Provider(isDefault = true) + public CredentialQueryResolver createCredentialResolver(ServiceExtensionContext context) { + context.getMonitor().warning(" #### Creating a default NOOP CredentialQueryResolver, that will always return 'null'!"); + return (query, issuerScopes) -> null; + } + + @Provider(isDefault = true) + public PresentationGenerator createPresentationGenerator(ServiceExtensionContext context) { + context.getMonitor().warning(" #### Creating a default NOOP PresentationGenerator, that will always return 'null'!"); + return (credentials, presentationDefinition) -> null; + } +} diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStore.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStore.java new file mode 100644 index 000000000..a6bdb70d2 --- /dev/null +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStore.java @@ -0,0 +1,93 @@ +/* + * 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.identityhub.defaults; + +import org.eclipse.edc.connector.core.store.ReflectionBasedQueryResolver; +import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; +import org.eclipse.edc.spi.query.QueryResolver; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Stream; + +import static org.eclipse.edc.spi.result.StoreResult.alreadyExists; +import static org.eclipse.edc.spi.result.StoreResult.notFound; +import static org.eclipse.edc.spi.result.StoreResult.success; + +public class InMemoryCredentialStore implements CredentialStore { + private final Map store = new HashMap<>(); + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + private final QueryResolver queryResolver = new ReflectionBasedQueryResolver<>(VerifiableCredentialResource.class); + + @Override + public StoreResult create(VerifiableCredentialResource credentialResource) { + lock.writeLock().lock(); + var id = credentialResource.getId(); + try { + if (store.containsKey(id)) { + return alreadyExists("A VerifiableCredentialResource with ID %s already exists".formatted(id)); + } + store.put(id, credentialResource); + return success(null); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public StoreResult> query(QuerySpec querySpec) { + lock.readLock().lock(); + try { + var result = queryResolver.query(store.values().stream(), querySpec); + return success(result); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public StoreResult update(VerifiableCredentialResource credentialResource) { + lock.writeLock().lock(); + try { + var id = credentialResource.getId(); + if (!store.containsKey(id)) { + return notFound("A VerifiableCredentialResource with ID %s was not found".formatted(id)); + } + store.put(id, credentialResource); + return success(); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public StoreResult delete(String id) { + lock.writeLock().lock(); + try { + if (!store.containsKey(id)) { + return notFound("A VerifiableCredentialResource with ID %s was not found".formatted(id)); + } + store.remove(id); + return success(); + } finally { + lock.writeLock().unlock(); + } + } + +} diff --git a/core/identity-hub-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/core/identity-hub-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index 4af2eb524..a0f8d0bab 100644 --- a/core/identity-hub-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/core/identity-hub-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -12,4 +12,5 @@ # # -org.eclipse.edc.identityhub.core.CoreServicesExtension \ No newline at end of file +org.eclipse.edc.identityhub.core.CoreServicesExtension +org.eclipse.edc.identityhub.DefaultServicesExtension \ No newline at end of file diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStoreTest.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStoreTest.java new file mode 100644 index 000000000..0b5295ae4 --- /dev/null +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStoreTest.java @@ -0,0 +1,157 @@ +/* + * 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.identityhub.defaults; + +import org.assertj.core.api.Assertions; +import org.eclipse.edc.identityhub.spi.store.model.VcState; +import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.junit.jupiter.api.Test; + +import static java.util.stream.IntStream.range; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; + +class InMemoryCredentialStoreTest { + + private final InMemoryCredentialStore store = new InMemoryCredentialStore(); + + @Test + void create() { + var result = store.create(createCredential()); + assertThat(result).isSucceeded(); + + } + + @Test + void create_whenExists_shouldReturnFailure() { + var credential = createCredential(); + var result = store.create(credential); + assertThat(result).isSucceeded(); + var result2 = store.create(credential); + + assertThat(result2).isFailed().detail().contains("already exists"); + } + + @Test + void query() { + range(0, 5) + .mapToObj(i -> createCredentialBuilder().id("id" + i).build()) + .forEach(store::create); + + } + + @Test + void query_noQuerySpec() { + var resources = range(0, 5) + .mapToObj(i -> createCredentialBuilder().id("id" + i).build()) + .toList(); + + resources.forEach(store::create); + + var res = store.query(QuerySpec.max()); + assertThat(res).isSucceeded(); + Assertions.assertThat(res.getContent()).hasSize(5).containsAll(resources); + } + + + @Test + void query_whenNotFound() { + var resources = range(0, 5) + .mapToObj(i -> createCredentialBuilder() + .id("id" + i) + .build()) + .toList(); + + resources.forEach(store::create); + + var query = QuerySpec.Builder.newInstance() + .filter(new Criterion("holderId", "=", "some-holder")) + .build(); + var res = store.query(query); + assertThat(res).isSucceeded(); + Assertions.assertThat(res.getContent()).isEmpty(); + } + + @Test + void query_byInvalidField_shouldReturnEmptyList() { + var resources = range(0, 5) + .mapToObj(i -> createCredentialBuilder() + .id("id" + i) + .build()) + .toList(); + + resources.forEach(store::create); + + var query = QuerySpec.Builder.newInstance() + .filter(new Criterion("invalidField", "=", "test-value")) + .build(); + var res = store.query(query); + assertThat(res).isSucceeded(); + Assertions.assertThat(res.getContent()).isNotNull().isEmpty(); + } + + @Test + void update() { + var credential = createCredentialBuilder(); + var result = store.create(credential.build()); + assertThat(result).isSucceeded(); + + var updateRes = store.update(credential.state(VcState.ISSUED).build()); + assertThat(updateRes).isSucceeded(); + } + + @Test + void update_whenIdChanges_fails() { + var credential = createCredentialBuilder(); + var result = store.create(credential.build()); + + var updateRes = store.update(credential.state(VcState.ISSUED).id("another-id").build()); + assertThat(updateRes).isFailed().detail().contains("with ID another-id was not found"); + } + + @Test + void update_whenNotExists() { + var credential = createCredentialBuilder(); + var updateRes = store.update(credential.state(VcState.ISSUED).id("another-id").build()); + assertThat(updateRes).isFailed().detail().contains("with ID another-id was not found"); + } + + @Test + void delete() { + var credential = createCredential(); + store.create(credential); + + var deleteRes = store.delete(credential.getId()); + assertThat(deleteRes).isSucceeded(); + } + + @Test + void delete_whenNotExists() { + assertThat(store.delete("not-exist")).isFailed() + .detail().contains("with ID not-exist was not found"); + } + + private VerifiableCredentialResource createCredential() { + return createCredentialBuilder().build(); + } + + private VerifiableCredentialResource.Builder createCredentialBuilder() { + return VerifiableCredentialResource.Builder.newInstance() + .issuerId("test-issuer") + .holderId("test-holder") + .id("test-id"); + } +} \ No newline at end of file diff --git a/core/identity-hub-transform/src/main/java/org/eclipse/edc/identityhub/transform/JsonObjectToPresentationQueryTransformer.java b/core/identity-hub-transform/src/main/java/org/eclipse/edc/identityhub/transform/JsonObjectToPresentationQueryTransformer.java index 94ea74b6e..abe4f1f10 100644 --- a/core/identity-hub-transform/src/main/java/org/eclipse/edc/identityhub/transform/JsonObjectToPresentationQueryTransformer.java +++ b/core/identity-hub-transform/src/main/java/org/eclipse/edc/identityhub/transform/JsonObjectToPresentationQueryTransformer.java @@ -60,7 +60,7 @@ private PresentationDefinition readPresentationDefinition(JsonValue v, Transform } else { jo = v.asJsonObject(); } - var rawJson = jo.getJsonObject(JsonLdKeywords.VALUE); + var rawJson = jo.get(JsonLdKeywords.VALUE); try { return mapper.readValue(rawJson.toString(), PresentationDefinition.class); } catch (JsonProcessingException e) { diff --git a/launcher/build.gradle.kts b/launcher/build.gradle.kts index 27f5cde6d..6b9bd93a0 100644 --- a/launcher/build.gradle.kts +++ b/launcher/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { runtimeOnly(project(":core:identity-hub-core")) runtimeOnly(project(":extensions:cryptography:public-key-provider")) runtimeOnly(libs.edc.identity.did.core) + runtimeOnly(libs.edc.identity.did.web) runtimeOnly(libs.bundles.connector) } diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/CredentialStore.java b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/CredentialStore.java new file mode 100644 index 000000000..9a46944a3 --- /dev/null +++ b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/CredentialStore.java @@ -0,0 +1,60 @@ +/* + * 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.identityhub.spi.store; + + +import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; + +import java.util.stream.Stream; + +/** + * The CredentialStore interface represents a store that manages verifiable credentials. + * It provides methods for creating, querying, updating, and deleting credentials. + */ +public interface CredentialStore { + /** + * Creates a verifiable credential resource in the store. + * + * @param credentialResource The verifiable credential resource to create. + * @return A StoreResult object indicating the result of the operation. + */ + StoreResult create(VerifiableCredentialResource credentialResource); + + /** + * Queries the store for verifiable credentials based on the given query specification. + * + * @param querySpec The {@link QuerySpec} indicating the criteria for the query. + * @return A {@link StoreResult} object containing a list of {@link VerifiableCredentialResource} objects that match the query. + */ + StoreResult> query(QuerySpec querySpec); + + /** + * Updates a verifiable credential resource in the store. + * + * @param credentialResource The verifiable credential resource to update. Note that all fields are overwritten. + * @return A {@link StoreResult} object indicating the result of the operation. + */ + StoreResult update(VerifiableCredentialResource credentialResource); + + /** + * Deletes a verifiable credential resource from the store based on the given ID. + * + * @param id The ID of the verifiable credential resource to delete. + * @return A {@link StoreResult} object indicating the result of the operation. + */ + StoreResult delete(String id); +} diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/IdentityResource.java b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/IdentityResource.java new file mode 100644 index 000000000..c78650a1e --- /dev/null +++ b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/IdentityResource.java @@ -0,0 +1,102 @@ +/* + * 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.identityhub.spi.store.model; + +import java.time.Clock; +import java.util.Objects; +import java.util.UUID; + +/** + * Abstract class representing an Identity Resource. + * Identity resources have an ID, a timestamp, an issuer ID, a holder ID, and a clock. + * They can be extended with custom properties and behaviors. + */ +public abstract class IdentityResource { + protected String id; + protected long timestamp; + protected String issuerId; + protected String holderId; + protected Clock clock; + + public Clock getClock() { + return clock; + } + + public String getId() { + return id; + } + + public long getTimestamp() { + return timestamp; + } + + public String getIssuerId() { + return issuerId; + } + + public String getHolderId() { + return holderId; + } + + public abstract static class Builder> { + protected final T resource; + + protected Builder(T resource) { + this.resource = resource; + } + + public B id(String id) { + resource.id = id; + return self(); + } + + public B timestamp(long timestamp) { + resource.timestamp = timestamp; + return self(); + } + + public B issuerId(String issuerId) { + resource.issuerId = issuerId; + return self(); + } + + public B clock(Clock clock) { + resource.clock = clock; + return self(); + } + + public B holderId(String holderId) { + resource.holderId = holderId; + return self(); + } + + public abstract B self(); + + protected T build() { + Objects.requireNonNull(resource.issuerId, "Must have an issuer."); + Objects.requireNonNull(resource.holderId, "Must have a holder."); + resource.clock = Objects.requireNonNullElse(resource.clock, Clock.systemUTC()); + + if (resource.id == null) { + resource.id = UUID.randomUUID().toString(); + } + + if (resource.timestamp == 0) { + resource.timestamp = resource.clock.millis(); + } + return resource; + } + } +} diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/VcState.java b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/VcState.java new file mode 100644 index 000000000..fda4f3abf --- /dev/null +++ b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/VcState.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.edc.identityhub.spi.store.model; + +import java.util.Arrays; + +public enum VcState { + INITIAL(100), + REQUESTING(200), + REQUESTED(300), + ISSUING(400), + ISSUED(500), + REISSUE_REQUESTING(600), + REISSUE_REQUESTED(700), + TERMINATED(800), + ERROR(900); + + private final int code; + + VcState(int code) { + this.code = code; + } + + public static VcState from(int code) { + return Arrays.stream(values()).filter(tps -> tps.code == code).findFirst().orElse(null); + } + + public int getCode() { + return code; + } +} diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/VerifiableCredentialResource.java b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/VerifiableCredentialResource.java new file mode 100644 index 000000000..aa8785a5c --- /dev/null +++ b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/VerifiableCredentialResource.java @@ -0,0 +1,94 @@ +/* + * 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.identityhub.spi.store.model; + +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.policy.model.Policy; + +/** + * Represents a Verifiable Credential Resource. + * The Verifiable Credential Resource extends the Identity Resource class and adds additional properties specific to verifiable credentials, + * specifically the issuance and re-issuance policies as well as a representation of the VC + */ +public class VerifiableCredentialResource extends IdentityResource { + private VcState state; + private Policy issuancePolicy; + private Policy reissuancePolicy; + private VerifiableCredentialContainer verifiableCredential; + + private VerifiableCredentialResource() { + + } + + public VcState getState() { + return state; + } + + public Policy getIssuancePolicy() { + return issuancePolicy; + } + + public Policy getReissuancePolicy() { + return reissuancePolicy; + } + + public VerifiableCredentialContainer getVerifiableCredential() { + return verifiableCredential; + } + + public static class Builder extends IdentityResource.Builder { + + protected Builder(VerifiableCredentialResource resource) { + super(resource); + } + + public static Builder newInstance() { + return new Builder(new VerifiableCredentialResource()); + } + + public Builder state(VcState state) { + resource.state = state; + return self(); + } + + public Builder issuancePolicy(Policy issuancePolicy) { + resource.issuancePolicy = issuancePolicy; + return self(); + } + + public Builder reissuancePolicy(Policy reissuancePolicy) { + resource.reissuancePolicy = reissuancePolicy; + return self(); + } + + public Builder credential(VerifiableCredentialContainer credential) { + resource.verifiableCredential = credential; + return self(); + } + + @Override + public Builder self() { + return this; + } + + @Override + public VerifiableCredentialResource build() { + if (resource.state == null) { + resource.state = VcState.INITIAL; + } + return super.build(); + } + } +} diff --git a/spi/identity-hub-store-spi/src/test/java/org/eclipse/edc/identityhub/spi/store/model/VerifiableCredentialResourceTest.java b/spi/identity-hub-store-spi/src/test/java/org/eclipse/edc/identityhub/spi/store/model/VerifiableCredentialResourceTest.java new file mode 100644 index 000000000..3b2e10cca --- /dev/null +++ b/spi/identity-hub-store-spi/src/test/java/org/eclipse/edc/identityhub/spi/store/model/VerifiableCredentialResourceTest.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.edc.identityhub.spi.store.model; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class VerifiableCredentialResourceTest { + + @Test + void verifyBuilder_whenInvalidRequiredProperties() { + // missing holder and issuer + assertThatThrownBy(() -> VerifiableCredentialResource.Builder.newInstance().build()).isInstanceOf(NullPointerException.class); + // missing issuer + assertThatThrownBy(() -> VerifiableCredentialResource.Builder.newInstance() + .holderId("test-holder") + .build()).isInstanceOf(NullPointerException.class); + + //missing holder + assertThatThrownBy(() -> VerifiableCredentialResource.Builder.newInstance() + .issuerId("test-issuer") + .build()).isInstanceOf(NullPointerException.class); + } + + @Test + void verifyBuilder_assertDefaultValues() { + var vc = VerifiableCredentialResource.Builder.newInstance() + .issuerId("test-issuer") + .holderId("test-holder") + .build(); + + assertThat(vc.getClock()).isNotNull(); + assertThat(vc.id).isNotNull(); + assertThat(vc.getState()).isEqualTo(VcState.INITIAL); + } +} \ No newline at end of file