From 3d216d9abe14b1c12be54b6d2f8de5196dc67ef2 Mon Sep 17 00:00:00 2001 From: Enrico Risa Date: Thu, 6 Feb 2025 15:11:25 +0100 Subject: [PATCH] feat: implements credential definition vertical --- .../issuerservice-core/build.gradle.kts | 2 + .../defaults/DefaultServiceExtension.java | 14 + .../InMemoryAttestationDefinitionStore.java | 46 +++ .../InMemoryCredentialDefinitionStore.java | 101 ++++++ .../defaults/DefaultServiceExtensionTest.java | 4 + ...InMemoryCredentialDefinitionStoreTest.java | 28 ++ .../build.gradle.kts | 14 + .../CredentialDefinitionServiceExtension.java | 43 +++ .../CredentialDefinitionServiceImpl.java | 102 ++++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 14 + .../store/InMemoryEntityStore.java | 11 + .../issuerservice-base-bom/build.gradle.kts | 1 + e2e-tests/admin-api-tests/build.gradle.kts | 1 + .../CredentialDefinitionApiEndToEndTest.java | 330 ++++++++++++++++++ .../api/issuer-admin-api/build.gradle.kts | 1 + .../build.gradle.kts | 44 +++ ...CredentialDefinitionAdminApiExtension.java | 47 +++ .../IssuerCredentialDefinitionAdminApi.java | 116 ++++++ ...redentialDefinitionAdminApiController.java | 87 +++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 + ...ntialDefinitionAdminApiControllerTest.java | 175 ++++++++++ .../resources/issuer-admin-api-version.json | 2 +- .../main/resources/issuer-api-version.json | 2 +- .../build.gradle.kts | 28 ++ .../BaseSqlDialectStatements.java | 82 +++++ .../CredentialDefinitionStoreStatements.java | 85 +++++ .../SqlCredentialDefinitionStore.java | 214 ++++++++++++ ...SqlCredentialDefinitionStoreExtension.java | 73 ++++ .../postgres/CredentialDefinitionMapping.java | 38 ++ .../postgres/PostgresDialectStatements.java | 29 ++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 + .../credential-definition-schema.sql | 31 ++ .../SqlCredentialDefinitionStoreTest.java | 57 +++ resources/openapi/issuer-api.version | 2 +- settings.gradle.kts | 4 + .../AttestationDefinitionStore.java | 11 + .../model/CredentialDefinition.java | 28 +- .../build.gradle.kts | 27 ++ .../CredentialDefinitionService.java | 34 ++ .../store/CredentialDefinitionStore.java | 78 +++++ .../CredentialDefinitionStoreTestBase.java | 197 +++++++++++ 41 files changed, 2229 insertions(+), 4 deletions(-) create mode 100644 core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryAttestationDefinitionStore.java create mode 100644 core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryCredentialDefinitionStore.java create mode 100644 core/issuerservice/issuerservice-core/src/test/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryCredentialDefinitionStoreTest.java create mode 100644 core/issuerservice/issuerservice-credential-definitions/build.gradle.kts create mode 100644 core/issuerservice/issuerservice-credential-definitions/src/main/java/org/eclipse/edc/issuerservice/credentialdefinition/CredentialDefinitionServiceExtension.java create mode 100644 core/issuerservice/issuerservice-credential-definitions/src/main/java/org/eclipse/edc/issuerservice/credentialdefinition/CredentialDefinitionServiceImpl.java create mode 100644 core/issuerservice/issuerservice-credential-definitions/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/CredentialDefinitionApiEndToEndTest.java create mode 100644 extensions/api/issuer-admin-api/credential-definition-api/build.gradle.kts create mode 100644 extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/IssuerCredentialDefinitionAdminApiExtension.java create mode 100644 extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApi.java create mode 100644 extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApiController.java create mode 100644 extensions/api/issuer-admin-api/credential-definition-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/api/issuer-admin-api/credential-definition-api/src/test/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApiControllerTest.java create mode 100644 extensions/store/sql/issuerservice-credential-definition-store-sql/build.gradle.kts create mode 100644 extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/BaseSqlDialectStatements.java create mode 100644 extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/CredentialDefinitionStoreStatements.java create mode 100644 extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStore.java create mode 100644 extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStoreExtension.java create mode 100644 extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/schema/postgres/CredentialDefinitionMapping.java create mode 100644 extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/schema/postgres/PostgresDialectStatements.java create mode 100644 extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/resources/credential-definition-schema.sql create mode 100644 extensions/store/sql/issuerservice-credential-definition-store-sql/src/test/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStoreTest.java create mode 100644 spi/issuerservice/issuerservice-credential-definition-spi/build.gradle.kts create mode 100644 spi/issuerservice/issuerservice-credential-definition-spi/src/main/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/CredentialDefinitionService.java create mode 100644 spi/issuerservice/issuerservice-credential-definition-spi/src/main/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/store/CredentialDefinitionStore.java create mode 100644 spi/issuerservice/issuerservice-credential-definition-spi/src/testFixtures/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/store/CredentialDefinitionStoreTestBase.java diff --git a/core/issuerservice/issuerservice-core/build.gradle.kts b/core/issuerservice/issuerservice-core/build.gradle.kts index 2aca45358..fc9f50a70 100644 --- a/core/issuerservice/issuerservice-core/build.gradle.kts +++ b/core/issuerservice/issuerservice-core/build.gradle.kts @@ -4,10 +4,12 @@ plugins { dependencies { api(project(":spi:issuerservice:issuerservice-participant-spi")) + api(project(":spi:issuerservice:issuerservice-credential-definition-spi")) implementation(project(":core:lib:common-lib")) implementation(libs.edc.lib.store) testImplementation(libs.edc.junit) testImplementation(testFixtures(project(":spi:issuerservice:issuerservice-participant-spi"))) + testImplementation(testFixtures(project(":spi:issuerservice:issuerservice-credential-definition-spi"))) } diff --git a/core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/DefaultServiceExtension.java b/core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/DefaultServiceExtension.java index fb576c13d..001f8cf44 100644 --- a/core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/DefaultServiceExtension.java +++ b/core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/DefaultServiceExtension.java @@ -14,7 +14,11 @@ package org.eclipse.edc.issuerservice.defaults; +import org.eclipse.edc.identityhub.spi.issuance.credentials.attestation.AttestationDefinitionStore; +import org.eclipse.edc.issuerservice.defaults.store.InMemoryAttestationDefinitionStore; +import org.eclipse.edc.issuerservice.defaults.store.InMemoryCredentialDefinitionStore; import org.eclipse.edc.issuerservice.defaults.store.InMemoryParticipantStore; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.store.CredentialDefinitionStore; import org.eclipse.edc.issuerservice.spi.participant.store.ParticipantStore; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Provider; @@ -31,4 +35,14 @@ public ParticipantStore createInMemoryParticipantStore() { return new InMemoryParticipantStore(); } + + @Provider(isDefault = true) + public AttestationDefinitionStore createInMemoryAttestationStore() { + return new InMemoryAttestationDefinitionStore(); + } + + @Provider(isDefault = true) + public CredentialDefinitionStore createInMemoryCredentialDefinitionStore() { + return new InMemoryCredentialDefinitionStore(); + } } diff --git a/core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryAttestationDefinitionStore.java b/core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryAttestationDefinitionStore.java new file mode 100644 index 000000000..7866b27c2 --- /dev/null +++ b/core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryAttestationDefinitionStore.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.defaults.store; + +import org.eclipse.edc.identityhub.spi.issuance.credentials.attestation.AttestationDefinitionStore; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.AttestationDefinition; +import org.eclipse.edc.identityhub.store.InMemoryEntityStore; +import org.eclipse.edc.spi.query.QueryResolver; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.store.ReflectionBasedQueryResolver; +import org.jetbrains.annotations.Nullable; + +public class InMemoryAttestationDefinitionStore extends InMemoryEntityStore implements AttestationDefinitionStore { + + @Override + public @Nullable AttestationDefinition resolveDefinition(String id) { + return store.get(id); + } + + @Override + public StoreResult delete(String id) { + return super.deleteById(id); + } + + @Override + protected String getId(AttestationDefinition newObject) { + return newObject.id(); + } + + @Override + protected QueryResolver createQueryResolver() { + return new ReflectionBasedQueryResolver<>(AttestationDefinition.class, criterionOperatorRegistry); + } +} diff --git a/core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryCredentialDefinitionStore.java b/core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryCredentialDefinitionStore.java new file mode 100644 index 000000000..9730b8054 --- /dev/null +++ b/core/issuerservice/issuerservice-core/src/main/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryCredentialDefinitionStore.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.defaults.store; + +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.identityhub.store.InMemoryEntityStore; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.store.CredentialDefinitionStore; +import org.eclipse.edc.spi.query.QueryResolver; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.store.ReflectionBasedQueryResolver; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +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 InMemoryCredentialDefinitionStore extends InMemoryEntityStore implements CredentialDefinitionStore { + + + private final Map credentialTypes = new HashMap<>(); + + @Override + public StoreResult create(CredentialDefinition credentialDefinition) { + lock.writeLock().lock(); + try { + if (credentialTypes.containsKey(credentialDefinition.getCredentialType())) { + return alreadyExists(alreadyExistsForTypeErrorMessage(credentialDefinition.getCredentialType())); + } + if (store.containsKey(credentialDefinition.getId())) { + return alreadyExists(alreadyExistsErrorMessage(credentialDefinition.getId())); + } + store.put(credentialDefinition.getId(), credentialDefinition); + credentialTypes.put(credentialDefinition.getCredentialType(), credentialDefinition.getId()); + return success(null); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public StoreResult update(CredentialDefinition credentialDefinition) { + lock.writeLock().lock(); + try { + if (!store.containsKey(credentialDefinition.getId())) { + return notFound(notFoundErrorMessage(credentialDefinition.getId())); + } + var credentialId = credentialTypes.get(credentialDefinition.getCredentialType()); + if (credentialId != null && !credentialId.equals(credentialDefinition.getId())) { + return alreadyExists(alreadyExistsForTypeErrorMessage(credentialDefinition.getCredentialType())); + } + var oldDefinition = store.put(credentialDefinition.getId(), credentialDefinition); + + Optional.ofNullable(oldDefinition) + .map(CredentialDefinition::getCredentialType) + .ifPresent(credentialTypes::remove); + + return success(); + } finally { + lock.writeLock().unlock(); + } + } + + public StoreResult deleteById(String id) { + lock.writeLock().lock(); + try { + if (!store.containsKey(id)) { + return notFound(notFoundErrorMessage(id)); + } + var credential = store.remove(id); + credentialTypes.remove(credential.getCredentialType()); + return success(); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + protected String getId(CredentialDefinition newObject) { + return newObject.getId(); + } + + @Override + protected QueryResolver createQueryResolver() { + return new ReflectionBasedQueryResolver<>(CredentialDefinition.class, criterionOperatorRegistry); + } +} diff --git a/core/issuerservice/issuerservice-core/src/test/java/org/eclipse/edc/issuerservice/defaults/DefaultServiceExtensionTest.java b/core/issuerservice/issuerservice-core/src/test/java/org/eclipse/edc/issuerservice/defaults/DefaultServiceExtensionTest.java index 0c7a2ed49..f85fd97e8 100644 --- a/core/issuerservice/issuerservice-core/src/test/java/org/eclipse/edc/issuerservice/defaults/DefaultServiceExtensionTest.java +++ b/core/issuerservice/issuerservice-core/src/test/java/org/eclipse/edc/issuerservice/defaults/DefaultServiceExtensionTest.java @@ -14,6 +14,8 @@ package org.eclipse.edc.issuerservice.defaults; +import org.eclipse.edc.issuerservice.defaults.store.InMemoryAttestationDefinitionStore; +import org.eclipse.edc.issuerservice.defaults.store.InMemoryCredentialDefinitionStore; import org.eclipse.edc.issuerservice.defaults.store.InMemoryParticipantStore; import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; import org.junit.jupiter.api.Test; @@ -26,6 +28,8 @@ class DefaultServiceExtensionTest { @Test void verifyDefaultServices(DefaultServiceExtension extension) { assertThat(extension.createInMemoryParticipantStore()).isInstanceOf(InMemoryParticipantStore.class); + assertThat(extension.createInMemoryCredentialDefinitionStore()).isInstanceOf(InMemoryCredentialDefinitionStore.class); + assertThat(extension.createInMemoryAttestationStore()).isInstanceOf(InMemoryAttestationDefinitionStore.class); } } \ No newline at end of file diff --git a/core/issuerservice/issuerservice-core/src/test/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryCredentialDefinitionStoreTest.java b/core/issuerservice/issuerservice-core/src/test/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryCredentialDefinitionStoreTest.java new file mode 100644 index 000000000..0703e4a20 --- /dev/null +++ b/core/issuerservice/issuerservice-core/src/test/java/org/eclipse/edc/issuerservice/defaults/store/InMemoryCredentialDefinitionStoreTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.defaults.store; + +import org.eclipse.edc.issuerservice.spi.credentialdefinition.store.CredentialDefinitionStore; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.store.CredentialDefinitionStoreTestBase; + +public class InMemoryCredentialDefinitionStoreTest extends CredentialDefinitionStoreTestBase { + + private final InMemoryCredentialDefinitionStore store = new InMemoryCredentialDefinitionStore(); + + @Override + protected CredentialDefinitionStore getStore() { + return store; + } +} diff --git a/core/issuerservice/issuerservice-credential-definitions/build.gradle.kts b/core/issuerservice/issuerservice-credential-definitions/build.gradle.kts new file mode 100644 index 000000000..c2168ec60 --- /dev/null +++ b/core/issuerservice/issuerservice-credential-definitions/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + `java-library` +} + +dependencies { + api(project(":spi:issuerservice:issuerservice-credential-definition-spi")) + api(project(":spi:issuance-credentials-spi")) + + implementation(libs.edc.spi.transaction) + implementation(libs.edc.lib.store) + testImplementation(libs.edc.junit) + testImplementation(testFixtures(project(":spi:issuerservice:issuerservice-credential-definition-spi"))) + +} diff --git a/core/issuerservice/issuerservice-credential-definitions/src/main/java/org/eclipse/edc/issuerservice/credentialdefinition/CredentialDefinitionServiceExtension.java b/core/issuerservice/issuerservice-credential-definitions/src/main/java/org/eclipse/edc/issuerservice/credentialdefinition/CredentialDefinitionServiceExtension.java new file mode 100644 index 000000000..956a844f2 --- /dev/null +++ b/core/issuerservice/issuerservice-credential-definitions/src/main/java/org/eclipse/edc/issuerservice/credentialdefinition/CredentialDefinitionServiceExtension.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.credentialdefinition; + +import org.eclipse.edc.identityhub.spi.issuance.credentials.attestation.AttestationDefinitionStore; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.store.CredentialDefinitionStore; +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.edc.transaction.spi.TransactionContext; + +import static org.eclipse.edc.issuerservice.credentialdefinition.CredentialDefinitionServiceExtension.NAME; + +@Extension(value = NAME) +public class CredentialDefinitionServiceExtension implements ServiceExtension { + public static final String NAME = "IssuerService Credential Definition Service Extension"; + @Inject + private TransactionContext transactionContext; + @Inject + private CredentialDefinitionStore store; + + @Inject + private AttestationDefinitionStore attestationDefinitionStore; + + @Provider + public CredentialDefinitionService getParticipantService() { + return new CredentialDefinitionServiceImpl(transactionContext, store, attestationDefinitionStore); + } +} diff --git a/core/issuerservice/issuerservice-credential-definitions/src/main/java/org/eclipse/edc/issuerservice/credentialdefinition/CredentialDefinitionServiceImpl.java b/core/issuerservice/issuerservice-credential-definitions/src/main/java/org/eclipse/edc/issuerservice/credentialdefinition/CredentialDefinitionServiceImpl.java new file mode 100644 index 000000000..9dc1dd6f0 --- /dev/null +++ b/core/issuerservice/issuerservice-credential-definitions/src/main/java/org/eclipse/edc/issuerservice/credentialdefinition/CredentialDefinitionServiceImpl.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.credentialdefinition; + +import org.eclipse.edc.identityhub.spi.issuance.credentials.attestation.AttestationDefinitionStore; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.AttestationDefinition; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.store.CredentialDefinitionStore; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import java.util.Collection; +import java.util.stream.Collectors; + +import static org.eclipse.edc.spi.result.ServiceResult.from; + +public class CredentialDefinitionServiceImpl implements CredentialDefinitionService { + private final TransactionContext transactionContext; + private final CredentialDefinitionStore credentialDefinitionStore; + private final AttestationDefinitionStore attestationDefinitionStore; + + public CredentialDefinitionServiceImpl(TransactionContext transactionContext, CredentialDefinitionStore credentialDefinitionStore, AttestationDefinitionStore attestationDefinitionStore) { + this.transactionContext = transactionContext; + this.credentialDefinitionStore = credentialDefinitionStore; + this.attestationDefinitionStore = attestationDefinitionStore; + } + + @Override + public ServiceResult createCredentialDefinition(CredentialDefinition credentialDefinition) { + return transactionContext.execute(() -> internalCreate(credentialDefinition)); + + } + + @Override + public ServiceResult deleteCredentialDefinition(String credentialDefinitionId) { + return transactionContext.execute(() -> from(credentialDefinitionStore.deleteById(credentialDefinitionId))); + } + + @Override + public ServiceResult updateCredentialDefinition(CredentialDefinition credentialDefinition) { + return transactionContext.execute(() -> internalUpdate(credentialDefinition)); + } + + @Override + public ServiceResult> queryCredentialDefinitions(QuerySpec querySpec) { + return transactionContext.execute(() -> from(credentialDefinitionStore.query(querySpec))); + + } + + @Override + public ServiceResult findById(String credentialDefinitionId) { + return transactionContext.execute(() -> from(credentialDefinitionStore.findById(credentialDefinitionId))); + + } + + private ServiceResult internalCreate(CredentialDefinition credentialDefinition) { + return validateAttestations(credentialDefinition) + .compose(u -> from(credentialDefinitionStore.create(credentialDefinition))); + } + + private ServiceResult internalUpdate(CredentialDefinition credentialDefinition) { + return validateAttestations(credentialDefinition) + .compose(u -> from(credentialDefinitionStore.update(credentialDefinition))); + } + + private ServiceResult validateAttestations(CredentialDefinition credentialDefinition) { + var query = QuerySpec.Builder.newInstance() + .filter(Criterion.criterion("id", "in", credentialDefinition.getAttestations())) + .build(); + return from(attestationDefinitionStore.query(query)) + .compose(attestationDefinitions -> checkAttestations(credentialDefinition, attestationDefinitions)); + } + + private ServiceResult checkAttestations(CredentialDefinition credentialDefinition, Collection attestationDefinitions) { + if (attestationDefinitions.size() != credentialDefinition.getAttestations().size()) { + + var attestationsIds = attestationDefinitions.stream().map(AttestationDefinition::id).collect(Collectors.toSet()); + + var missingAttestations = credentialDefinition.getAttestations().stream() + .filter(attestationId -> !attestationsIds.contains(attestationId)) + .collect(Collectors.toSet()); + + return ServiceResult.badRequest("Attestation definitions [%s] not found".formatted(String.join(",", missingAttestations))); + } + return ServiceResult.success(); + } +} diff --git a/core/issuerservice/issuerservice-credential-definitions/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/core/issuerservice/issuerservice-credential-definitions/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..cb054551e --- /dev/null +++ b/core/issuerservice/issuerservice-credential-definitions/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,14 @@ +# +# Copyright (c) 2025 Cofinity-X +# +# 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: +# Cofinity-X - initial API and implementation +# +# +org.eclipse.edc.issuerservice.credentialdefinition.CredentialDefinitionServiceExtension diff --git a/core/lib/common-lib/src/main/java/org/eclipse/edc/identityhub/store/InMemoryEntityStore.java b/core/lib/common-lib/src/main/java/org/eclipse/edc/identityhub/store/InMemoryEntityStore.java index 6c125c4f5..0e356311f 100644 --- a/core/lib/common-lib/src/main/java/org/eclipse/edc/identityhub/store/InMemoryEntityStore.java +++ b/core/lib/common-lib/src/main/java/org/eclipse/edc/identityhub/store/InMemoryEntityStore.java @@ -44,6 +44,17 @@ protected InMemoryEntityStore() { queryResolver = createQueryResolver(); } + + public StoreResult findById(String id) { + lock.readLock().lock(); + try { + var result = store.get(id); + return result == null ? notFound("An entity with ID '%s' does not exist.".formatted(id)) : success(result); + } finally { + lock.readLock().unlock(); + } + } + /** * Creates a new entity if none exists. * diff --git a/dist/bom/issuerservice-base-bom/build.gradle.kts b/dist/bom/issuerservice-base-bom/build.gradle.kts index 0f72fe55a..741c02117 100644 --- a/dist/bom/issuerservice-base-bom/build.gradle.kts +++ b/dist/bom/issuerservice-base-bom/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { runtimeOnly(project(":core:identity-hub-keypairs")) runtimeOnly(project(":core:issuerservice:issuerservice-core")) runtimeOnly(project(":core:issuerservice:issuerservice-participants")) + runtimeOnly(project(":core:issuerservice:issuerservice-credential-definitions")) runtimeOnly(project(":extensions:did:local-did-publisher")) // API modules runtimeOnly(project(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-api")) diff --git a/e2e-tests/admin-api-tests/build.gradle.kts b/e2e-tests/admin-api-tests/build.gradle.kts index 0a70fff16..c1423a803 100644 --- a/e2e-tests/admin-api-tests/build.gradle.kts +++ b/e2e-tests/admin-api-tests/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { testImplementation(project(":core:identity-hub-participants")) testImplementation(project(":extensions:api:issuer-admin-api:participant-api")) // for the DTOs testImplementation(project(":spi:issuerservice:issuerservice-participant-spi")) + testImplementation(project(":spi:issuerservice:issuerservice-credential-definition-spi")) testImplementation(libs.edc.junit) testImplementation(libs.restAssured) testImplementation(libs.awaitility) diff --git a/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/CredentialDefinitionApiEndToEndTest.java b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/CredentialDefinitionApiEndToEndTest.java new file mode 100644 index 000000000..663d1fc46 --- /dev/null +++ b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/CredentialDefinitionApiEndToEndTest.java @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.tests; + +import io.restassured.http.ContentType; +import org.eclipse.edc.identityhub.spi.issuance.credentials.attestation.AttestationDefinitionStore; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.AttestationDefinition; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialRuleDefinition; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.MappingDefinition; +import org.eclipse.edc.identityhub.tests.fixtures.IssuerServiceEndToEndExtension; +import org.eclipse.edc.identityhub.tests.fixtures.IssuerServiceEndToEndTestContext; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + + +public class CredentialDefinitionApiEndToEndTest { + abstract static class Tests { + + + @AfterEach + void teardown(CredentialDefinitionService service) { + service.queryCredentialDefinitions(QuerySpec.max()).getContent() + .forEach(p -> service.deleteCredentialDefinition(p.getId()).getContent()); + + } + + @Test + void createCredentialDefinition(IssuerServiceEndToEndTestContext context, CredentialDefinitionService service, AttestationDefinitionStore store) { + + + store.create(new AttestationDefinition("test-attestation", "type", Map.of())); + + var definition = CredentialDefinition.Builder.newInstance() + .id("test-definition-id") + .schema("https://example.org/my-schema.json") + .credentialType("MembershipCredential") + .mapping(new MappingDefinition("input", "output", true)) + .validity(1000) + .rule(new CredentialRuleDefinition("rule", Map.of("key", "value"))) + .attestation("test-attestation") + .build(); + + context.getAdminEndpoint().baseRequest() + .contentType(ContentType.JSON) + .body(definition) + .post("/v1alpha/credentialdefinitions") + .then() + .statusCode(201) + .header("Location", Matchers.endsWith("/credentialdefinitions/test-definition-id")); + + assertThat(service.findById(definition.getId())).isSucceeded() + .usingRecursiveComparison() + .isEqualTo(definition); + } + + @Test + void createCredentialDefinition_whenExists(IssuerServiceEndToEndTestContext context, CredentialDefinitionService service) { + + var definition = CredentialDefinition.Builder.newInstance() + .id("test-definition-id") + .schema("https://example.org/my-schema.json") + .credentialType("MyType") + .build(); + + service.createCredentialDefinition(definition); + + context.getAdminEndpoint().baseRequest() + .contentType(ContentType.JSON) + .body(""" + { + "id": "test-definition-id", + "credentialType": "MembershipCredential", + "schema": "https://example.org/membership-credential-schema.json" + } + """) + .post("/v1alpha/credentialdefinitions") + .then() + .statusCode(409); + } + + @Test + void createCredentialDefinition_whenCredentialTypeExists(IssuerServiceEndToEndTestContext context, CredentialDefinitionService service) { + + var definition = CredentialDefinition.Builder.newInstance() + .id("id") + .schema("https://example.org/my-schema.json") + .credentialType("MembershipCredential") + .build(); + + service.createCredentialDefinition(definition); + + context.getAdminEndpoint().baseRequest() + .contentType(ContentType.JSON) + .body(""" + { + "id": "test-definition-id", + "credentialType": "MembershipCredential", + "schema": "https://example.org/membership-credential-schema.json" + } + """) + .post("/v1alpha/credentialdefinitions") + .then() + .statusCode(409); + } + + @Test + void createCredentialDefinition_whenMissingFields(IssuerServiceEndToEndTestContext context) { + + context.getAdminEndpoint().baseRequest() + .contentType(ContentType.JSON) + .body(""" + { + "id": "test-definition-id" + } + """) + .post("/v1alpha/credentialdefinitions") + .then() + .statusCode(400); + } + + @Test + void createCredentialDefinition_whenMissingAttestations(IssuerServiceEndToEndTestContext context) { + + context.getAdminEndpoint().baseRequest() + .contentType(ContentType.JSON) + .body(""" + { + "id": "test-definition-id", + "credentialType": "MembershipCredential", + "schema": "https://example.org/membership-credential-schema.json", + "attestations": ["notfound"] + } + """) + .post("/v1alpha/credentialdefinitions") + .then() + .statusCode(400) + .body("[0].message", containsString("notfound")); + } + + + @Test + void queryCredentialDefinitions(IssuerServiceEndToEndTestContext context, CredentialDefinitionService service) { + + var definition = CredentialDefinition.Builder.newInstance() + .id("id") + .schema("https://example.org/my-schema.json") + .credentialType("MembershipCredential") + .build(); + + service.createCredentialDefinition(definition); + + var res = context.getAdminEndpoint().baseRequest() + .contentType(ContentType.JSON) + .body(QuerySpec.Builder.newInstance().filter(new Criterion("credentialType", "=", "MembershipCredential")).build()) + .post("/v1alpha/credentialdefinitions/query") + .then() + .statusCode(200) + .body(Matchers.notNullValue()) + .extract().body().as(CredentialDefinition[].class); + + assertThat(res).hasSize(1).allSatisfy(d -> assertThat(definition).usingRecursiveComparison().isEqualTo(d)); + } + + @Test + void queryCredentialDefinitions_noResult(IssuerServiceEndToEndTestContext context) { + + var res = context.getAdminEndpoint().baseRequest() + .contentType(ContentType.JSON) + .body(QuerySpec.Builder.newInstance().filter(new Criterion("id", "=", "test-credential-definition-id")).build()) + .post("/v1alpha/credentialdefinitions/query") + .then() + .statusCode(200) + .body(Matchers.notNullValue()) + .extract().body().as(CredentialDefinition[].class); + + assertThat(res).isEmpty(); + } + + @Test + void getById(IssuerServiceEndToEndTestContext context, CredentialDefinitionService service) { + + var definition = CredentialDefinition.Builder.newInstance() + .id("test-credential-definition-id") + .schema("https://example.org/my-schema.json") + .credentialType("MembershipCredential") + .build(); + + service.createCredentialDefinition(definition); + + var res = context.getAdminEndpoint().baseRequest() + .get("/v1alpha/credentialdefinitions/test-credential-definition-id") + .then() + .statusCode(200) + .body(Matchers.notNullValue()) + .extract().body().as(CredentialDefinition.class); + + assertThat(res).usingRecursiveComparison().isEqualTo(definition); + } + + @Test + void getById_whenNotFound(IssuerServiceEndToEndTestContext context) { + + + context.getAdminEndpoint().baseRequest() + .get("/v1alpha/credentialdefinitions/test-credential-definition-id") + .then() + .statusCode(404); + + } + + + @Test + void updateCredentialDefinition(IssuerServiceEndToEndTestContext context, CredentialDefinitionService service) { + + var definition = CredentialDefinition.Builder.newInstance() + .id("test-credential-definition-id") + .schema("https://example.org/my-schema.json") + .credentialType("MembershipCredential") + .build(); + + service.createCredentialDefinition(definition); + + definition = CredentialDefinition.Builder.newInstance() + .id("test-credential-definition-id") + .schema("https://example.org/new-schema.json") + .credentialType("MembershipCredential") + .build(); + + context.getAdminEndpoint().baseRequest() + .contentType(ContentType.JSON) + .body(definition) + .put("/v1alpha/credentialdefinitions") + .then() + .statusCode(200); + + var updatedDefinition = service.findById(definition.getId()).getContent(); + + assertThat(updatedDefinition).usingRecursiveComparison().isEqualTo(definition); + } + + @Test + void updateCredentialDefinition_whenNotFound(IssuerServiceEndToEndTestContext context, CredentialDefinitionService service) { + + var definition = CredentialDefinition.Builder.newInstance() + .id("test-credential-definition-id") + .schema("https://example.org/my-schema.json") + .credentialType("MembershipCredential") + .build(); + + context.getAdminEndpoint().baseRequest() + .contentType(ContentType.JSON) + .body(definition) + .put("/v1alpha/credentialdefinitions") + .then() + .statusCode(404); + + } + + @Test + void deleteCredentialDefinition(IssuerServiceEndToEndTestContext context, CredentialDefinitionService service) { + + var definition = CredentialDefinition.Builder.newInstance() + .id("test-credential-definition-id") + .schema("https://example.org/my-schema.json") + .credentialType("MembershipCredential") + .build(); + + service.createCredentialDefinition(definition); + + context.getAdminEndpoint().baseRequest() + .delete("/v1alpha/credentialdefinitions/test-credential-definition-id") + .then() + .statusCode(204); + + assertThat(service.findById(definition.getId())).isFailed(); + + } + + @Test + void deleteCredentialDefinition_whenNotExists(IssuerServiceEndToEndTestContext context) { + + + context.getAdminEndpoint().baseRequest() + .delete("/v1alpha/credentialdefinitions/test-credential-definition-id") + .then() + .statusCode(404); + + } + } + + @Nested + @EndToEndTest + @ExtendWith(IssuerServiceEndToEndExtension.InMemory.class) + class InMemory extends Tests { + } + + @Nested + @PostgresqlIntegrationTest + @ExtendWith(IssuerServiceEndToEndExtension.Postgres.class) + class Postgres extends Tests { + + } +} diff --git a/extensions/api/issuer-admin-api/build.gradle.kts b/extensions/api/issuer-admin-api/build.gradle.kts index 3193efef1..bf316a972 100644 --- a/extensions/api/issuer-admin-api/build.gradle.kts +++ b/extensions/api/issuer-admin-api/build.gradle.kts @@ -21,4 +21,5 @@ dependencies { api(project(":extensions:api:issuer-admin-api:attestation-api")) api(project(":extensions:api:issuer-admin-api:credentials-api")) api(project(":extensions:api:issuer-admin-api:participant-api")) + api(project(":extensions:api:issuer-admin-api:credential-definition-api")) } diff --git a/extensions/api/issuer-admin-api/credential-definition-api/build.gradle.kts b/extensions/api/issuer-admin-api/credential-definition-api/build.gradle.kts new file mode 100644 index 000000000..ed05e88ff --- /dev/null +++ b/extensions/api/issuer-admin-api/credential-definition-api/build.gradle.kts @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +plugins { + `java-library` + `maven-publish` + id("io.swagger.core.v3.swagger-gradle-plugin") +} + +dependencies { + api(project(":spi:issuerservice:issuerservice-credential-definition-spi")) + implementation(project(":extensions:api:issuer-admin-api:issuer-admin-api-configuration")) + implementation(libs.edc.spi.validator) + implementation(libs.edc.spi.web) + implementation(libs.edc.lib.util) + implementation(libs.edc.lib.jerseyproviders) + implementation(libs.jakarta.rsApi) + implementation(libs.jakarta.annotation) + + + testImplementation(libs.edc.junit) + testImplementation(libs.edc.jsonld) + testImplementation(testFixtures(libs.edc.core.jersey)) + testImplementation(libs.nimbus.jwt) + testImplementation(libs.restAssured) + testImplementation(libs.tink) +} + +edcBuild { + swagger { + apiGroup.set("issuer-admin-api") + } +} diff --git a/extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/IssuerCredentialDefinitionAdminApiExtension.java b/extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/IssuerCredentialDefinitionAdminApiExtension.java new file mode 100644 index 000000000..778e3b514 --- /dev/null +++ b/extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/IssuerCredentialDefinitionAdminApiExtension.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.api.admin.credentialdefinition; + +import org.eclipse.edc.identityhub.spi.webcontext.IdentityHubApiContext; +import org.eclipse.edc.issuerservice.api.admin.credentialdefinition.v1.unstable.IssuerCredentialDefinitionAdminApiController; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; + +import static org.eclipse.edc.issuerservice.api.admin.credentialdefinition.IssuerCredentialDefinitionAdminApiExtension.NAME; + +@Extension(value = NAME) +public class IssuerCredentialDefinitionAdminApiExtension implements ServiceExtension { + + public static final String NAME = "Issuer Service Credential Definition Admin API Extension"; + @Inject + private WebService webService; + @Inject + private CredentialDefinitionService credentialDefinitionService; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var controller = new IssuerCredentialDefinitionAdminApiController(credentialDefinitionService); + webService.registerResource(IdentityHubApiContext.ISSUERADMIN, controller); + } +} diff --git a/extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApi.java b/extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApi.java new file mode 100644 index 000000000..3b3b98a33 --- /dev/null +++ b/extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApi.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.api.admin.credentialdefinition.v1.unstable; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.web.spi.ApiErrorDetail; + +import java.util.Collection; + +@OpenAPIDefinition(info = @Info(description = "This API is used to manipulate credential definitions in an Issuer Service", title = "Issuer Service Credential Definitions Admin API", version = "1")) +@Tag(name = "Issuer Service Credential Definition Admin API") +public interface IssuerCredentialDefinitionAdminApi { + + + @Operation(description = "Adds a new credential definition.", + operationId = "createCredentialDefinition", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = CredentialDefinition.class), mediaType = "application/json")), + responses = { + @ApiResponse(responseCode = "201", description = "The credential definition was created successfully."), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "409", description = "Can't create the credential definition, because a object with the same ID already exists", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + Response createCredentialDefinition(CredentialDefinition credentialDefinition); + + @Operation(description = "Updates credential definition.", + operationId = "updateCredentialDefinition", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = CredentialDefinition.class), mediaType = "application/json")), + responses = { + @ApiResponse(responseCode = "200", description = "The credential definition was updated successfully."), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "Can't update the credential definition because it was not found.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + Response updateCredentialDefinition(CredentialDefinition credentialDefinition); + + @Operation(description = "Gets a credential definition by its ID.", + operationId = "getParticipantById", + parameters = { @Parameter(name = "credentialDefinitionId", description = "ID of the credential definition that should be returned", required = true, in = ParameterIn.PATH) }, + responses = { + @ApiResponse(responseCode = "200", description = "The credential definition was found.", + content = @Content(schema = @Schema(implementation = CredentialDefinition.class), mediaType = "application/json")), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "The credential definition was not found.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + CredentialDefinition getCredentialDefinitionById(String credentialDefinitionId); + + @Operation(description = "Gets all credential definitions for a certain query.", + operationId = "queryCredentialDefinitions", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = QuerySpec.class), mediaType = "application/json")), + responses = { + @ApiResponse(responseCode = "200", description = "A list of credentials definitions.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = CredentialDefinition.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + } + ) + Collection queryCredentialDefinitions(QuerySpec querySpec); + + + @Operation(description = "Deletes a credential definition by its ID.", + operationId = "deleteCredentialDefinitionById", + parameters = { @Parameter(name = "credentialDefinitionId", description = "ID of the credential definition that should be returned", required = true, in = ParameterIn.PATH) }, + responses = { + @ApiResponse(responseCode = "200", description = "The credential definition was deleted.", + content = @Content(schema = @Schema(implementation = CredentialDefinition.class), mediaType = "application/json")), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "The credential definition was not found.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + }) + void deleteCredentialDefinitionById(String credentialDefinitionId); + +} diff --git a/extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApiController.java b/extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApiController.java new file mode 100644 index 000000000..c10c60465 --- /dev/null +++ b/extensions/api/issuer-admin-api/credential-definition-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApiController.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.api.admin.credentialdefinition.v1.unstable; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.identityhub.api.Versions; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.spi.query.QuerySpec; + +import java.net.URI; +import java.util.Collection; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; + +@Consumes(APPLICATION_JSON) +@Produces(APPLICATION_JSON) +@Path(Versions.UNSTABLE + "/credentialdefinitions") +public class IssuerCredentialDefinitionAdminApiController implements IssuerCredentialDefinitionAdminApi { + + private final CredentialDefinitionService credentialDefinitionService; + + public IssuerCredentialDefinitionAdminApiController(CredentialDefinitionService credentialDefinitionService) { + this.credentialDefinitionService = credentialDefinitionService; + } + + @POST + @Override + public Response createCredentialDefinition(CredentialDefinition credentialDefinition) { + return credentialDefinitionService.createCredentialDefinition(credentialDefinition) + .map(v -> Response.created(URI.create(Versions.UNSTABLE + "/credentialdefinitions/" + credentialDefinition.getId())).build()) + .orElseThrow(exceptionMapper(CredentialDefinition.class, credentialDefinition.getId())); + } + + @PUT + @Override + public Response updateCredentialDefinition(CredentialDefinition credentialDefinition) { + return credentialDefinitionService.updateCredentialDefinition(credentialDefinition) + .map(v -> Response.ok().build()) + .orElseThrow(exceptionMapper(CredentialDefinition.class, credentialDefinition.getId())); + } + + @GET + @Path("/{credentialDefinitionId}") + @Override + public CredentialDefinition getCredentialDefinitionById(@PathParam("credentialDefinitionId") String credentialDefinitionId) { + return credentialDefinitionService.findById(credentialDefinitionId) + .orElseThrow(exceptionMapper(CredentialDefinition.class, credentialDefinitionId)); + } + + @POST + @Path("/query") + @Override + public Collection queryCredentialDefinitions(QuerySpec querySpec) { + return credentialDefinitionService.queryCredentialDefinitions(querySpec) + .orElseThrow(exceptionMapper(CredentialDefinition.class, null)); + } + + @DELETE + @Path("/{credentialDefinitionId}") + @Override + public void deleteCredentialDefinitionById(@PathParam("credentialDefinitionId") String credentialDefinitionId) { + credentialDefinitionService.deleteCredentialDefinition(credentialDefinitionId) + .orElseThrow(exceptionMapper(CredentialDefinition.class, credentialDefinitionId)); + } +} diff --git a/extensions/api/issuer-admin-api/credential-definition-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/api/issuer-admin-api/credential-definition-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..38547426a --- /dev/null +++ b/extensions/api/issuer-admin-api/credential-definition-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2025 Cofinity-X +# +# 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: +# Cofinity-X - initial API and implementation +# +# + +org.eclipse.edc.issuerservice.api.admin.credentialdefinition.IssuerCredentialDefinitionAdminApiExtension \ No newline at end of file diff --git a/extensions/api/issuer-admin-api/credential-definition-api/src/test/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApiControllerTest.java b/extensions/api/issuer-admin-api/credential-definition-api/src/test/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApiControllerTest.java new file mode 100644 index 000000000..6d598da20 --- /dev/null +++ b/extensions/api/issuer-admin-api/credential-definition-api/src/test/java/org/eclipse/edc/issuerservice/api/admin/credentialdefinition/v1/unstable/IssuerCredentialDefinitionAdminApiControllerTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.api.admin.credentialdefinition.v1.unstable; + +import io.restassured.specification.RequestSpecification; +import org.eclipse.edc.identityhub.api.Versions; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.junit.annotations.ComponentTest; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ComponentTest +class IssuerCredentialDefinitionAdminApiControllerTest extends RestControllerTestBase { + + private final CredentialDefinitionService credentialDefinitionService = mock(); + + @Test + void createCredentialDefinition() { + when(credentialDefinitionService.createCredentialDefinition(any())).thenReturn(ServiceResult.success()); + + baseRequest() + .body(credentialDefinition()) + .post() + .then() + .log().ifValidationFails() + .statusCode(201) + .header("Location", endsWith("/credentialdefinitions/test-id")) + .body(emptyString()); + } + + @Test + void createCredentialDefinition_whenAlreadyExists() { + when(credentialDefinitionService.createCredentialDefinition(any())).thenReturn(ServiceResult.conflict("already exists")); + + baseRequest() + .body(credentialDefinition()) + .post() + .then() + .log().ifValidationFails() + .statusCode(409) + .body(notNullValue()); + } + + @Test + void updateCredentialDefinition() { + when(credentialDefinitionService.updateCredentialDefinition(any())).thenReturn(ServiceResult.success()); + + baseRequest() + .body(credentialDefinition()) + .put() + .then() + .log().ifValidationFails() + .statusCode(200) + .body(emptyString()); + } + + @Test + void updateCredentialDefinition_notFound() { + when(credentialDefinitionService.updateCredentialDefinition(any())).thenReturn(ServiceResult.notFound("not found")); + + baseRequest() + .body(credentialDefinition()) + .put() + .then() + .log().ifValidationFails() + .statusCode(404) + .body(notNullValue()); + } + + @Test + void getCredentialDefinitionById() { + var test = credentialDefinition(); + when(credentialDefinitionService.findById(any())).thenReturn(ServiceResult.success(test)); + + var response = baseRequest() + .get("/test-id") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(CredentialDefinition.class); + + assertThat(response).usingRecursiveComparison().isEqualTo(test); + } + + @Test + void getCredentialDefinitionById_notFound() { + when(credentialDefinitionService.findById(any())).thenReturn(ServiceResult.notFound("not found")); + + baseRequest() + .get("/test-id") + .then() + .log().ifValidationFails() + .statusCode(404); + } + + @Test + void queryCredentialDefinitions() { + var test = credentialDefinition(); + when(credentialDefinitionService.queryCredentialDefinitions(any())).thenReturn(ServiceResult.success(Set.of(test))); + + var dto = baseRequest() + .body(QuerySpec.Builder.newInstance().build()) + .post("/query") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(CredentialDefinition[].class); + + assertThat(dto).hasSize(1) + .usingRecursiveComparison().isEqualTo(test); + } + + + @Test + void queryCredentialDefinitions_noneFound() { + when(credentialDefinitionService.queryCredentialDefinitions(any())).thenReturn(ServiceResult.success(Set.of())); + + var dto = baseRequest() + .body(QuerySpec.Builder.newInstance().build()) + .post("/query") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(CredentialDefinition[].class); + + assertThat(dto).isEmpty(); + } + + @Override + protected Object controller() { + return new IssuerCredentialDefinitionAdminApiController(credentialDefinitionService); + } + + private RequestSpecification baseRequest() { + return given() + .contentType("application/json") + .baseUri("http://localhost:" + port + Versions.UNSTABLE + "/credentialdefinitions") + .when(); + } + + + private CredentialDefinition credentialDefinition() { + return CredentialDefinition.Builder.newInstance() + .id("test-id") + .credentialType("Membership") + .schema("json-schema") + .build(); + } +} \ No newline at end of file diff --git a/extensions/api/issuer-admin-api/issuer-admin-api-configuration/src/main/resources/issuer-admin-api-version.json b/extensions/api/issuer-admin-api/issuer-admin-api-configuration/src/main/resources/issuer-admin-api-version.json index 4960433c7..25330b821 100644 --- a/extensions/api/issuer-admin-api/issuer-admin-api-configuration/src/main/resources/issuer-admin-api-version.json +++ b/extensions/api/issuer-admin-api/issuer-admin-api-configuration/src/main/resources/issuer-admin-api-version.json @@ -2,7 +2,7 @@ { "version": "1.0.0-alpha", "urlPath": "/v1alpha", - "lastUpdated": "2025-02-06T10:00:00Z", + "lastUpdated": "2025-02-07T10:00:00Z", "maturity": null } ] diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/resources/issuer-api-version.json b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/resources/issuer-api-version.json index 29e73c830..30622a4ff 100644 --- a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/resources/issuer-api-version.json +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/resources/issuer-api-version.json @@ -2,7 +2,7 @@ { "version": "1.0.0", "urlPath": "/v1alpha", - "lastUpdated": "2025-02-05T12:00:00Z", + "lastUpdated": "2025-02-07T08:00:00Z", "maturity": null } ] \ No newline at end of file diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/build.gradle.kts b/extensions/store/sql/issuerservice-credential-definition-store-sql/build.gradle.kts new file mode 100644 index 000000000..daa9abef0 --- /dev/null +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + api(project(":spi:issuerservice:issuerservice-credential-definition-spi")) + implementation(libs.edc.lib.sql) + implementation(libs.edc.sql.bootstrapper) + implementation(libs.edc.spi.transaction.datasource) + + testImplementation(testFixtures(project(":spi:issuerservice:issuerservice-credential-definition-spi"))) + testImplementation(testFixtures(libs.edc.sql.test.fixtures)) + testImplementation(libs.edc.junit) +} diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/BaseSqlDialectStatements.java b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/BaseSqlDialectStatements.java new file mode 100644 index 000000000..40f5d9418 --- /dev/null +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/BaseSqlDialectStatements.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.store.sql.credentialdefinition; + +import org.eclipse.edc.issuerservice.store.sql.credentialdefinition.schema.postgres.CredentialDefinitionMapping; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.translation.PostgresqlOperatorTranslator; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +import static java.lang.String.format; + +public class BaseSqlDialectStatements implements CredentialDefinitionStoreStatements { + @Override + public String getInsertTemplate() { + return executeStatement() + .column(getIdColumn()) + .column(getCredentialTypeColumn()) + .jsonColumn(getAttestationsColumn()) + .jsonColumn(getRulesColumn()) + .jsonColumn(getMappingsColumn()) + .column(getSchemaColumn()) + .column(getValidityColumn()) + .column(getDataModelColumn()) + .column(getCreateTimestampColumn()) + .column(getLastModifiedTimestampColumn()) + .insertInto(getCredentialDefinitionTable()); + } + + @Override + public String getUpdateTemplate() { + return executeStatement() + .column(getCredentialTypeColumn()) + .jsonColumn(getAttestationsColumn()) + .jsonColumn(getRulesColumn()) + .jsonColumn(getMappingsColumn()) + .column(getSchemaColumn()) + .column(getValidityColumn()) + .column(getDataModelColumn()) + .column(getLastModifiedTimestampColumn()) + .update(getCredentialDefinitionTable(), getIdColumn()); + } + + @Override + public String getDeleteByIdTemplate() { + return executeStatement().delete(getCredentialDefinitionTable(), getIdColumn()); + } + + @Override + public String getFindByIdTemplate() { + return format("SELECT * FROM %s WHERE %s = ?", getCredentialDefinitionTable(), getIdColumn()); + + } + + @Override + public String getFindCredentialTypeTemplate() { + return format("SELECT * FROM %s WHERE %s = ?", getCredentialDefinitionTable(), getCredentialTypeColumn()); + + } + + @Override + public SqlQueryStatement createQuery(QuerySpec querySpec) { + var select = getSelectStatement(); + return new SqlQueryStatement(select, querySpec, new CredentialDefinitionMapping(this), new PostgresqlOperatorTranslator()); + } + + @Override + public String getSelectStatement() { + return format("SELECT * FROM %s", getCredentialDefinitionTable()); + } +} diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/CredentialDefinitionStoreStatements.java b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/CredentialDefinitionStoreStatements.java new file mode 100644 index 000000000..3284ea9f3 --- /dev/null +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/CredentialDefinitionStoreStatements.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.store.sql.credentialdefinition; + +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.statement.SqlStatements; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +/** + * Defines SQL-statements and column names for use with a SQL-based {@link CredentialDefinition} + */ +public interface CredentialDefinitionStoreStatements extends SqlStatements { + + default String getCredentialDefinitionTable() { + return "credential_definitions"; + } + + default String getIdColumn() { + return "id"; + } + + default String getCredentialTypeColumn() { + return "credential_type"; + } + + default String getAttestationsColumn() { + return "attestations"; + } + + default String getRulesColumn() { + return "rules"; + } + + default String getMappingsColumn() { + return "mappings"; + } + + default String getSchemaColumn() { + return "json_schema"; + } + + default String getValidityColumn() { + return "validity"; + } + + default String getDataModelColumn() { + return "data_model"; + } + + default String getCreateTimestampColumn() { + return "created_date"; + } + + default String getLastModifiedTimestampColumn() { + return "last_modified_date"; + } + + + String getInsertTemplate(); + + String getUpdateTemplate(); + + String getDeleteByIdTemplate(); + + String getFindByIdTemplate(); + + String getFindCredentialTypeTemplate(); + + SqlQueryStatement createQuery(QuerySpec query); + + String getSelectStatement(); +} diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStore.java b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStore.java new file mode 100644 index 000000000..e6e961bdb --- /dev/null +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStore.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.store.sql.credentialdefinition; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.DataModelVersion; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialRuleDefinition; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.MappingDefinition; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.store.CredentialDefinitionStore; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.sql.store.AbstractSqlStore; +import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Clock; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static java.util.Optional.ofNullable; +import static org.eclipse.edc.spi.result.StoreResult.alreadyExists; +import static org.eclipse.edc.spi.result.StoreResult.success; + + +/** + * SQL-based {@link CredentialDefinition} store intended for use with PostgreSQL + */ +public class SqlCredentialDefinitionStore extends AbstractSqlStore implements CredentialDefinitionStore { + + private static final TypeReference> ATTESTATIONS_LIST_REF = new TypeReference<>() { + }; + + private static final TypeReference> RULES_LIST_REF = new TypeReference<>() { + }; + + private static final TypeReference> MAPPINGS_LIST_REF = new TypeReference<>() { + }; + + private final CredentialDefinitionStoreStatements statements; + private final Clock clock; + + public SqlCredentialDefinitionStore(DataSourceRegistry dataSourceRegistry, + String dataSourceName, + TransactionContext transactionContext, + ObjectMapper objectMapper, + QueryExecutor queryExecutor, + CredentialDefinitionStoreStatements statements, + Clock clock) { + super(dataSourceRegistry, dataSourceName, transactionContext, objectMapper, queryExecutor); + this.statements = statements; + this.clock = clock; + } + + @Override + public StoreResult findById(String id) { + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + return ofNullable(findByIdInternal(connection, id)) + .map(StoreResult::success) + .orElseGet(() -> StoreResult.notFound(notFoundErrorMessage(id))); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult create(CredentialDefinition credentialDefinition) { + var id = credentialDefinition.getId(); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findByIdInternal(connection, id) != null) { + return alreadyExists(alreadyExistsErrorMessage(id)); + } + + if (findByCredentialType(connection, credentialDefinition.getCredentialType()) != null) { + return alreadyExists(alreadyExistsForTypeErrorMessage(credentialDefinition.getCredentialType())); + } + + var stmt = statements.getInsertTemplate(); + var timestamp = clock.millis(); + queryExecutor.execute(connection, stmt, + credentialDefinition.getId(), + credentialDefinition.getCredentialType(), + toJson(credentialDefinition.getAttestations()), + toJson(credentialDefinition.getRules()), + toJson(credentialDefinition.getMappings()), + credentialDefinition.getSchema(), + credentialDefinition.getValidity(), + credentialDefinition.getDataModel().name(), + timestamp, + timestamp + ); + return success(); + + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult update(CredentialDefinition credentialDefinition) { + var id = credentialDefinition.getId(); + + Objects.requireNonNull(credentialDefinition); + Objects.requireNonNull(id); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findByIdInternal(connection, id) != null) { + + + var definitionByCredentialType = findByCredentialType(connection, credentialDefinition.getCredentialType()); + if (definitionByCredentialType != null && !definitionByCredentialType.getId().equals(id)) { + return alreadyExists(alreadyExistsForTypeErrorMessage(credentialDefinition.getCredentialType())); + } + queryExecutor.execute(connection, + statements.getUpdateTemplate(), + credentialDefinition.getCredentialType(), + toJson(credentialDefinition.getAttestations()), + toJson(credentialDefinition.getRules()), + toJson(credentialDefinition.getMappings()), + credentialDefinition.getSchema(), + credentialDefinition.getValidity(), + credentialDefinition.getDataModel().name(), + clock.millis(), + credentialDefinition.getId() + ); + return StoreResult.success(); + } + return StoreResult.notFound(notFoundErrorMessage(id)); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult> query(QuerySpec querySpec) { + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + var query = statements.createQuery(querySpec); + return success(queryExecutor.query(connection, true, this::mapResultSet, query.getQueryAsString(), query.getParameters()).toList()); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult deleteById(String id) { + Objects.requireNonNull(id); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findByIdInternal(connection, id) != null) { + var stmt = statements.getDeleteByIdTemplate(); + queryExecutor.execute(connection, stmt, id); + return success(); + } + return StoreResult.notFound(notFoundErrorMessage(id)); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + private CredentialDefinition findByIdInternal(Connection connection, String id) { + return transactionContext.execute(() -> { + var stmt = statements.getFindByIdTemplate(); + return queryExecutor.single(connection, false, this::mapResultSet, stmt, id); + }); + } + + private CredentialDefinition findByCredentialType(Connection connection, String credentialType) { + return transactionContext.execute(() -> { + var stmt = statements.getFindCredentialTypeTemplate(); + return queryExecutor.single(connection, false, this::mapResultSet, stmt, credentialType); + }); + } + + private CredentialDefinition mapResultSet(ResultSet resultSet) throws Exception { + + return CredentialDefinition.Builder.newInstance() + .id(resultSet.getString(statements.getIdColumn())) + .credentialType(resultSet.getString(statements.getCredentialTypeColumn())) + .attestations(fromJson(resultSet.getString(statements.getAttestationsColumn()), ATTESTATIONS_LIST_REF)) + .rules(fromJson(resultSet.getString(statements.getRulesColumn()), RULES_LIST_REF)) + .mappings(fromJson(resultSet.getString(statements.getMappingsColumn()), MAPPINGS_LIST_REF)) + .schema(resultSet.getString(statements.getSchemaColumn())) + .validity(resultSet.getLong(statements.getValidityColumn())) + .dataModel(DataModelVersion.valueOf(resultSet.getString(statements.getDataModelColumn()))) + .build(); + } +} diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStoreExtension.java b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStoreExtension.java new file mode 100644 index 000000000..5bdfbaba5 --- /dev/null +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStoreExtension.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.store.sql.credentialdefinition; + +import org.eclipse.edc.issuerservice.spi.credentialdefinition.store.CredentialDefinitionStore; +import org.eclipse.edc.issuerservice.store.sql.credentialdefinition.schema.postgres.PostgresDialectStatements; +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.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.sql.bootstrapper.SqlSchemaBootstrapper; +import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import java.time.Clock; + +import static org.eclipse.edc.issuerservice.store.sql.credentialdefinition.SqlCredentialDefinitionStoreExtension.NAME; + +@Extension(value = NAME) +public class SqlCredentialDefinitionStoreExtension implements ServiceExtension { + public static final String NAME = "IssuerService Credential definition SQL Store Extension"; + + @Setting(description = "The datasource to be used", defaultValue = DataSourceRegistry.DEFAULT_DATASOURCE, key = "edc.sql.store.credentialdefinitions.datasource") + private String dataSourceName; + + @Inject + private DataSourceRegistry dataSourceRegistry; + @Inject + private TransactionContext transactionContext; + @Inject + private TypeManager typemanager; + @Inject + private QueryExecutor queryExecutor; + @Inject(required = false) + private CredentialDefinitionStoreStatements statements; + @Inject + private SqlSchemaBootstrapper sqlSchemaBootstrapper; + + @Inject + private Clock clock; + + @Override + public void initialize(ServiceExtensionContext context) { + sqlSchemaBootstrapper.addStatementFromResource(dataSourceName, "credential-definition-schema.sql"); + } + + @Provider + public CredentialDefinitionStore createSqlStore() { + return new SqlCredentialDefinitionStore(dataSourceRegistry, dataSourceName, transactionContext, typemanager.getMapper(), + queryExecutor, getStatementImpl(), clock); + } + + private CredentialDefinitionStoreStatements getStatementImpl() { + return statements != null ? statements : new PostgresDialectStatements(); + } + +} diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/schema/postgres/CredentialDefinitionMapping.java b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/schema/postgres/CredentialDefinitionMapping.java new file mode 100644 index 000000000..20e7be4ed --- /dev/null +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/schema/postgres/CredentialDefinitionMapping.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.store.sql.credentialdefinition.schema.postgres; + +import org.eclipse.edc.issuerservice.store.sql.credentialdefinition.CredentialDefinitionStoreStatements; +import org.eclipse.edc.sql.translation.TranslationMapping; + + +/** + * Provides a mapping from the canonical format to SQL column names for a {@code VerifiableCredentialResource} + */ +public class CredentialDefinitionMapping extends TranslationMapping { + + public static final String FIELD_ID = "id"; + public static final String FIELD_CREDENTIAL_TYPE = "credentialType"; + public static final String FIELD_CREATE_TIMESTAMP = "createdAt"; + public static final String FIELD_LASTMODIFIED_TIMESTAMP = "lastModified"; + + + public CredentialDefinitionMapping(CredentialDefinitionStoreStatements statements) { + add(FIELD_ID, statements.getIdColumn()); + add(FIELD_CREDENTIAL_TYPE, statements.getCredentialTypeColumn()); + add(FIELD_CREATE_TIMESTAMP, statements.getCreateTimestampColumn()); + add(FIELD_LASTMODIFIED_TIMESTAMP, statements.getLastModifiedTimestampColumn()); + } +} \ No newline at end of file diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/schema/postgres/PostgresDialectStatements.java b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/schema/postgres/PostgresDialectStatements.java new file mode 100644 index 000000000..48d78ef59 --- /dev/null +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/schema/postgres/PostgresDialectStatements.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.store.sql.credentialdefinition.schema.postgres; + +import org.eclipse.edc.issuerservice.store.sql.credentialdefinition.BaseSqlDialectStatements; +import org.eclipse.edc.sql.dialect.PostgresDialect; + +/** + * Postgres-specific specialization for creating queries based on Postgres JSON operators + */ +public class PostgresDialectStatements extends BaseSqlDialectStatements { + + @Override + public String getFormatAsJsonOperator() { + return PostgresDialect.getJsonCastOperator(); + } +} diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..bfa649b4e --- /dev/null +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2025 Cofinity-X +# +# 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: +# Cofinity-X - initial API and implementation +# +# + +org.eclipse.edc.issuerservice.store.sql.credentialdefinition.SqlCredentialDefinitionStoreExtension \ No newline at end of file diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/resources/credential-definition-schema.sql b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/resources/credential-definition-schema.sql new file mode 100644 index 000000000..3f9b1756a --- /dev/null +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/resources/credential-definition-schema.sql @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +-- only intended for and tested with Postgres! +CREATE TABLE IF NOT EXISTS credential_definitions +( + id VARCHAR NOT NULL , + credential_type VARCHAR NOT NULL UNIQUE , + attestations JSON NOT NULL , + rules JSON NOT NULL , + mappings JSON NOT NULL , + json_schema VARCHAR NOT NULL , + validity BIGINT NOT NULL , + data_model VARCHAR NOT NULL , + created_date BIGINT NOT NULL , -- POSIX timestamp of the creation of the PC + last_modified_date BIGINT , -- POSIX timestamp of the last modified date + PRIMARY KEY (id) +); + + diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/test/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStoreTest.java b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/test/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStoreTest.java new file mode 100644 index 000000000..19b892118 --- /dev/null +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/test/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStoreTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.store.sql.credentialdefinition; + +import org.eclipse.edc.issuerservice.spi.credentialdefinition.store.CredentialDefinitionStore; +import org.eclipse.edc.issuerservice.spi.credentialdefinition.store.CredentialDefinitionStoreTestBase; +import org.eclipse.edc.issuerservice.store.sql.credentialdefinition.schema.postgres.PostgresDialectStatements; +import org.eclipse.edc.json.JacksonTypeManager; +import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; +import org.eclipse.edc.junit.testfixtures.TestUtils; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.sql.testfixtures.PostgresqlStoreSetupExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Clock; + +@PostgresqlIntegrationTest +@ExtendWith(PostgresqlStoreSetupExtension.class) +class SqlCredentialDefinitionStoreTest extends CredentialDefinitionStoreTestBase { + + private final CredentialDefinitionStoreStatements statements = new PostgresDialectStatements(); + private SqlCredentialDefinitionStore store; + + @BeforeEach + void setup(PostgresqlStoreSetupExtension extension, QueryExecutor queryExecutor) { + var typeManager = new JacksonTypeManager(); + store = new SqlCredentialDefinitionStore(extension.getDataSourceRegistry(), extension.getDatasourceName(), + extension.getTransactionContext(), typeManager.getMapper(), queryExecutor, statements, Clock.systemUTC()); + + var schema = TestUtils.getResourceFileContentAsString("credential-definition-schema.sql"); + extension.runQuery(schema); + } + + @AfterEach + void tearDown(PostgresqlStoreSetupExtension extension) { + extension.runQuery("DROP TABLE " + statements.getCredentialDefinitionTable() + " CASCADE"); + } + + @Override + protected CredentialDefinitionStore getStore() { + return store; + } +} \ No newline at end of file diff --git a/resources/openapi/issuer-api.version b/resources/openapi/issuer-api.version index d1fae9b90..74243bcb1 100644 --- a/resources/openapi/issuer-api.version +++ b/resources/openapi/issuer-api.version @@ -1 +1 @@ -extensions/protocols/dcp/issuer-api/src/main/resources/issuer-api-version.json \ No newline at end of file +extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/resources/issuer-api-version.json \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 782642935..134160501 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,7 @@ include(":spi:issuance-credentials-spi") // IssuerService SPI modules include(":spi:issuerservice:issuerservice-participant-spi") include(":spi:issuerservice:credential-revocation-spi") +include(":spi:issuerservice:issuerservice-credential-definition-spi") // IdentityHub core modules include(":core:identity-hub-core") @@ -44,6 +45,7 @@ include(":core:identity-hub-did") include(":core:issuerservice:issuerservice-core") include(":core:issuerservice:issuerservice-participants") include(":core:issuerservice:issuerservice-credential-revocation") +include(":core:issuerservice:issuerservice-credential-definitions") // lib modules include(":core:lib:keypair-lib") @@ -56,6 +58,7 @@ include(":extensions:store:sql:identity-hub-credentials-store-sql") include(":extensions:store:sql:identity-hub-participantcontext-store-sql") include(":extensions:store:sql:identity-hub-keypair-store-sql") include(":extensions:store:sql:issuerservice-participant-store-sql") +include(":extensions:store:sql:issuerservice-credential-definition-store-sql") include(":extensions:did:local-did-publisher") include(":extensions:common:credential-watchdog") include(":extensions:sts:sts-account-provisioner") @@ -85,6 +88,7 @@ include(":extensions:api:issuer-admin-api:issuer-admin-api-configuration") include(":extensions:api:issuer-admin-api:participant-api") include(":extensions:api:issuer-admin-api:credentials-api") include(":extensions:api:issuer-admin-api:attestation-api") +include(":extensions:api:issuer-admin-api:credential-definition-api") // Identity API validators include(":extensions:api:identity-api:validators:keypair-validators") diff --git a/spi/issuance-credentials-spi/src/main/java/org/eclipse/edc/identityhub/spi/issuance/credentials/attestation/AttestationDefinitionStore.java b/spi/issuance-credentials-spi/src/main/java/org/eclipse/edc/identityhub/spi/issuance/credentials/attestation/AttestationDefinitionStore.java index bc5576ebb..3c7157cbc 100644 --- a/spi/issuance-credentials-spi/src/main/java/org/eclipse/edc/identityhub/spi/issuance/credentials/attestation/AttestationDefinitionStore.java +++ b/spi/issuance-credentials-spi/src/main/java/org/eclipse/edc/identityhub/spi/issuance/credentials/attestation/AttestationDefinitionStore.java @@ -15,9 +15,12 @@ package org.eclipse.edc.identityhub.spi.issuance.credentials.attestation; import org.eclipse.edc.identityhub.spi.issuance.credentials.model.AttestationDefinition; +import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.StoreResult; import org.jetbrains.annotations.Nullable; +import java.util.Collection; + /** * Persists attestation definitions. */ @@ -44,4 +47,12 @@ public interface AttestationDefinitionStore { */ StoreResult delete(String id); + /** + * Queries for attestation definitions + * + * @param querySpec the query to use. + * @return A (potentially empty) list of attestation definitions. + */ + StoreResult> query(QuerySpec querySpec); + } diff --git a/spi/issuance-credentials-spi/src/main/java/org/eclipse/edc/identityhub/spi/issuance/credentials/model/CredentialDefinition.java b/spi/issuance-credentials-spi/src/main/java/org/eclipse/edc/identityhub/spi/issuance/credentials/model/CredentialDefinition.java index 35d27cf8c..5f682beb7 100644 --- a/spi/issuance-credentials-spi/src/main/java/org/eclipse/edc/identityhub/spi/issuance/credentials/model/CredentialDefinition.java +++ b/spi/issuance-credentials-spi/src/main/java/org/eclipse/edc/identityhub/spi/issuance/credentials/model/CredentialDefinition.java @@ -14,17 +14,24 @@ package org.eclipse.edc.identityhub.spi.issuance.credentials.model; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import org.eclipse.edc.iam.verifiablecredentials.spi.model.DataModelVersion; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.UUID; import static java.util.Objects.requireNonNull; /** * Defines credential type that can be issued, its schema, and requirements for issuance. */ + +@JsonDeserialize(builder = CredentialDefinition.Builder.class) public class CredentialDefinition { private final List attestations = new ArrayList<>(); @@ -34,10 +41,15 @@ public class CredentialDefinition { private String schema; private long validity; private DataModelVersion dataModel = DataModelVersion.V_1_1; + private String id; private CredentialDefinition() { } + public String getId() { + return id; + } + public String getCredentialType() { return credentialType; } @@ -66,6 +78,8 @@ public List getMappings() { return mappings; } + + @JsonPOJOBuilder(withPrefix = "") public static final class Builder { private final CredentialDefinition definition; @@ -73,10 +87,16 @@ private Builder() { definition = new CredentialDefinition(); } + @JsonCreator public static Builder newInstance() { return new Builder(); } + public Builder id(String id) { + this.definition.id = id; + return this; + } + public Builder credentialType(String credentialType) { this.definition.credentialType = credentialType; return this; @@ -102,6 +122,7 @@ public Builder attestations(Collection attestations) { return this; } + @JsonIgnore public Builder attestation(String attestation) { this.definition.attestations.add(attestation); return this; @@ -112,6 +133,7 @@ public Builder rules(Collection rules) { return this; } + @JsonIgnore public Builder rule(CredentialRuleDefinition rule) { this.definition.rules.add(rule); return this; @@ -122,12 +144,16 @@ public Builder mappings(Collection rules) { return this; } - public Builder mappings(MappingDefinition mapping) { + @JsonIgnore + public Builder mapping(MappingDefinition mapping) { this.definition.mappings.add(mapping); return this; } public CredentialDefinition build() { + if (definition.id == null) { + definition.id = UUID.randomUUID().toString(); + } requireNonNull(definition.credentialType, "credentialType"); requireNonNull(definition.schema, "schema"); return definition; diff --git a/spi/issuerservice/issuerservice-credential-definition-spi/build.gradle.kts b/spi/issuerservice/issuerservice-credential-definition-spi/build.gradle.kts new file mode 100644 index 000000000..20c06ee32 --- /dev/null +++ b/spi/issuerservice/issuerservice-credential-definition-spi/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +plugins { + `java-library` + `java-test-fixtures` + `maven-publish` +} + +dependencies { + api(libs.edc.spi.core) + api(project(":spi:issuance-credentials-spi")) + testFixturesImplementation(libs.edc.junit) + testFixturesImplementation(libs.assertj) + testFixturesImplementation(libs.junit.jupiter.api) +} diff --git a/spi/issuerservice/issuerservice-credential-definition-spi/src/main/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/CredentialDefinitionService.java b/spi/issuerservice/issuerservice-credential-definition-spi/src/main/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/CredentialDefinitionService.java new file mode 100644 index 000000000..a7409619f --- /dev/null +++ b/spi/issuerservice/issuerservice-credential-definition-spi/src/main/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/CredentialDefinitionService.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.credentialdefinition; + +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; + +import java.util.Collection; + +public interface CredentialDefinitionService { + + ServiceResult createCredentialDefinition(CredentialDefinition credentialDefinition); + + ServiceResult deleteCredentialDefinition(String credentialDefinitionId); + + ServiceResult updateCredentialDefinition(CredentialDefinition credentialDefinition); + + ServiceResult> queryCredentialDefinitions(QuerySpec querySpec); + + ServiceResult findById(String credentialDefinitionId); +} diff --git a/spi/issuerservice/issuerservice-credential-definition-spi/src/main/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/store/CredentialDefinitionStore.java b/spi/issuerservice/issuerservice-credential-definition-spi/src/main/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/store/CredentialDefinitionStore.java new file mode 100644 index 000000000..ebcef48a1 --- /dev/null +++ b/spi/issuerservice/issuerservice-credential-definition-spi/src/main/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/store/CredentialDefinitionStore.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.credentialdefinition.store; + +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; + +import java.util.Collection; + +/** + * Stores {@link CredentialDefinition} objects and provides basic CRUD operations + */ +public interface CredentialDefinitionStore { + + /** + * Find a {@link CredentialDefinition} by its ID + * + * @param credentialDefinitionId The credential definition ID + */ + StoreResult findById(String credentialDefinitionId); + + /** + * Stores the credential definition in the database + * + * @param credentialDefinition the {@link CredentialDefinition} + * @return success if stored, a failure if a Credential Definition with the same ID already exists + */ + StoreResult create(CredentialDefinition credentialDefinition); + + /** + * Updates the credential definition with the given data. Existing data will be overwritten with the given object. + * + * @param credentialDefinition a (fully populated) {@link CredentialDefinition} + * @return success if updated, a failure if not exist + */ + StoreResult update(CredentialDefinition credentialDefinition); + + /** + * Queries for credential definitions + * + * @param querySpec the query to use. + * @return A (potentially empty) list of credential definitions. + */ + StoreResult> query(QuerySpec querySpec); + + /** + * Deletes a credential definition with the given ID + * + * @param credentialDefinitionId the credential definition ID + * @return success if deleted, a failure otherwise + */ + StoreResult deleteById(String credentialDefinitionId); + + default String alreadyExistsErrorMessage(String id) { + return "A Credential definition with ID '%s' already exists.".formatted(id); + } + + default String alreadyExistsForTypeErrorMessage(String credentialType) { + return "A Credential definition with credential type '%s' already exists.".formatted(credentialType); + } + + default String notFoundErrorMessage(String id) { + return "A Credential definition ID '%s' does not exist.".formatted(id); + } +} diff --git a/spi/issuerservice/issuerservice-credential-definition-spi/src/testFixtures/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/store/CredentialDefinitionStoreTestBase.java b/spi/issuerservice/issuerservice-credential-definition-spi/src/testFixtures/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/store/CredentialDefinitionStoreTestBase.java new file mode 100644 index 000000000..6379e4e37 --- /dev/null +++ b/spi/issuerservice/issuerservice-credential-definition-spi/src/testFixtures/java/org/eclipse/edc/issuerservice/spi/credentialdefinition/store/CredentialDefinitionStoreTestBase.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.credentialdefinition.store; + +import org.assertj.core.api.Assertions; +import org.eclipse.edc.identityhub.spi.issuance.credentials.model.CredentialDefinition; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static java.util.stream.IntStream.range; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; + +public abstract class CredentialDefinitionStoreTestBase { + + @Test + void create() { + var credentialDefinition = createCredentialDefinition(); + var result = getStore().create(credentialDefinition); + assertThat(result).isSucceeded(); + var query = getStore().query(QuerySpec.max()); + assertThat(query).isSucceeded(); + Assertions.assertThat(query.getContent()).usingRecursiveFieldByFieldElementComparator().containsExactly(credentialDefinition); + } + + @Test + void create_whenExists_shouldReturnFailure() { + var credentialDefinition = createCredentialDefinition(); + var result = getStore().create(credentialDefinition); + assertThat(result).isSucceeded(); + var result2 = getStore().create(credentialDefinition); + + assertThat(result2).isFailed().detail().contains("already exists"); + } + + @Test + void create_whenTypeExists_shouldReturnFailure() { + var credentialDefinition = createCredentialDefinition(); + + var result = getStore().create(credentialDefinition); + + var newCredentialDefinition = createCredentialDefinition(UUID.randomUUID().toString(), credentialDefinition.getCredentialType()); + assertThat(result).isSucceeded(); + var result2 = getStore().create(newCredentialDefinition); + + assertThat(result2).isFailed().detail().contains("already exists"); + } + + @Test + void query_byId() { + range(0, 5) + .mapToObj(i -> createCredentialDefinition("id" + i, "Membership" + i)) + .forEach(getStore()::create); + + var q = QuerySpec.Builder.newInstance() + .filter(new Criterion("credentialType", "=", "Membership4")) + .build(); + + assertThat(getStore().query(q)).isSucceeded() + .satisfies(str -> Assertions.assertThat(str).hasSize(1)); + } + + + @Test + void query_noQuerySpec() { + var resources = range(0, 5) + .mapToObj(i -> createCredentialDefinition("id" + i, "Membership" + i)) + .toList(); + + resources.forEach(getStore()::create); + + var res = getStore().query(QuerySpec.none()); + assertThat(res).isSucceeded(); + Assertions.assertThat(res.getContent()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(resources.toArray(new CredentialDefinition[0])); + } + + @Test + void query_whenNotFound() { + var resources = range(0, 5) + .mapToObj(i -> createCredentialDefinition("id" + i, "Membership" + i)) + .toList(); + + resources.forEach(getStore()::create); + + var query = QuerySpec.Builder.newInstance() + .filter(new Criterion("credentialType", "=", "Membership99")) + .build(); + var res = getStore().query(query); + assertThat(res).isSucceeded(); + Assertions.assertThat(res.getContent()).isEmpty(); + } + + @Test + void query_byInvalidField_shouldReturnEmptyList() { + var resources = range(0, 5) + .mapToObj(i -> createCredentialDefinition("id" + i, "Membership" + i)) + .toList(); + + + resources.forEach(getStore()::create); + + var query = QuerySpec.Builder.newInstance() + .filter(new Criterion("invalidField", "=", "test-value")) + .build(); + var res = getStore().query(query); + assertThat(res).isSucceeded(); + Assertions.assertThat(res.getContent()).isNotNull().isEmpty(); + } + + @Test + void update() { + var credentialDefinition = createCredentialDefinition(); + var result = getStore().create(credentialDefinition); + assertThat(result).isSucceeded(); + + var updated = createCredentialDefinition(credentialDefinition.getId(), credentialDefinition.getCredentialType()); + var updateRes = getStore().update(updated); + assertThat(updateRes).isSucceeded(); + } + + @Test + void update_whenNotExists() { + var credentialDefinition = createCredentialDefinition(); + + var updateRes = getStore().update(credentialDefinition); + assertThat(updateRes).isFailed().detail().contains("ID '%s' does not exist.".formatted(credentialDefinition.getId())); + } + + @Test + void update_whenTypeExists_fails() { + var credentialDefinition = createCredentialDefinition(); + var credentialDefinition1 = createCredentialDefinition(UUID.randomUUID().toString(), "Membership1"); + var result = getStore().create(credentialDefinition); + var result1 = getStore().create(credentialDefinition1); + assertThat(result).isSucceeded(); + assertThat(result1).isSucceeded(); + + credentialDefinition = createCredentialDefinition(credentialDefinition.getId(), "Membership1"); + + var updateRes = getStore().update(credentialDefinition); + assertThat(updateRes).isFailed(); + } + + @Test + void update_whenChangingType() { + var credentialDefinition = createCredentialDefinition(); + var result = getStore().create(credentialDefinition); + assertThat(result).isSucceeded(); + + credentialDefinition = createCredentialDefinition(credentialDefinition.getId(), "Membership1"); + + var updateRes = getStore().update(credentialDefinition); + assertThat(updateRes).isSucceeded(); + } + + @Test + void delete() { + var context = createCredentialDefinition(); + getStore().create(context); + + var deleteRes = getStore().deleteById(context.getId()); + assertThat(deleteRes).isSucceeded(); + } + + @Test + void delete_whenNotExists() { + assertThat(getStore().deleteById("not-exist")).isFailed() + .detail().contains("does not exist."); + } + + protected abstract CredentialDefinitionStore getStore(); + + private CredentialDefinition createCredentialDefinition() { + return createCredentialDefinition(UUID.randomUUID().toString(), "Membership"); + } + + private CredentialDefinition createCredentialDefinition(String id, String type) { + return CredentialDefinition.Builder.newInstance().id(id).schema("").credentialType(type).build(); + } + +}