From eb7b540951617c7e90dd58b89d767de308519eb9 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Sun, 10 Dec 2023 11:01:02 +0100 Subject: [PATCH] feat(sql): create SqlDidResourceStore (#191) * feat: add implementations for `DidResourceStore` * DEPENDENCIES --- DEPENDENCIES | 29 +- .../build.gradle.kts | 27 ++ .../docs/schema.sql | 24 ++ .../store/sql/BaseSqlDialectStatements.java | 67 +++++ .../did/store/sql/DidResourceStatements.java | 60 ++++ .../did/store/sql/SqlDidResourceStore.java | 147 ++++++++++ .../sql/SqlDidResourceStoreExtension.java | 63 ++++ .../schema/postgres/DidDocumentMapping.java | 37 +++ .../schema/postgres/DidResourceMapping.java | 39 +++ .../postgres/PostgresDialectStatements.java | 52 ++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 + .../src/main/resources/did.json | 57 ++++ .../store/sql/SqlDidResourceStoreTest.java | 58 ++++ .../src/test/resources/did.json | 57 ++++ core/identity-hub-did/build.gradle.kts | 5 +- .../defaults/DidDefaultServicesExtension.java | 32 +++ .../defaults/InMemoryDidResourceStore.java | 101 +++++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../InMemoryDidResourceStoreTest.java | 28 ++ gradle/libs.versions.toml | 2 + settings.gradle.kts | 1 + spi/identity-hub-did-spi/build.gradle.kts | 10 +- .../identithub/did/spi/model/DidResource.java | 15 +- .../identithub/did/spi/model/DidState.java | 6 + .../did/spi/store/DidResourceStore.java | 13 +- .../store/test/DidResourceStoreTestBase.java | 269 ++++++++++++++++++ 26 files changed, 1191 insertions(+), 24 deletions(-) create mode 100644 core/identity-hub-did-store-sql/build.gradle.kts create mode 100644 core/identity-hub-did-store-sql/docs/schema.sql create mode 100644 core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/BaseSqlDialectStatements.java create mode 100644 core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/DidResourceStatements.java create mode 100644 core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStore.java create mode 100644 core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreExtension.java create mode 100644 core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidDocumentMapping.java create mode 100644 core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidResourceMapping.java create mode 100644 core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/PostgresDialectStatements.java create mode 100644 core/identity-hub-did-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 core/identity-hub-did-store-sql/src/main/resources/did.json create mode 100644 core/identity-hub-did-store-sql/src/test/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreTest.java create mode 100644 core/identity-hub-did-store-sql/src/test/resources/did.json create mode 100644 core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/DidDefaultServicesExtension.java create mode 100644 core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStore.java create mode 100644 core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStoreTest.java create mode 100644 spi/identity-hub-did-spi/src/testFixtures/java/org/eclipse/edc/identityhub/did/store/test/DidResourceStoreTestBase.java diff --git a/DEPENDENCIES b/DEPENDENCIES index cd7b50306..46ed30205 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -28,28 +28,25 @@ maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.16.0, Apache-2. maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.14.0, Apache-2.0, approved, #5933 maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.15.1, Apache-2.0, approved, #8802 maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.15.2, Apache-2.0, approved, #8802 -maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.16.0, , restricted, clearlydefined -maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jakarta-jsonp/2.16.0, , restricted, clearlydefined +maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.16.0, Apache-2.0, approved, #11855 +maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jakarta-jsonp/2.16.0, Apache-2.0, approved, #11854 maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.14.0, Apache-2.0, approved, #4699 maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.14.2, Apache-2.0, approved, #4699 maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.1, Apache-2.0, approved, #7930 maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.2, Apache-2.0, approved, #7930 -maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.16.0, , restricted, clearlydefined -maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-base/2.16.0, , restricted, clearlydefined +maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.16.0, Apache-2.0, approved, #11853 +maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-base/2.16.0, Apache-2.0, approved, #11851 maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.15.1, Apache-2.0, approved, #9236 maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.15.2, Apache-2.0, approved, #9236 -maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.16.0, , restricted, clearlydefined +maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.16.0, Apache-2.0, approved, #11858 maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.15.3, Apache-2.0, approved, #9241 -maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.16.0, , restricted, clearlydefined +maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.16.0, Apache-2.0, approved, #11856 maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.15.1, Apache-2.0, approved, #7929 -maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.16.0, , restricted, clearlydefined +maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.16.0, Apache-2.0, approved, #11852 maven/mavencentral/com.fasterxml.uuid/java-uuid-generator/4.1.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.github.cliftonlabs/json-simple/3.0.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.github.docker-java/docker-java-api/3.3.3, Apache-2.0, approved, #10346 maven/mavencentral/com.github.docker-java/docker-java-api/3.3.4, Apache-2.0, approved, #10346 -maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.3, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #7946 maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.4, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #7946 -maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.3, Apache-2.0, approved, #7942 maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.4, Apache-2.0, approved, #7942 maven/mavencentral/com.github.java-json-tools/btf/1.3, Apache-2.0 OR LGPL-3.0-or-later, approved, #2721 maven/mavencentral/com.github.java-json-tools/jackson-coreutils-equivalence/1.0, LGPL-3.0 OR Apache-2.0, approved, clearlydefined @@ -83,6 +80,7 @@ maven/mavencentral/com.lmax/disruptor/3.4.4, Apache-2.0, approved, clearlydefine maven/mavencentral/com.networknt/json-schema-validator/1.0.76, Apache-2.0, approved, CQ22638 maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.28, Apache-2.0, approved, clearlydefined maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37, Apache-2.0, approved, #11701 +maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37.2, Apache-2.0, approved, #11701 maven/mavencentral/com.puppycrawl.tools/checkstyle/10.0, LGPL-2.1-or-later, approved, #7936 maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause, approved, clearlydefined maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.12.0, Apache-2.0, approved, #11159 @@ -182,7 +180,6 @@ maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636 maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.1, Apache-2.0, approved, #7164 maven/mavencentral/net.bytebuddy/byte-buddy/1.12.21, Apache-2.0 AND BSD-3-Clause, approved, #1811 maven/mavencentral/net.bytebuddy/byte-buddy/1.14.1, Apache-2.0 AND BSD-3-Clause, approved, #7163 -maven/mavencentral/net.java.dev.jna/jna/5.12.1, Apache-2.0 OR LGPL-2.1-or-later, approved, #3217 maven/mavencentral/net.java.dev.jna/jna/5.13.0, Apache-2.0 AND LGPL-2.1-or-later, approved, #6709 maven/mavencentral/net.javacrumbs.json-unit/json-unit-core/2.36.0, Apache-2.0, approved, clearlydefined maven/mavencentral/net.minidev/accessors-smart/2.4.7, Apache-2.0, approved, #7515 @@ -218,6 +215,7 @@ maven/mavencentral/org.bouncycastle/bcutil-jdk18on/1.72, MIT, approved, #3790 maven/mavencentral/org.bouncycastle/bcutil-jdk18on/1.77, MIT, approved, #11596 maven/mavencentral/org.ccil.cowan.tagsoup/tagsoup/1.2.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.checkerframework/checker-qual/3.12.0, MIT, approved, clearlydefined +maven/mavencentral/org.checkerframework/checker-qual/3.31.0, MIT, approved, clearlydefined maven/mavencentral/org.eclipse.angus/angus-activation/1.0.0, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.angus maven/mavencentral/org.eclipse.edc/api-observability/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/autodoc-processor/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc @@ -254,6 +252,7 @@ maven/mavencentral/org.eclipse.edc/policy-evaluator/0.4.2-SNAPSHOT, Apache-2.0, maven/mavencentral/org.eclipse.edc/policy-model/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/policy-spi/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/runtime-metamodel/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/sql-core/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/state-machine/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transaction-datasource-spi/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transaction-spi/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc @@ -320,7 +319,7 @@ maven/mavencentral/org.jetbrains/annotations/13.0, Apache-2.0, approved, clearly maven/mavencentral/org.jetbrains/annotations/17.0.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.jetbrains/annotations/24.0.1, Apache-2.0, approved, #7417 maven/mavencentral/org.jetbrains/annotations/24.1.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.junit-pioneer/junit-pioneer/2.2.0, , restricted, clearlydefined +maven/mavencentral/org.junit-pioneer/junit-pioneer/2.2.0, EPL-2.0, approved, #11857 maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.10.1, EPL-2.0, approved, #9714 maven/mavencentral/org.junit.jupiter/junit-jupiter-engine/5.10.1, EPL-2.0, approved, #9711 maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.10.1, EPL-2.0, approved, #9708 @@ -345,6 +344,7 @@ maven/mavencentral/org.ow2.asm/asm-tree/9.6, BSD-3-Clause, approved, #10773 maven/mavencentral/org.ow2.asm/asm/9.1, BSD-3-Clause, approved, CQ23029 maven/mavencentral/org.ow2.asm/asm/9.2, BSD-3-Clause, approved, CQ23635 maven/mavencentral/org.ow2.asm/asm/9.6, BSD-3-Clause, approved, #10776 +maven/mavencentral/org.postgresql/postgresql/42.7.0, BSD-2-Clause AND LicenseRef-scancode-free-unknown AND Apache-2.0, restricted, #11681 maven/mavencentral/org.reflections/reflections/0.10.2, Apache-2.0 AND WTFPL, approved, clearlydefined maven/mavencentral/org.rnorth.duct-tape/duct-tape/1.0.8, MIT, approved, clearlydefined maven/mavencentral/org.slf4j/slf4j-api/1.7.22, MIT, approved, CQ11943 @@ -356,9 +356,10 @@ maven/mavencentral/org.slf4j/slf4j-api/1.7.36, MIT, approved, CQ13368 maven/mavencentral/org.slf4j/slf4j-api/2.0.5, MIT, approved, #5915 maven/mavencentral/org.slf4j/slf4j-api/2.0.6, MIT, approved, #5915 maven/mavencentral/org.slf4j/slf4j-api/2.0.9, MIT, approved, #5915 -maven/mavencentral/org.testcontainers/junit-jupiter/1.19.1, MIT, approved, #10344 +maven/mavencentral/org.testcontainers/database-commons/1.19.3, Apache-2.0, approved, #10345 +maven/mavencentral/org.testcontainers/jdbc/1.19.3, Apache-2.0, approved, #10348 maven/mavencentral/org.testcontainers/junit-jupiter/1.19.3, MIT, approved, #10344 -maven/mavencentral/org.testcontainers/testcontainers/1.19.1, Apache-2.0 AND MIT, approved, #10347 +maven/mavencentral/org.testcontainers/postgresql/1.19.3, MIT, approved, #10350 maven/mavencentral/org.testcontainers/testcontainers/1.19.3, Apache-2.0 AND MIT, approved, #10347 maven/mavencentral/org.xmlunit/xmlunit-core/2.9.1, Apache-2.0, approved, #6272 maven/mavencentral/org.xmlunit/xmlunit-placeholders/2.9.1, Apache-2.0, approved, clearlydefined diff --git a/core/identity-hub-did-store-sql/build.gradle.kts b/core/identity-hub-did-store-sql/build.gradle.kts new file mode 100644 index 000000000..187a8a97f --- /dev/null +++ b/core/identity-hub-did-store-sql/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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 + * + */ + +plugins { + `java-library` +} + +dependencies { + api(project(":spi:identity-hub-did-spi")) + implementation(libs.edc.core.sql) // for the SqlStatements + implementation(libs.edc.spi.transaction.datasource) + + testImplementation(testFixtures(project(":spi:identity-hub-did-spi"))) + testImplementation(testFixtures(libs.edc.core.sql)) + testImplementation(libs.edc.junit) +} diff --git a/core/identity-hub-did-store-sql/docs/schema.sql b/core/identity-hub-did-store-sql/docs/schema.sql new file mode 100644 index 000000000..194f0a3f2 --- /dev/null +++ b/core/identity-hub-did-store-sql/docs/schema.sql @@ -0,0 +1,24 @@ +/* + * 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 + * + */ + +-- only intended for and tested with Postgres! +CREATE TABLE IF NOT EXISTS did_resources +( + did VARCHAR NOT NULL, + create_timestamp BIGINT NOT NULL, + state_timestamp BIGINT NOT NULL, + state INT NOT NULL, + did_document JSON NOT NULL, + PRIMARY KEY (did) +); diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/BaseSqlDialectStatements.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/BaseSqlDialectStatements.java new file mode 100644 index 000000000..7a2a4adab --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/BaseSqlDialectStatements.java @@ -0,0 +1,67 @@ +/* + * 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.did.store.sql; + +import org.eclipse.edc.identityhub.did.store.sql.schema.postgres.DidResourceMapping; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +import static java.lang.String.format; + +public class BaseSqlDialectStatements implements DidResourceStatements { + @Override + public String getInsertTemplate() { + return executeStatement() + .column(getIdColumn()) + .column(getStateColumn()) + .column(getCreateTimestampColumn()) + .column(getStateTimestampColumn()) + .jsonColumn(getDidDocumentColumn()) + .insertInto(getDidResourceTableName()); + } + + @Override + public String getUpdateTemplate() { + return executeStatement() + .column(getIdColumn()) + .column(getStateColumn()) + .column(getCreateTimestampColumn()) + .column(getStateTimestampColumn()) + .jsonColumn(getDidDocumentColumn()) + .update(getDidResourceTableName(), getIdColumn()); + } + + @Override + public String getDeleteByIdTemplate() { + return executeStatement().delete(getDidResourceTableName(), getIdColumn()); + } + + @Override + public String getFindByIdTemplate() { + return format("SELECT * FROM %s WHERE %s = ?", getDidResourceTableName(), getIdColumn()); + + } + + @Override + public SqlQueryStatement createQuery(QuerySpec querySpec) { + var select = getSelectStatement(); + return new SqlQueryStatement(select, querySpec, new DidResourceMapping(this)); + } + + @Override + public String getSelectStatement() { + return format("SELECT * FROM %s", getDidResourceTableName()); + } +} diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/DidResourceStatements.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/DidResourceStatements.java new file mode 100644 index 000000000..38413e0d5 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/DidResourceStatements.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.did.store.sql; + +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 org.eclipse.edc.identithub.did.spi.store.DidResourceStore} + */ +public interface DidResourceStatements extends SqlStatements { + default String getDidResourceTableName() { + return "did_resources"; + } + + default String getIdColumn() { + return "did"; + } + + default String getStateColumn() { + return "state"; + } + + default String getStateTimestampColumn() { + return "state_timestamp"; + } + + default String getCreateTimestampColumn() { + return "create_timestamp"; + } + + default String getDidDocumentColumn() { + return "did_document"; + } + + String getInsertTemplate(); + + String getUpdateTemplate(); + + String getDeleteByIdTemplate(); + + String getFindByIdTemplate(); + + SqlQueryStatement createQuery(QuerySpec query); + + String getSelectStatement(); +} diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStore.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStore.java new file mode 100644 index 000000000..d68dd1d40 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStore.java @@ -0,0 +1,147 @@ +/* + * 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.did.store.sql; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.iam.did.spi.document.DidDocument; +import org.eclipse.edc.identithub.did.spi.model.DidResource; +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +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.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Objects; + + +/** + * SQL-based {@link DidResourceStore} intended for use with PostgreSQLs + */ +public class SqlDidResourceStore extends AbstractSqlStore implements DidResourceStore { + + private final DidResourceStatements statements; + + public SqlDidResourceStore(DataSourceRegistry dataSourceRegistry, String dataSourceName, TransactionContext transactionContext, + ObjectMapper objectMapper, QueryExecutor queryExecutor, DidResourceStatements statements) { + super(dataSourceRegistry, dataSourceName, transactionContext, objectMapper, queryExecutor); + this.statements = statements; + } + + + @Override + public StoreResult save(DidResource resource) { + var did = resource.getDid(); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findById(did) != null) { + return StoreResult.alreadyExists(alreadyExistsErrorMessage(did)); + } + + var stmt = statements.getInsertTemplate(); + queryExecutor.execute(connection, stmt, + did, + resource.getState(), + resource.getCreateTimestamp(), + resource.getStateTimestamp(), + toJson(resource.getDocument())); + return StoreResult.success(); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult update(DidResource resource) { + var did = resource.getDid(); + Objects.requireNonNull(resource); + Objects.requireNonNull(did); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findById(did) != null) { + queryExecutor.execute(connection, statements.getUpdateTemplate(), + did, + resource.getState(), + resource.getCreateTimestamp(), + resource.getStateTimestamp(), + toJson(resource.getDocument()), + did); + return StoreResult.success(); + } + return StoreResult.notFound(notFoundErrorMessage(did)); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public DidResource findById(String did) { + Objects.requireNonNull(did); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + var sql = statements.getFindByIdTemplate(); + return queryExecutor.single(connection, false, this::mapResultSet, sql, did); + } catch (Exception exception) { + throw new EdcPersistenceException(exception); + } + }); + } + + @Override + public Collection query(QuerySpec query) { + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + var sql = statements.createQuery(query); + return queryExecutor.query(connection, true, this::mapResultSet, sql.getQueryAsString(), sql.getParameters()).toList(); + } catch (Exception exception) { + throw new EdcPersistenceException(exception); + } + }); + } + + @Override + public StoreResult deleteById(String did) { + Objects.requireNonNull(did); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findById(did) != null) { + var stmt = statements.getDeleteByIdTemplate(); + queryExecutor.execute(connection, stmt, did); + return StoreResult.success(); + } + return StoreResult.notFound(notFoundErrorMessage(did)); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + private DidResource mapResultSet(ResultSet resultSet) throws Exception { + return DidResource.Builder.newInstance() + .did(resultSet.getString(statements.getIdColumn())) + .createTimestamp(resultSet.getLong(statements.getCreateTimestampColumn())) + .stateTimeStamp(resultSet.getLong(statements.getStateTimestampColumn())) + .document(fromJson(resultSet.getString(statements.getDidDocumentColumn()), DidDocument.class)) + .state(resultSet.getInt(statements.getStateColumn())) + .build(); + } +} diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreExtension.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreExtension.java new file mode 100644 index 000000000..8c6f50108 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreExtension.java @@ -0,0 +1,63 @@ +/* + * 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.did.store.sql; + +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.identityhub.did.store.sql.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.did.store.sql.SqlDidResourceStoreExtension.NAME; + +@Extension(value = NAME) +public class SqlDidResourceStoreExtension implements ServiceExtension { + public static final String NAME = "DID Resource SQL Store Extension"; + + @Setting(value = "Datasource name for the DidResource database", defaultValue = DataSourceRegistry.DEFAULT_DATASOURCE) + public static final String DATASOURCE_SETTING_NAME = "edc.datasource.didresource.name"; + @Inject + private DataSourceRegistry dataSourceRegistry; + @Inject + private TransactionContext transactionContext; + @Inject + private TypeManager typemanager; + @Inject + private QueryExecutor queryExecutor; + @Inject(required = false) + private DidResourceStatements statements; + + + @Provider + public DidResourceStore createSqlStore(ServiceExtensionContext context) { + return new SqlDidResourceStore(dataSourceRegistry, getDataSourceName(context), transactionContext, typemanager.getMapper(), + queryExecutor, getStatementImpl()); + } + + private DidResourceStatements 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/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidDocumentMapping.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidDocumentMapping.java new file mode 100644 index 000000000..cbe845c83 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidDocumentMapping.java @@ -0,0 +1,37 @@ +/* + * 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.did.store.sql.schema.postgres; + +import org.eclipse.edc.identityhub.did.store.sql.DidResourceStatements; +import org.eclipse.edc.sql.translation.JsonFieldMapping; +import org.eclipse.edc.sql.translation.TranslationMapping; + +/** + * Provides a mapping from the canonical format to SQL column names for a {@link org.eclipse.edc.iam.did.spi.document.DidDocument} + */ +public class DidDocumentMapping extends TranslationMapping { + + public static final String FIELD_ID = "id"; + public static final String FIELD_SERVICE = "service"; + public static final String FIELD_VERIFICATION_METHOD = "verificationMethod"; + public static final String FIELD_AUTHENTICATION = "authentication"; + + public DidDocumentMapping(DidResourceStatements statements) { + add(FIELD_ID, statements.getIdColumn()); + add(FIELD_SERVICE, new JsonFieldMapping(FIELD_SERVICE)); + add(FIELD_VERIFICATION_METHOD, new JsonFieldMapping(FIELD_VERIFICATION_METHOD)); + add(FIELD_AUTHENTICATION, FIELD_AUTHENTICATION); + } +} diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidResourceMapping.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidResourceMapping.java new file mode 100644 index 000000000..8dc334711 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidResourceMapping.java @@ -0,0 +1,39 @@ +/* + * 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.did.store.sql.schema.postgres; + +import org.eclipse.edc.identityhub.did.store.sql.DidResourceStatements; +import org.eclipse.edc.sql.translation.TranslationMapping; + + +/** + * Provides a mapping from the canonical format to SQL column names for a {@link org.eclipse.edc.identithub.did.spi.model.DidResource} + */ +public class DidResourceMapping extends TranslationMapping { + + public static final String FIELD_DID = "did"; + public static final String FIELD_STATE = "state"; + public static final String FIELD_CREATE_TIMESTAMP = "create_timestamp"; + public static final String FIELD_STATE_TIMESTAMP = "state_timestamp"; + public static final String FIELD_DOCUMENT = "document"; + + public DidResourceMapping(DidResourceStatements statements) { + add(FIELD_DID, statements.getIdColumn()); + add(FIELD_STATE, statements.getStateColumn()); + add(FIELD_CREATE_TIMESTAMP, statements.getCreateTimestampColumn()); + add(FIELD_STATE_TIMESTAMP, statements.getStateTimestampColumn()); + add(FIELD_DOCUMENT, new DidDocumentMapping(statements)); + } +} \ No newline at end of file diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/PostgresDialectStatements.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/PostgresDialectStatements.java new file mode 100644 index 000000000..8576d27b2 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/PostgresDialectStatements.java @@ -0,0 +1,52 @@ +/* + * 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.did.store.sql.schema.postgres; + +import org.eclipse.edc.identityhub.did.store.sql.BaseSqlDialectStatements; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.dialect.PostgresDialect; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +import static java.lang.String.format; +import static org.eclipse.edc.sql.dialect.PostgresDialect.getSelectFromJsonArrayTemplate; + +/** + * Postgres-specific specialization for creating queries based on Postgres JSON operators + */ +public class PostgresDialectStatements extends BaseSqlDialectStatements { + @Override + public String getFormatAsJsonOperator() { + return PostgresDialect.getJsonCastOperator(); + } + + @Override + public SqlQueryStatement createQuery(QuerySpec querySpec) { + if (querySpec.containsAnyLeftOperand("document.service")) { + var select = getSelectFromJsonArrayTemplate(getSelectStatement(), "%s -> '%s'".formatted(getDidDocumentColumn(), "service"), DidDocumentMapping.FIELD_SERVICE); + return new SqlQueryStatement(select, querySpec, new DidResourceMapping(this)); + } else if (querySpec.containsAnyLeftOperand("document.verificationMethod")) { + var select = getSelectFromJsonArrayTemplate(getSelectStatement(), "%s -> '%s'".formatted(getDidDocumentColumn(), "verificationMethod"), DidDocumentMapping.FIELD_VERIFICATION_METHOD); + return new SqlQueryStatement(select, querySpec, new DidResourceMapping(this)); + } else if (querySpec.containsAnyLeftOperand("document.authentication")) { + var select = getSelectFromJsonArrayTextTemplate(getSelectStatement(), "%s -> '%s'".formatted(getDidDocumentColumn(), "authentication"), DidDocumentMapping.FIELD_AUTHENTICATION); + return new SqlQueryStatement(select, querySpec, new DidResourceMapping(this)); + } + return super.createQuery(querySpec); + } + + private String getSelectFromJsonArrayTextTemplate(String selectStatement, String jsonPath, String aliasName) { + return format("%s, json_array_elements_text(%s) as %s", selectStatement, jsonPath, aliasName); + } +} diff --git a/core/identity-hub-did-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/core/identity-hub-did-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..e327798fc --- /dev/null +++ b/core/identity-hub-did-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.did.store.sql.SqlDidResourceStoreExtension \ No newline at end of file diff --git a/core/identity-hub-did-store-sql/src/main/resources/did.json b/core/identity-hub-did-store-sql/src/main/resources/did.json new file mode 100644 index 000000000..511d12ef5 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/resources/did.json @@ -0,0 +1,57 @@ +{ + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "alsoKnownAs": { + "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", + "@type": "@id" + }, + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + }, + "service": { + "@id": "https://www.w3.org/ns/did#service", + "@type": "@id", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "serviceEndpoint": { + "@id": "https://www.w3.org/ns/did#serviceEndpoint", + "@type": "@id" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } +} \ No newline at end of file diff --git a/core/identity-hub-did-store-sql/src/test/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreTest.java b/core/identity-hub-did-store-sql/src/test/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreTest.java new file mode 100644 index 000000000..81530a71f --- /dev/null +++ b/core/identity-hub-did-store-sql/src/test/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreTest.java @@ -0,0 +1,58 @@ +/* + * 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.did.store.sql; + +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.identityhub.did.store.sql.schema.postgres.PostgresDialectStatements; +import org.eclipse.edc.identityhub.did.store.test.DidResourceStoreTestBase; +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 SqlDidResourceStoreTest extends DidResourceStoreTestBase { + + private final DidResourceStatements statements = new PostgresDialectStatements(); + private SqlDidResourceStore store; + + @BeforeEach + void setup(PostgresqlStoreSetupExtension extension, QueryExecutor queryExecutor) throws IOException { + var typeManager = new TypeManager(); + store = new SqlDidResourceStore(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.getDidResourceTableName() + " CASCADE"); + } + + @Override + protected DidResourceStore getStore() { + return store; + } +} \ No newline at end of file diff --git a/core/identity-hub-did-store-sql/src/test/resources/did.json b/core/identity-hub-did-store-sql/src/test/resources/did.json new file mode 100644 index 000000000..511d12ef5 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/test/resources/did.json @@ -0,0 +1,57 @@ +{ + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "alsoKnownAs": { + "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", + "@type": "@id" + }, + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + }, + "service": { + "@id": "https://www.w3.org/ns/did#service", + "@type": "@id", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "serviceEndpoint": { + "@id": "https://www.w3.org/ns/did#serviceEndpoint", + "@type": "@id" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } +} \ No newline at end of file diff --git a/core/identity-hub-did/build.gradle.kts b/core/identity-hub-did/build.gradle.kts index 4d4943e90..41f9be43d 100644 --- a/core/identity-hub-did/build.gradle.kts +++ b/core/identity-hub-did/build.gradle.kts @@ -4,9 +4,12 @@ plugins { dependencies { api(project(":spi:identity-hub-spi")) + api(project(":spi:identity-hub-did-spi")) + + implementation(libs.edc.core.connector) // for the reflection-based query resolver testImplementation(libs.edc.junit) testImplementation(libs.edc.ext.jsonld) testImplementation(testFixtures(project(":spi:identity-hub-spi"))) - testImplementation(libs.edc.identity.did.crypto) // EC private key wrapper + testImplementation(testFixtures(project(":spi:identity-hub-did-spi"))) } diff --git a/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/DidDefaultServicesExtension.java b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/DidDefaultServicesExtension.java new file mode 100644 index 000000000..d8d31b5c0 --- /dev/null +++ b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/DidDefaultServicesExtension.java @@ -0,0 +1,32 @@ +/* + * 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.did.defaults; + +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.system.ServiceExtension; + +import static org.eclipse.edc.identityhub.did.defaults.DidDefaultServicesExtension.NAME; + +@Extension(value = NAME) +public class DidDefaultServicesExtension implements ServiceExtension { + public static final String NAME = "DID Default Services Extension"; + + @Provider(isDefault = true) + public DidResourceStore createInMemoryDidResourceStore() { + return new InMemoryDidResourceStore(); + } +} diff --git a/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStore.java b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStore.java new file mode 100644 index 000000000..95bf5568a --- /dev/null +++ b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStore.java @@ -0,0 +1,101 @@ +/* + * 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.did.defaults; + +import org.eclipse.edc.connector.core.store.ReflectionBasedQueryResolver; +import org.eclipse.edc.identithub.did.spi.model.DidResource; +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.spi.query.QueryResolver; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Stores {@link DidResource} objects in an in-memory map. This implementation is thread-safe + */ +public class InMemoryDidResourceStore implements DidResourceStore { + private final Map store = new HashMap<>(); + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final QueryResolver queryResolver = new ReflectionBasedQueryResolver<>(DidResource.class); + + + @Override + public StoreResult save(DidResource resource) { + lock.writeLock().lock(); + try { + var did = resource.getDid(); + if (store.containsKey(did)) { + return StoreResult.alreadyExists(alreadyExistsErrorMessage(did)); + } + store.put(did, resource); + return StoreResult.success(); + } finally { + lock.writeLock().unlock(); + } + } + + + @Override + public StoreResult update(DidResource resource) { + lock.writeLock().lock(); + try { + var did = resource.getDid(); + if (!store.containsKey(did)) { + return StoreResult.notFound(notFoundErrorMessage(did)); + } + store.put(did, resource); + return StoreResult.success(); + } finally { + lock.writeLock().unlock(); + } + } + + + @Override + public DidResource findById(String did) { + lock.readLock().lock(); + try { + return store.get(did); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection query(QuerySpec query) { + lock.readLock().lock(); + try { + return queryResolver.query(store.values().stream(), query).toList(); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public StoreResult deleteById(String did) { + lock.writeLock().lock(); + try { + return store.remove(did) == null + ? StoreResult.notFound(notFoundErrorMessage(did)) + : StoreResult.success(); + } finally { + lock.writeLock().unlock(); + } + } +} diff --git a/core/identity-hub-did/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/core/identity-hub-did/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index 308271903..58586ce19 100644 --- a/core/identity-hub-did/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/core/identity-hub-did/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -12,4 +12,5 @@ # # +org.eclipse.edc.identityhub.did.defaults.DidDefaultServicesExtension org.eclipse.edc.identityhub.did.DidServicesExtension \ No newline at end of file diff --git a/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStoreTest.java b/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStoreTest.java new file mode 100644 index 000000000..a3c397271 --- /dev/null +++ b/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStoreTest.java @@ -0,0 +1,28 @@ +/* + * 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.did.defaults; + +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.identityhub.did.store.test.DidResourceStoreTestBase; + +class InMemoryDidResourceStoreTest extends DidResourceStoreTestBase { + + private final DidResourceStore store = new InMemoryDidResourceStore(); + + @Override + protected DidResourceStore getStore() { + return store; + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 861367f09..19a88be50 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,6 +74,8 @@ postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } restAssured = { module = "io.rest-assured:rest-assured", version.ref = "restAssured" } swagger-jaxrs = { module = "io.swagger.core.v3:swagger-jaxrs2-jakarta", version.ref = "swagger" } testcontainers-junit = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } +testcontainers-postgres = { module = "org.testcontainers:postgres", version.ref = "testcontainers" } + [bundles] connector = ["edc-boot", "edc-core-connector", "edc-ext-http", "edc-ext-observability", "edc-ext-jsonld"] diff --git a/settings.gradle.kts b/settings.gradle.kts index 2f9eddaef..1df621b7c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,6 +31,7 @@ include(":spi:identity-hub-did-spi") include(":core:identity-hub-api") include(":core:identity-hub-credentials") include(":core:identity-hub-did") +include(":core:identity-hub-did-store-sql") // extension modules include(":extensions:cryptography:public-key-provider") diff --git a/spi/identity-hub-did-spi/build.gradle.kts b/spi/identity-hub-did-spi/build.gradle.kts index 133467508..cc0e3bf1f 100644 --- a/spi/identity-hub-did-spi/build.gradle.kts +++ b/spi/identity-hub-did-spi/build.gradle.kts @@ -18,10 +18,12 @@ plugins { `maven-publish` } -val swagger: String by project - dependencies { - api(libs.edc.spi.identitytrust) - implementation(libs.edc.spi.identity.did) + api(libs.edc.spi.identity.did) + + testFixturesImplementation(libs.edc.spi.identity.did) + testFixturesImplementation(libs.junit.jupiter.api) + testFixturesImplementation(libs.edc.junit) + testFixturesImplementation(libs.assertj) } diff --git a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java index 80d66e6c8..92efecddc 100644 --- a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java +++ b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java @@ -27,7 +27,7 @@ public class DidResource { @JsonIgnore private Clock clock = Clock.systemUTC(); private String did; - private DidState state = DidState.INITIAL; + private int state = DidState.INITIAL.code(); private long stateTimestamp; private long createTimestamp; private DidDocument document; @@ -41,10 +41,14 @@ public String getDid() { return did; } - public DidState getState() { + public int getState() { return state; } + public DidState getStateAsEnum() { + return DidState.from(state); + } + public long getStateTimestamp() { return stateTimestamp; } @@ -70,7 +74,7 @@ public Builder did(String did) { } public Builder state(DidState state) { - this.resource.state = state; + this.resource.state = state.code(); return this; } @@ -105,6 +109,11 @@ public DidResource build() { return resource; } + public Builder state(int code) { + this.resource.state = code; + return this; + } + public static Builder newInstance() { return new Builder(); } diff --git a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidState.java b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidState.java index a15e16cab..d7d8efbd6 100644 --- a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidState.java +++ b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidState.java @@ -14,6 +14,8 @@ package org.eclipse.edc.identithub.did.spi.model; +import java.util.Arrays; + /** * The DidState enum represents the state of a DID resource in the internal store. */ @@ -45,4 +47,8 @@ public enum DidState { public int code() { return code; } + + public static DidState from(int code) { + return Arrays.stream(values()).filter(tps -> tps.code == code).findFirst().orElse(null); + } } diff --git a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/store/DidResourceStore.java b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/store/DidResourceStore.java index 8541d32ad..02d96f5e8 100644 --- a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/store/DidResourceStore.java +++ b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/store/DidResourceStore.java @@ -16,8 +16,10 @@ import org.eclipse.edc.identithub.did.spi.model.DidResource; import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.StoreResult; +import java.util.Collection; import java.util.List; /** @@ -51,14 +53,14 @@ public interface DidResourceStore { * @param did The DID to search for. * @return The {@link DidResource} object found in the store, or null if no matching object is found. */ - DidResource getById(String did); + DidResource findById(String did); /** * Retrieves all {@link DidResource} objects from the store. * * @return A {@link List} containing {@link DidResource} objects retrieved from the store. */ - List getAll(); + Collection query(QuerySpec query); /** * Deletes a {@link DidResource} object from the store with the specified DID. If the specified DID document does not @@ -69,4 +71,11 @@ public interface DidResourceStore { */ StoreResult deleteById(String did); + default String alreadyExistsErrorMessage(String did) { + return "A DidResource with ID %s already exists.".formatted(did); + } + + default String notFoundErrorMessage(String did) { + return "A DidResource with ID %s was not found.".formatted(did); + } } diff --git a/spi/identity-hub-did-spi/src/testFixtures/java/org/eclipse/edc/identityhub/did/store/test/DidResourceStoreTestBase.java b/spi/identity-hub-did-spi/src/testFixtures/java/org/eclipse/edc/identityhub/did/store/test/DidResourceStoreTestBase.java new file mode 100644 index 000000000..7937835e0 --- /dev/null +++ b/spi/identity-hub-did-spi/src/testFixtures/java/org/eclipse/edc/identityhub/did/store/test/DidResourceStoreTestBase.java @@ -0,0 +1,269 @@ +/* + * 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.did.store.test; + +import org.assertj.core.api.Assertions; +import org.eclipse.edc.iam.did.spi.document.DidDocument; +import org.eclipse.edc.iam.did.spi.document.Service; +import org.eclipse.edc.iam.did.spi.document.VerificationMethod; +import org.eclipse.edc.identithub.did.spi.model.DidResource; +import org.eclipse.edc.identithub.did.spi.model.DidState; +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.spi.message.Range; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.query.SortOrder; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import static java.util.stream.IntStream.range; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; + +/** + * Base test class for DidResourceStore implementations. + */ +public abstract class DidResourceStoreTestBase { + + public static final String DID = "did:web:test"; + + @Test + void save() { + var didResource = createDidResource(DID).build(); + assertThat(getStore().save(didResource)).isSucceeded(); + } + + @Test + void save_alreadyExists() { + var didResource = createDidResource(DID).build(); + assertThat(getStore().save(didResource)).isSucceeded(); + assertThat(getStore().save(didResource)).isFailed().detail().isEqualTo("A DidResource with ID %s already exists.".formatted(DID)); + } + + @Test + void update() { + var builder = createDidResource(DID); + var didResource = builder.build(); + getStore().save(didResource); + + var didResource2 = builder.state(DidState.GENERATED).build(); + assertThat(getStore().update(didResource2)).isSucceeded(); + var fromDb = getStore().findById(DID); + Assertions.assertThat(fromDb).usingRecursiveComparison().isEqualTo(didResource2); + } + + @Test + void update_notExists() { + assertThat(getStore().update(createDidResource(DID).build())).isFailed() + .detail().isEqualTo("A DidResource with ID %s was not found.".formatted(DID)); + } + + @Test + void findById() { + var didResource = createDidResource(DID).build(); + getStore().save(didResource); + + Assertions.assertThat(getStore().findById(DID)) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(didResource); + } + + @Test + void findById_notExists() { + Assertions.assertThat(getStore().findById("did:web:notexist")).isNull(); + } + + @Test + void query() { + var dids = range(0, 10) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList(); + dids.forEach(getStore()::save); + + Assertions.assertThat(getStore().query(QuerySpec.none())) + .hasSize(10) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrderElementsOf(dids); + } + + @Test + void query_withPage() { + var dids = range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList(); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().range(new Range(25, 35)).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(10) + .usingRecursiveFieldByFieldElementComparator() + .containsAnyElementsOf(dids); + } + + @Test + void query_withSorting() { + var dids = range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList(); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().sortOrder(SortOrder.DESC).sortField("did").build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(50) + .usingRecursiveFieldByFieldElementComparator() + .containsAnyElementsOf(dids) + .extracting(DidResource::getDid) + .isSortedAccordingTo(Comparator.reverseOrder()); + } + + @Test + void query_bySimpleProperty() { + var dids = new ArrayList<>(range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList()); + + var expected = createDidResource(DID + "69").state(DidState.PUBLISHED).build(); + dids.add(expected); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().filter(new Criterion("state", "=", DidState.PUBLISHED.code())).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(expected); + } + + @Test + void query_byComplexProperty_service() { + var dids = new ArrayList<>(range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList()); + + var expected = createDidResource(DID + "69") + .document(DidDocument.Builder.newInstance() + .id(DID + "69") + .service(List.of(new Service("test-service", "foo-type", "https://foo.bar"))) + .build()) + .build(); + + dids.add(expected); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().filter(new Criterion("document.service.type", "=", "foo-type")).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(expected); + } + + @Test + void query_byComplexProperty_verificationMethod() { + var dids = new ArrayList<>(range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList()); + + var expected = createDidResource(DID + "69") + .document(DidDocument.Builder.newInstance() + .id(DID + "69") + .service(List.of(new Service("test-service", "foo-type", "https://foo.bar"))) + .verificationMethod(List.of(VerificationMethod.Builder.newInstance().id("vm-1").type("test-type").publicKeyMultibase("asdfl;aksdflaskdfj").build())) + .build()) + .build(); + + dids.add(expected); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().filter(new Criterion("document.verificationMethod.type", "=", "test-type")).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(expected); + } + + @Test + void query_byComplexProperty_authentication() { + var dids = new ArrayList<>(range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList()); + + var expected = createDidResource(DID + "69") + .document(DidDocument.Builder.newInstance() + .id(DID + "69") + .service(List.of(new Service("test-service", "foo-type", "https://foo.bar"))) + .verificationMethod(List.of(VerificationMethod.Builder.newInstance().id("vm-1").type("test-type").publicKeyMultibase("asdfl;aksdflaskdfj").build())) + .authentication(List.of("auth1", "auth2")) + .build()) + .build(); + + dids.add(expected); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().filter(new Criterion("document.authentication", "=", "auth1")).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(expected); + } + + @Test + void query_byComplexProperty_id() { + var dids = new ArrayList<>(range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList()); + + var expected = createDidResource(DID + "69") + .document(DidDocument.Builder.newInstance() + .id(DID + "69") + .build()) + .build(); + + dids.add(expected); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().filter(new Criterion("document.id", "=", DID + "69")).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(expected); + } + + @Test + void deleteById() { + var didResource = createDidResource(DID).build(); + getStore().save(didResource); + assertThat(getStore().deleteById(DID)).isSucceeded(); + Assertions.assertThat(getStore().query(QuerySpec.none())).isEmpty(); + } + + @Test + void deleteById_notExist() { + assertThat(getStore().deleteById(DID)).isFailed() + .detail().isEqualTo("A DidResource with ID %s was not found.".formatted(DID)); + } + + protected abstract DidResourceStore getStore(); + + private DidResource.Builder createDidResource(String did) { + return DidResource.Builder.newInstance() + .did(did) + .document(DidDocument.Builder.newInstance() + .id(did) + .build()) + .state(DidState.INITIAL); + } +}