From b13918617c4b8f8421322150b918bb567b114905 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Tue, 16 Jan 2024 14:32:08 +0100 Subject: [PATCH] feat: add ParticipantContextStore (#224) * feat: add ParticipantContextStore (mem + sql) * pr remarks --- .../identityhub/DefaultServicesExtension.java | 9 +- .../defaults/InMemoryCredentialStore.java | 76 +------ .../defaults/InMemoryEntityStore.java | 119 +++++++++++ .../InMemoryParticipantContextStore.java | 36 ++++ .../defaults/InMemoryCredentialStoreTest.java | 2 +- .../InMemoryParticipantContextStoreTest.java | 28 +++ .../sql/credentials/SqlCredentialStore.java | 2 +- .../credentials/SqlCredentialsStoreTest.java | 2 +- .../build.gradle.kts | 27 +++ .../docs/schema.sql | 25 +++ .../BaseSqlDialectStatements.java | 67 +++++++ .../ParticipantContextStoreStatements.java | 61 ++++++ .../SqlParticipantContextStore.java | 158 +++++++++++++++ .../SqlParticipantContextStoreExtension.java | 63 ++++++ .../postgres/ParticipantContextMapping.java | 39 ++++ .../postgres/PostgresDialectStatements.java | 29 +++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 ++ .../SqlParticipantContextStoreTest.java | 60 ++++++ settings.gradle.kts | 1 + .../model/participant/ParticipantContext.java | 124 ++++++++++++ .../participant/ParticipantContextState.java | 33 ++++ .../participant/ParticipantContextTest.java | 74 +++++++ .../spi/store/CredentialStore.java | 2 +- .../spi/store/ParticipantContextStore.java | 66 +++++++ .../store/test/CredentialStoreTestBase.java | 10 +- .../test/ParticipantContextStoreTestBase.java | 187 ++++++++++++++++++ 26 files changed, 1237 insertions(+), 78 deletions(-) create mode 100644 core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryEntityStore.java create mode 100644 core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryParticipantContextStore.java create mode 100644 core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryParticipantContextStoreTest.java create mode 100644 extensions/store/sql/identity-hub-participantcontext-store-sql/build.gradle.kts create mode 100644 extensions/store/sql/identity-hub-participantcontext-store-sql/docs/schema.sql create mode 100644 extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/BaseSqlDialectStatements.java create mode 100644 extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/ParticipantContextStoreStatements.java create mode 100644 extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/SqlParticipantContextStore.java create mode 100644 extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/SqlParticipantContextStoreExtension.java create mode 100644 extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/schema/postgres/ParticipantContextMapping.java create mode 100644 extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/schema/postgres/PostgresDialectStatements.java create mode 100644 extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/store/sql/identity-hub-participantcontext-store-sql/src/test/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlParticipantContextStoreTest.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContext.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContextState.java create mode 100644 spi/identity-hub-spi/src/test/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContextTest.java create mode 100644 spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/ParticipantContextStore.java rename spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/{credentials => }/store/test/CredentialStoreTestBase.java (98%) create mode 100644 spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/ParticipantContextStoreTestBase.java diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java index 8c3a08e6e..b9f6f57cf 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java @@ -17,9 +17,11 @@ import com.apicatalog.ld.signature.SignatureSuite; import org.eclipse.edc.identityhub.defaults.EdcScopeToCriterionTransformer; import org.eclipse.edc.identityhub.defaults.InMemoryCredentialStore; +import org.eclipse.edc.identityhub.defaults.InMemoryParticipantContextStore; import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer; import org.eclipse.edc.identityhub.spi.model.IdentityHubConstants; import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; import org.eclipse.edc.identityhub.token.rules.ClaimIsPresentRule; import org.eclipse.edc.identitytrust.verification.SignatureSuiteRegistry; import org.eclipse.edc.jsonld.util.JacksonJsonLd; @@ -64,10 +66,15 @@ public void initialize(ServiceExtensionContext context) { } @Provider(isDefault = true) - public CredentialStore createInMemStore() { + public CredentialStore createDefaultCredentialStore() { return new InMemoryCredentialStore(); } + @Provider(isDefault = true) + public ParticipantContextStore createDefaultParticipantContextStore() { + return new InMemoryParticipantContextStore(); + } + @Provider(isDefault = true) public ScopeToCriterionTransformer createScopeTransformer(ServiceExtensionContext context) { context.getMonitor().warning("Using the default EdcScopeToCriterionTransformer. This is not intended for production use and should be replaced " + diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStore.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStore.java index 08966075d..86a163d62 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStore.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStore.java @@ -18,79 +18,19 @@ import org.eclipse.edc.identityhub.spi.store.CredentialStore; import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; import org.eclipse.edc.spi.query.QueryResolver; -import org.eclipse.edc.spi.query.QuerySpec; -import org.eclipse.edc.spi.result.StoreResult; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.Predicate; -import java.util.stream.Stream; - -import static org.eclipse.edc.spi.result.StoreResult.alreadyExists; -import static org.eclipse.edc.spi.result.StoreResult.notFound; -import static org.eclipse.edc.spi.result.StoreResult.success; - -public class InMemoryCredentialStore implements CredentialStore { - private final Map store = new HashMap<>(); - private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); - private final QueryResolver queryResolver = new ReflectionBasedQueryResolver<>(VerifiableCredentialResource.class, new CriterionToCredentialResourceConverter()); - - @Override - public StoreResult create(VerifiableCredentialResource credentialResource) { - lock.writeLock().lock(); - var id = credentialResource.getId(); - try { - if (store.containsKey(id)) { - return alreadyExists("A VerifiableCredentialResource with ID %s already exists".formatted(id)); - } - store.put(id, credentialResource); - return success(null); - } finally { - lock.writeLock().unlock(); - } - } - - @Override - public StoreResult> query(QuerySpec querySpec) { - lock.readLock().lock(); - try { - // if no filter is present, we return true - Predicate fallback = querySpec.getFilterExpression().isEmpty() ? x -> true : x -> false; - var result = queryResolver.query(store.values().stream(), querySpec, Predicate::or, fallback); - return success(result); - } finally { - lock.readLock().unlock(); - } - } +/** + * In-memory variant of the {@link CredentialStore} that is thread-safe. + */ +public class InMemoryCredentialStore extends InMemoryEntityStore implements CredentialStore { @Override - public StoreResult update(VerifiableCredentialResource credentialResource) { - lock.writeLock().lock(); - try { - var id = credentialResource.getId(); - if (!store.containsKey(id)) { - return notFound("A VerifiableCredentialResource with ID %s was not found".formatted(id)); - } - store.put(id, credentialResource); - return success(); - } finally { - lock.writeLock().unlock(); - } + protected String getId(VerifiableCredentialResource newObject) { + return newObject.getId(); } @Override - public StoreResult delete(String id) { - lock.writeLock().lock(); - try { - if (!store.containsKey(id)) { - return notFound("A VerifiableCredentialResource with ID %s was not found".formatted(id)); - } - store.remove(id); - return success(); - } finally { - lock.writeLock().unlock(); - } + protected QueryResolver createQueryResolver() { + return new ReflectionBasedQueryResolver<>(VerifiableCredentialResource.class, new CriterionToCredentialResourceConverter()); } - } diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryEntityStore.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryEntityStore.java new file mode 100644 index 000000000..b94fd826b --- /dev/null +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryEntityStore.java @@ -0,0 +1,119 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.defaults; + +import org.eclipse.edc.spi.query.QueryResolver; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static org.eclipse.edc.spi.result.StoreResult.alreadyExists; +import static org.eclipse.edc.spi.result.StoreResult.notFound; +import static org.eclipse.edc.spi.result.StoreResult.success; + +/** + * Base class for in-mem entity stores, that implement basic CRUD operations. + */ +abstract class InMemoryEntityStore { + protected final Map store = new HashMap<>(); + protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + protected final QueryResolver queryResolver = createQueryResolver(); + + /** + * Creates a new entity if none exists. + * + * @param newObject the new object to insert. + * @return failure if an object with the same ID already exists. + */ + public StoreResult create(T newObject) { + lock.writeLock().lock(); + var id = getId(newObject); + try { + if (store.containsKey(id)) { + return alreadyExists("A VerifiableCredentialResource with ID %s already exists".formatted(id)); + } + store.put(id, newObject); + return success(null); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Performs a query using the given query parameters. + * + * @param querySpec A non-null QuerySpec. + * @return A (potentially empty) Stream of objects. Callers must close the stream. + */ + public StoreResult> query(QuerySpec querySpec) { + lock.readLock().lock(); + try { + // if no filter is present, we return true + Predicate fallback = querySpec.getFilterExpression().isEmpty() ? x -> true : x -> false; + var result = queryResolver.query(store.values().stream(), querySpec, Predicate::or, fallback); + return success(result); + } finally { + lock.readLock().unlock(); + } + } + + /** + * Replaces an existing entity with a new object. + * + * @param newObject the new entity + * @return failure if an object with the same ID was not found. + */ + public StoreResult update(T newObject) { + lock.writeLock().lock(); + try { + var id = getId(newObject); + if (!store.containsKey(id)) { + return notFound("A VerifiableCredentialResource with ID %s was not found".formatted(id)); + } + store.put(id, newObject); + return success(); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Deletes the object with the given ID + * + * @param id The ID of the object to delete. + * @return failure if an object with the given ID was not found. + */ + public StoreResult deleteById(String id) { + lock.writeLock().lock(); + try { + if (!store.containsKey(id)) { + return notFound("A VerifiableCredentialResource with ID %s was not found".formatted(id)); + } + store.remove(id); + return success(); + } finally { + lock.writeLock().unlock(); + } + } + + protected abstract String getId(T newObject); + + protected abstract QueryResolver createQueryResolver(); +} diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryParticipantContextStore.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryParticipantContextStore.java new file mode 100644 index 000000000..41b0fc929 --- /dev/null +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryParticipantContextStore.java @@ -0,0 +1,36 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.defaults; + +import org.eclipse.edc.connector.core.store.CriterionToPredicateConverterImpl; +import org.eclipse.edc.connector.core.store.ReflectionBasedQueryResolver; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; +import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; +import org.eclipse.edc.spi.query.QueryResolver; + +/** + * In-memory variant of the {@link ParticipantContextStore} that is thread-safe. + */ +public class InMemoryParticipantContextStore extends InMemoryEntityStore implements ParticipantContextStore { + @Override + protected String getId(ParticipantContext newObject) { + return newObject.getParticipantId(); + } + + @Override + protected QueryResolver createQueryResolver() { + return new ReflectionBasedQueryResolver<>(ParticipantContext.class, new CriterionToPredicateConverterImpl()); + } +} diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStoreTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStoreTest.java index 3bc1dc8a5..9406ce17c 100644 --- a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStoreTest.java +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryCredentialStoreTest.java @@ -14,8 +14,8 @@ package org.eclipse.edc.identityhub.defaults; -import org.eclipse.edc.identityhub.credentials.store.test.CredentialStoreTestBase; import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identityhub.store.test.CredentialStoreTestBase; class InMemoryCredentialStoreTest extends CredentialStoreTestBase { diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryParticipantContextStoreTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryParticipantContextStoreTest.java new file mode 100644 index 000000000..2c5998e75 --- /dev/null +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/defaults/InMemoryParticipantContextStoreTest.java @@ -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 + * + */ + +package org.eclipse.edc.identityhub.defaults; + +import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; +import org.eclipse.edc.identityhub.store.test.ParticipantContextStoreTestBase; + +class InMemoryParticipantContextStoreTest extends ParticipantContextStoreTestBase { + + private final InMemoryParticipantContextStore store = new InMemoryParticipantContextStore(); + + @Override + protected ParticipantContextStore getStore() { + return store; + } +} \ No newline at end of file diff --git a/extensions/store/sql/identity-hub-credentials-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlCredentialStore.java b/extensions/store/sql/identity-hub-credentials-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlCredentialStore.java index 685a647d7..67b575c2b 100644 --- a/extensions/store/sql/identity-hub-credentials-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlCredentialStore.java +++ b/extensions/store/sql/identity-hub-credentials-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlCredentialStore.java @@ -124,7 +124,7 @@ public StoreResult update(VerifiableCredentialResource credentialResource) } @Override - public StoreResult delete(String id) { + public StoreResult deleteById(String id) { Objects.requireNonNull(id); return transactionContext.execute(() -> { try (var connection = getConnection()) { diff --git a/extensions/store/sql/identity-hub-credentials-store-sql/src/test/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlCredentialsStoreTest.java b/extensions/store/sql/identity-hub-credentials-store-sql/src/test/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlCredentialsStoreTest.java index 435ef7e32..b4f44c657 100644 --- a/extensions/store/sql/identity-hub-credentials-store-sql/src/test/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlCredentialsStoreTest.java +++ b/extensions/store/sql/identity-hub-credentials-store-sql/src/test/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlCredentialsStoreTest.java @@ -14,9 +14,9 @@ package org.eclipse.edc.identityhub.store.sql.credentials; -import org.eclipse.edc.identityhub.credentials.store.test.CredentialStoreTestBase; import org.eclipse.edc.identityhub.spi.store.CredentialStore; import org.eclipse.edc.identityhub.store.sql.credentials.schema.postgres.PostgresDialectStatements; +import org.eclipse.edc.identityhub.store.test.CredentialStoreTestBase; import org.eclipse.edc.junit.annotations.ComponentTest; import org.eclipse.edc.spi.types.TypeManager; import org.eclipse.edc.sql.QueryExecutor; diff --git a/extensions/store/sql/identity-hub-participantcontext-store-sql/build.gradle.kts b/extensions/store/sql/identity-hub-participantcontext-store-sql/build.gradle.kts new file mode 100644 index 000000000..bc281f68f --- /dev/null +++ b/extensions/store/sql/identity-hub-participantcontext-store-sql/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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:identity-hub-store-spi")) + implementation(libs.edc.core.sql) // for the SqlStatements + implementation(libs.edc.spi.transaction.datasource) + + testImplementation(testFixtures(project(":spi:identity-hub-store-spi"))) + testImplementation(testFixtures(libs.edc.core.sql)) + testImplementation(libs.edc.junit) +} diff --git a/extensions/store/sql/identity-hub-participantcontext-store-sql/docs/schema.sql b/extensions/store/sql/identity-hub-participantcontext-store-sql/docs/schema.sql new file mode 100644 index 000000000..3f789a29d --- /dev/null +++ b/extensions/store/sql/identity-hub-participantcontext-store-sql/docs/schema.sql @@ -0,0 +1,25 @@ +/* + * 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 + * + */ + +-- only intended for and tested with Postgres! +CREATE TABLE participant_context +( + participant_id VARCHAR PRIMARY KEY NOT NULL, -- ID of the ParticipantContext + created_date BIGINT NOT NULL, -- POSIX timestamp of the creation of the PC + last_modified_date BIGINT, -- POSIX timestamp of the last modified date + state INTEGER NOT NULL, -- 0 = CREATED, 1 = ACTIVE, 2 = DEACTIVATED + api_token_alias VARCHAR NOT NULL -- alias under which this PC's api token is stored in the vault +); +CREATE UNIQUE INDEX participant_context_participant_id_uindex ON participant_context USING btree (participant_id); + diff --git a/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/BaseSqlDialectStatements.java b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/BaseSqlDialectStatements.java new file mode 100644 index 000000000..89e36c549 --- /dev/null +++ b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/BaseSqlDialectStatements.java @@ -0,0 +1,67 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.store.sql.participantcontext; + +import org.eclipse.edc.identityhub.store.sql.participantcontext.schema.postgres.ParticipantContextMapping; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +import static java.lang.String.format; + +public class BaseSqlDialectStatements implements ParticipantContextStoreStatements { + @Override + public String getInsertTemplate() { + return executeStatement() + .column(getIdColumn()) + .column(getCreateTimestampColumn()) + .column(getLastModifiedTimestampColumn()) + .column(getStateColumn()) + .column(getApiTokenAliasColumn()) + .insertInto(getParticipantContextTable()); + } + + @Override + public String getUpdateTemplate() { + return executeStatement() + .column(getIdColumn()) + .column(getCreateTimestampColumn()) + .column(getLastModifiedTimestampColumn()) + .column(getStateColumn()) + .column(getApiTokenAliasColumn()) + .update(getParticipantContextTable(), getIdColumn()); + } + + @Override + public String getDeleteByIdTemplate() { + return executeStatement().delete(getParticipantContextTable(), getIdColumn()); + } + + @Override + public String getFindByIdTemplate() { + return format("SELECT * FROM %s WHERE %s = ?", getParticipantContextTable(), getIdColumn()); + + } + + @Override + public SqlQueryStatement createQuery(QuerySpec querySpec) { + var select = getSelectStatement(); + return new SqlQueryStatement(select, querySpec, new ParticipantContextMapping(this), false); + } + + @Override + public String getSelectStatement() { + return format("SELECT * FROM %s", getParticipantContextTable()); + } +} diff --git a/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/ParticipantContextStoreStatements.java b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/ParticipantContextStoreStatements.java new file mode 100644 index 000000000..1f3fde6ba --- /dev/null +++ b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/ParticipantContextStoreStatements.java @@ -0,0 +1,61 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.store.sql.participantcontext; + +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; +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 ParticipantContext} + */ +public interface ParticipantContextStoreStatements extends SqlStatements { + default String getParticipantContextTable() { + return "participant_context"; + } + + default String getIdColumn() { + return "participant_id"; + } + + default String getCreateTimestampColumn() { + return "created_date"; + } + + default String getLastModifiedTimestampColumn() { + return "last_modified_date"; + } + + default String getStateColumn() { + return "state"; + } + + default String getApiTokenAliasColumn() { + return "api_token_alias"; + } + + String getInsertTemplate(); + + String getUpdateTemplate(); + + String getDeleteByIdTemplate(); + + String getFindByIdTemplate(); + + SqlQueryStatement createQuery(QuerySpec query); + + String getSelectStatement(); +} diff --git a/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/SqlParticipantContextStore.java b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/SqlParticipantContextStore.java new file mode 100644 index 000000000..e4a445e8f --- /dev/null +++ b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/SqlParticipantContextStore.java @@ -0,0 +1,158 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.store.sql.participantcontext; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContextState; +import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; +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.util.Objects; +import java.util.stream.Stream; + +import static org.eclipse.edc.spi.result.StoreResult.alreadyExists; +import static org.eclipse.edc.spi.result.StoreResult.success; + + +/** + * SQL-based {@link ParticipantContext} store intended for use with PostgreSQL + */ +public class SqlParticipantContextStore extends AbstractSqlStore implements ParticipantContextStore { + + private final ParticipantContextStoreStatements statements; + + public SqlParticipantContextStore(DataSourceRegistry dataSourceRegistry, + String dataSourceName, + TransactionContext transactionContext, + ObjectMapper objectMapper, + QueryExecutor queryExecutor, + ParticipantContextStoreStatements statements) { + super(dataSourceRegistry, dataSourceName, transactionContext, objectMapper, queryExecutor); + this.statements = statements; + } + + @Override + public StoreResult create(ParticipantContext participantContext) { + var id = participantContext.getParticipantId(); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findByIdInternal(connection, id) != null) { + return alreadyExists(alreadyExistsErrorMessage(id)); + } + + var stmt = statements.getInsertTemplate(); + queryExecutor.execute(connection, stmt, + participantContext.getParticipantId(), + participantContext.getCreatedDate(), + participantContext.getLastModifiedDate(), + participantContext.getState(), + participantContext.getApiTokenAlias()); + return success(); + + } 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())); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult update(ParticipantContext participantContext) { + var id = participantContext.getParticipantId(); + + Objects.requireNonNull(participantContext); + Objects.requireNonNull(id); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findByIdInternal(connection, id) != null) { + queryExecutor.execute(connection, + statements.getUpdateTemplate(), + id, + participantContext.getCreatedDate(), + participantContext.getLastModifiedDate(), + participantContext.getState(), + participantContext.getApiTokenAlias(), + id); + return StoreResult.success(); + } + return StoreResult.notFound(notFoundErrorMessage(id)); + } 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 ParticipantContext findByIdInternal(Connection connection, String id) { + return transactionContext.execute(() -> { + var stmt = statements.getFindByIdTemplate(); + return queryExecutor.single(connection, false, this::mapResultSet, stmt, id); + }); + } + + private ParticipantContext mapResultSet(ResultSet resultSet) throws Exception { + + var id = resultSet.getString(statements.getIdColumn()); + var created = resultSet.getLong(statements.getCreateTimestampColumn()); + var lastmodified = resultSet.getLong(statements.getLastModifiedTimestampColumn()); + var state = resultSet.getInt(statements.getStateColumn()); + var tokenAliase = resultSet.getString(statements.getApiTokenAliasColumn()); + + return ParticipantContext.Builder.newInstance() + .participantId(id) + .createdAt(created) + .lastModified(lastmodified) + .state(ParticipantContextState.values()[state]) + .apiTokenAlias(tokenAliase) + .build(); + } +} diff --git a/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/SqlParticipantContextStoreExtension.java b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/SqlParticipantContextStoreExtension.java new file mode 100644 index 000000000..5f9ab4669 --- /dev/null +++ b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/SqlParticipantContextStoreExtension.java @@ -0,0 +1,63 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.store.sql.participantcontext; + +import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; +import org.eclipse.edc.identityhub.store.sql.participantcontext.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.transaction.datasource.spi.DataSourceRegistry; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import static org.eclipse.edc.identityhub.store.sql.participantcontext.SqlParticipantContextStoreExtension.NAME; + +@Extension(value = NAME) +public class SqlParticipantContextStoreExtension implements ServiceExtension { + public static final String NAME = "ParticipantContext SQL Store Extension"; + + @Setting(value = "Datasource name for the ParticipantContext database", defaultValue = DataSourceRegistry.DEFAULT_DATASOURCE) + public static final String DATASOURCE_SETTING_NAME = "edc.datasource.participantcontext.name"; + @Inject + private DataSourceRegistry dataSourceRegistry; + @Inject + private TransactionContext transactionContext; + @Inject + private TypeManager typemanager; + @Inject + private QueryExecutor queryExecutor; + @Inject(required = false) + private ParticipantContextStoreStatements statements; + + + @Provider + public ParticipantContextStore createSqlStore(ServiceExtensionContext context) { + return new SqlParticipantContextStore(dataSourceRegistry, getDataSourceName(context), transactionContext, typemanager.getMapper(), + queryExecutor, getStatementImpl()); + } + + private ParticipantContextStoreStatements getStatementImpl() { + return statements != null ? statements : new PostgresDialectStatements(); + } + + private String getDataSourceName(ServiceExtensionContext context) { + return context.getConfig().getString(DATASOURCE_SETTING_NAME, DataSourceRegistry.DEFAULT_DATASOURCE); + } +} diff --git a/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/schema/postgres/ParticipantContextMapping.java b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/schema/postgres/ParticipantContextMapping.java new file mode 100644 index 000000000..81a62f41c --- /dev/null +++ b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/schema/postgres/ParticipantContextMapping.java @@ -0,0 +1,39 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.store.sql.participantcontext.schema.postgres; + +import org.eclipse.edc.identityhub.store.sql.participantcontext.ParticipantContextStoreStatements; +import org.eclipse.edc.sql.translation.TranslationMapping; + + +/** + * Provides a mapping from the canonical format to SQL column names for a {@link org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource} + */ +public class ParticipantContextMapping extends TranslationMapping { + + public static final String FIELD_ID = "participantId"; + public static final String FIELD_CREATE_TIMESTAMP = "createdDate"; + public static final String FIELD_LASTMODIFIED_TIMESTAMP = "lastModifiedDate"; + public static final String FIELD_STATE = "state"; + public static final String FIELD_API_TOKEN_ALIAS = "apiTokenAlias"; + + public ParticipantContextMapping(ParticipantContextStoreStatements statements) { + add(FIELD_ID, statements.getIdColumn()); + add(FIELD_CREATE_TIMESTAMP, statements.getCreateTimestampColumn()); + add(FIELD_STATE, statements.getStateColumn()); + add(FIELD_LASTMODIFIED_TIMESTAMP, statements.getLastModifiedTimestampColumn()); + add(FIELD_API_TOKEN_ALIAS, statements.getApiTokenAliasColumn()); + } +} \ No newline at end of file diff --git a/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/schema/postgres/PostgresDialectStatements.java b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/schema/postgres/PostgresDialectStatements.java new file mode 100644 index 000000000..d4358482b --- /dev/null +++ b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/participantcontext/schema/postgres/PostgresDialectStatements.java @@ -0,0 +1,29 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.store.sql.participantcontext.schema.postgres; + +import org.eclipse.edc.identityhub.store.sql.participantcontext.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/identity-hub-participantcontext-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..1e9e0f2ca --- /dev/null +++ b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2023 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 +# +# + +org.eclipse.edc.identityhub.store.sql.participantcontext.SqlParticipantContextStoreExtension \ No newline at end of file diff --git a/extensions/store/sql/identity-hub-participantcontext-store-sql/src/test/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlParticipantContextStoreTest.java b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/test/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlParticipantContextStoreTest.java new file mode 100644 index 000000000..a87e7e5c8 --- /dev/null +++ b/extensions/store/sql/identity-hub-participantcontext-store-sql/src/test/java/org/eclipse/edc/identityhub/store/sql/credentials/SqlParticipantContextStoreTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 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 + * + */ + +package org.eclipse.edc.identityhub.store.sql.credentials; + +import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; +import org.eclipse.edc.identityhub.store.sql.participantcontext.ParticipantContextStoreStatements; +import org.eclipse.edc.identityhub.store.sql.participantcontext.SqlParticipantContextStore; +import org.eclipse.edc.identityhub.store.sql.participantcontext.schema.postgres.PostgresDialectStatements; +import org.eclipse.edc.identityhub.store.test.ParticipantContextStoreTestBase; +import org.eclipse.edc.junit.annotations.ComponentTest; +import org.eclipse.edc.spi.types.TypeManager; +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.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +@ComponentTest +@ExtendWith(PostgresqlStoreSetupExtension.class) +class SqlParticipantContextStoreTest extends ParticipantContextStoreTestBase { + + private final ParticipantContextStoreStatements statements = new PostgresDialectStatements(); + private SqlParticipantContextStore store; + + @BeforeEach + void setup(PostgresqlStoreSetupExtension extension, QueryExecutor queryExecutor) throws IOException { + var typeManager = new TypeManager(); + store = new SqlParticipantContextStore(extension.getDataSourceRegistry(), extension.getDatasourceName(), + extension.getTransactionContext(), typeManager.getMapper(), queryExecutor, statements); + + var schema = Files.readString(Paths.get("./docs/schema.sql")); + extension.runQuery(schema); + } + + @AfterEach + void tearDown(PostgresqlStoreSetupExtension extension) { + extension.runQuery("DROP TABLE " + statements.getParticipantContextTable() + " CASCADE"); + } + + @Override + protected ParticipantContextStore getStore() { + return store; + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a2c3f5adf..5026d7230 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,6 +36,7 @@ include(":core:identity-hub-did") include(":extensions:common:security") include(":extensions:store:sql:identity-hub-did-store-sql") include(":extensions:store:sql:identity-hub-credentials-store-sql") +include(":extensions:store:sql:identity-hub-participantcontext-store-sql") include(":extensions:did:local-did-publisher") include(":extensions:did:did-management-api") diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContext.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContext.java new file mode 100644 index 000000000..2bb05c951 --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContext.java @@ -0,0 +1,124 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.spi.model.participant; + +import java.time.Instant; +import java.util.Objects; + +public class ParticipantContext { + private String participantId; + private long createdDate; + private long lastModifiedDate; + private int state; // CREATED, ACTIVATED, DEACTIVATED + private String apiTokenAlias; // or apiTokenAlias + + private ParticipantContext() { + } + + /** + * Participant IDs must be stable and globally unique (i.e. per dataspace). They will be visible in contracts, negotiations, etc. + */ + public String getParticipantId() { + return participantId; + } + + /** + * The POSIX timestamp in ms when this entry was created. Immutable + */ + public long getCreatedDate() { + return createdDate; + } + + /** + * The POSIX timestamp in ms when this entry was last modified. + */ + public long getLastModifiedDate() { + return lastModifiedDate; + } + + /** + * The ParticipantContext's state. 0 = CREATED, 1 = ACTIVATED, 2 = DEACTIVATED + */ + public int getState() { + return state; + } + + public ParticipantContextState getStateAsEnum() { + return ParticipantContextState.values()[state]; + } + + /** + * Get the alias, under which the API token for this {@link ParticipantContext} is stored in the {@link org.eclipse.edc.spi.security.Vault}. + * Note that API tokens should never be stored in the database, much less so unencrypted. + */ + public String getApiTokenAlias() { + return apiTokenAlias; + } + + /** + * Updates the last-modified field. + */ + public void updateLastModified() { + this.lastModifiedDate = Instant.now().toEpochMilli(); + } + + public static final class Builder { + private final ParticipantContext participantContext; + + private Builder() { + participantContext = new ParticipantContext(); + participantContext.createdDate = Instant.now().toEpochMilli(); + } + + public Builder createdAt(long createdAt) { + this.participantContext.createdDate = createdAt; + return this; + } + + public Builder lastModified(long lastModified) { + this.participantContext.lastModifiedDate = lastModified; + return this; + } + + public Builder participantId(String participantId) { + this.participantContext.participantId = participantId; + return this; + } + + public Builder state(ParticipantContextState state) { + this.participantContext.state = state.ordinal(); + return this; + } + + public Builder apiTokenAlias(String apiToken) { + this.participantContext.apiTokenAlias = apiToken; + return this; + } + + public ParticipantContext build() { + Objects.requireNonNull(participantContext.participantId, "Participant ID cannot be null"); + Objects.requireNonNull(participantContext.apiTokenAlias, "API Token Alias cannot be null"); + + if (participantContext.getLastModifiedDate() == 0L) { + participantContext.lastModifiedDate = participantContext.getCreatedDate(); + } + return participantContext; + } + + public static Builder newInstance() { + return new Builder(); + } + } +} \ No newline at end of file diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContextState.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContextState.java new file mode 100644 index 000000000..54dcb6b78 --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContextState.java @@ -0,0 +1,33 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.spi.model.participant; + +/** + * The state a {@link ParticipantContext} entry is in. + */ +public enum ParticipantContextState { + /** + * The {@link ParticipantContext} was created in the database, but is not yet operational. + */ + CREATED, + /** + * The {@link ParticipantContext} is operational and can be used. + */ + ACTIVATED, + /** + * The {@link ParticipantContext} is disabled and can not be used currently. + */ + DEACTIVATED +} diff --git a/spi/identity-hub-spi/src/test/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContextTest.java b/spi/identity-hub-spi/src/test/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContextTest.java new file mode 100644 index 000000000..a62221d86 --- /dev/null +++ b/spi/identity-hub-spi/src/test/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContextTest.java @@ -0,0 +1,74 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.spi.model.participant; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +class ParticipantContextTest { + + @Test + void verifyCreateTimestamp() { + var context = ParticipantContext.Builder.newInstance() + .participantId("test-id") + .apiTokenAlias("foo-token") + .build(); + + assertThat(context.getCreatedDate()).isNotZero().isLessThanOrEqualTo(Instant.now().toEpochMilli()); + + var context2 = ParticipantContext.Builder.newInstance() + .participantId("test-id") + .apiTokenAlias("foo-token") + .createdAt(42) + .build(); + + assertThat(context2.getCreatedDate()).isEqualTo(42); + } + + @Test + void verifyLastModifiedTimestamp() { + var context = ParticipantContext.Builder.newInstance() + .participantId("test-id") + .apiTokenAlias("foo-token") + .build(); + + assertThat(context.getLastModifiedDate()).isNotZero().isEqualTo(context.getCreatedDate()); + + var context2 = ParticipantContext.Builder.newInstance() + .participantId("test-id") + .apiTokenAlias("foo-token") + .lastModified(42) + .build(); + + assertThat(context2.getLastModifiedDate()).isEqualTo(42); + } + + @Test + void verifyState() { + var context = ParticipantContext.Builder.newInstance() + .participantId("test-id") + .apiTokenAlias("foo-token") + .state(ParticipantContextState.CREATED); + + assertThat(context.build().getState()).isEqualTo(0); + assertThat(context.state(ParticipantContextState.ACTIVATED).build().getState()).isEqualTo(1); + assertThat(context.state(ParticipantContextState.DEACTIVATED).build().getState()).isEqualTo(2); + + } + +} \ No newline at end of file diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/CredentialStore.java b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/CredentialStore.java index 3b265e3cb..dab78b385 100644 --- a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/CredentialStore.java +++ b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/CredentialStore.java @@ -56,7 +56,7 @@ public interface CredentialStore { * @param id The ID of the verifiable credential resource to delete. * @return A {@link StoreResult} object indicating the result of the operation. */ - StoreResult delete(String id); + StoreResult deleteById(String id); default String alreadyExistsErrorMessage(String id) { return "A VerifiableCredentialResource with ID %s already exists.".formatted(id); diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/ParticipantContextStore.java b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/ParticipantContextStore.java new file mode 100644 index 000000000..0f83b9a01 --- /dev/null +++ b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/ParticipantContextStore.java @@ -0,0 +1,66 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.spi.store; + +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; + +import java.util.stream.Stream; + +/** + * Stores {@link ParticipantContext} objects and provides basic CRUD operations. + */ +public interface ParticipantContextStore { + /** + * Creates a ParticipantContext resource in the store. + * + * @param context The ParticipantContext resource to create. + * @return success if not exists, a failure if already exists + */ + StoreResult create(ParticipantContext context); + + /** + * Queries the store for ParticipantContexts based on the given query specification. + * + * @param querySpec The {@link QuerySpec} indicating the criteria for the query. + * @return A {@link StoreResult} object containing a list of {@link ParticipantContext} objects that match the query. + */ + StoreResult> query(QuerySpec querySpec); + + /** + * Updates a ParticipantContext resource in the store. + * + * @param participantContext The ParticipantContext resource to update. Note that all fields are overwritten. + * @return success if participant context exists, failure otherwise + */ + StoreResult update(ParticipantContext participantContext); + + /** + * Deletes a ParticipantContext resource from the store based on the given ID. + * + * @param id The ID of the ParticipantContext resource to delete. + * @return success if the object could be deleted, a failure otherwise + */ + StoreResult deleteById(String id); + + default String alreadyExistsErrorMessage(String id) { + return "A VerifiableCredentialResource with ID %s already exists.".formatted(id); + } + + default String notFoundErrorMessage(String id) { + return "A VerifiableCredentialResource with ID %s was not found.".formatted(id); + } +} diff --git a/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/credentials/store/test/CredentialStoreTestBase.java b/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/CredentialStoreTestBase.java similarity index 98% rename from spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/credentials/store/test/CredentialStoreTestBase.java rename to spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/CredentialStoreTestBase.java index 5e4eb4423..f0c7022bb 100644 --- a/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/credentials/store/test/CredentialStoreTestBase.java +++ b/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/CredentialStoreTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Metaform Systems, Inc. + * 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 @@ -8,11 +8,11 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Metaform Systems,Inc. - initial API and implementation + * Metaform Systems, Inc. - initial API and implementation * */ -package org.eclipse.edc.identityhub.credentials.store.test; +package org.eclipse.edc.identityhub.store.test; import org.assertj.core.api.Assertions; import org.eclipse.edc.identityhub.spi.store.CredentialStore; @@ -382,13 +382,13 @@ void delete() { var credential = createCredential(); getStore().create(credential); - var deleteRes = getStore().delete(credential.getId()); + var deleteRes = getStore().deleteById(credential.getId()); assertThat(deleteRes).isSucceeded(); } @Test void delete_whenNotExists() { - assertThat(getStore().delete("not-exist")).isFailed() + assertThat(getStore().deleteById("not-exist")).isFailed() .detail().contains("with ID not-exist was not found"); } diff --git a/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/ParticipantContextStoreTestBase.java b/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/ParticipantContextStoreTestBase.java new file mode 100644 index 000000000..bac3685fb --- /dev/null +++ b/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/ParticipantContextStoreTestBase.java @@ -0,0 +1,187 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.store.test; + +import org.assertj.core.api.Assertions; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; +import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.junit.jupiter.api.Test; + +import static java.util.stream.IntStream.range; +import static org.eclipse.edc.identityhub.spi.model.participant.ParticipantContextState.ACTIVATED; +import static org.eclipse.edc.identityhub.spi.model.participant.ParticipantContextState.CREATED; +import static org.eclipse.edc.identityhub.spi.model.participant.ParticipantContextState.DEACTIVATED; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; + +public abstract class ParticipantContextStoreTestBase { + + @Test + void create() { + var result = getStore().create(createParticipantContext()); + assertThat(result).isSucceeded(); + + } + + @Test + void create_whenExists_shouldReturnFailure() { + var context = createParticipantContext(); + var result = getStore().create(context); + assertThat(result).isSucceeded(); + var result2 = getStore().create(context); + + assertThat(result2).isFailed().detail().contains("already exists"); + } + + @Test + void query_byId() { + range(0, 5) + .mapToObj(i -> createParticipantContextBuilder().participantId("id" + i).build()) + .forEach(getStore()::create); + + var query = QuerySpec.Builder.newInstance() + .filter(new Criterion("participantId", "=", "id2")) + .build(); + + assertThat(getStore().query(query)).isSucceeded() + .satisfies(str -> Assertions.assertThat(str).hasSize(1)); + } + + @Test + void query_byProperty() { + var participantContext = createParticipantContextBuilder().state(DEACTIVATED).build(); + getStore().create(participantContext); + + var query = QuerySpec.Builder.newInstance() + .filter(new Criterion("state", "=", 2)) + .build(); + + assertThat(getStore().query(query)).isSucceeded() + .satisfies(str -> Assertions.assertThat(str) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(participantContext)); + } + + @Test + void query_noQuerySpec() { + var resources = range(0, 5) + .mapToObj(i -> createParticipantContextBuilder().participantId("id" + i).build()) + .toList(); + + resources.forEach(getStore()::create); + + var res = getStore().query(QuerySpec.none()); + assertThat(res).isSucceeded(); + Assertions.assertThat(res.getContent()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(resources.toArray(new ParticipantContext[0])); + } + + @Test + void query_whenNotFound() { + var resources = range(0, 5) + .mapToObj(i -> createParticipantContextBuilder() + .participantId("id" + i) + .build()) + .toList(); + + resources.forEach(getStore()::create); + + var query = QuerySpec.Builder.newInstance() + .filter(new Criterion("participantId", "=", "id7")) + .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 -> createParticipantContextBuilder() + .participantId("id" + i) + .build()) + .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 context = createParticipantContextBuilder(); + var result = getStore().create(context.build()); + assertThat(result).isSucceeded(); + + var updateRes = getStore().update(context.state(ACTIVATED).build()); + assertThat(updateRes).isSucceeded(); + } + + @Test + void update_whenIdChanges_fails() { + var context = createParticipantContextBuilder(); + var result = getStore().create(context.build()); + + var updateRes = getStore().update(context.state(DEACTIVATED).participantId("another-id").build()); + assertThat(updateRes).isFailed().detail().contains("with ID another-id was not found"); + } + + @Test + void update_whenNotExists() { + var context = createParticipantContextBuilder(); + var updateRes = getStore().update(context.state(DEACTIVATED).participantId("another-id").build()); + assertThat(updateRes).isFailed().detail().contains("with ID another-id was not found"); + } + + @Test + void delete() { + var context = createParticipantContext(); + getStore().create(context); + + var deleteRes = getStore().deleteById(context.getParticipantId()); + assertThat(deleteRes).isSucceeded(); + } + + @Test + void delete_whenNotExists() { + assertThat(getStore().deleteById("not-exist")).isFailed() + .detail().contains("with ID not-exist was not found"); + } + + protected abstract ParticipantContextStore getStore(); + + private ParticipantContext createParticipantContext() { + return ParticipantContext.Builder.newInstance() + .participantId("test-participant") + .state(CREATED) + .apiTokenAlias("test-alias") + .build(); + } + + private ParticipantContext.Builder createParticipantContextBuilder() { + return ParticipantContext.Builder.newInstance() + .participantId("test-participant") + .state(CREATED) + .apiTokenAlias("test-alias"); + } +}