diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index 58c5a1676..c42a7f201 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -96,6 +96,20 @@ jobs: with: command: ./gradlew compileJava compileTestJava test -DincludeTags="ComponentTest,ApiTest,EndToEndTest" + Postgresql-Integration-Tests: + runs-on: ubuntu-latest + env: + JACOCO: true + + steps: + - uses: actions/checkout@v4 + - uses: eclipse-edc/.github/.github/actions/setup-build@main + + - name: Postgresql Tests + uses: eclipse-edc/.github/.github/actions/run-tests@main + with: + command: ./gradlew test -DincludeTags="PostgresqlIntegrationTest" + Upload-Coverage-Report-To-Codecov: needs: - Test diff --git a/DEPENDENCIES b/DEPENDENCIES index e1d44945e..b1c51e740 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -10,51 +10,48 @@ maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.14.0, Apache maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.14.1, Apache-2.0, approved, #5303 maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.16.2, Apache-2.0, approved, #11606 maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.17.0, Apache-2.0, approved, #13672 -maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.17.1, Apache-2.0, approved, #13672 +maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.17.2, Apache-2.0, approved, #13672 maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.14.1, Apache-2.0 AND MIT, approved, #4303 maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.16.2, Apache-2.0 AND MIT, approved, #11602 -maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.17.1, , approved, #13665 +maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.17.2, , approved, #13665 maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.11.0, Apache-2.0, approved, CQ23093 maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.14.0, Apache-2.0, approved, #4105 maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.14.1, Apache-2.0, approved, #15232 maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.16.2, Apache-2.0, approved, #11605 maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.17.0, Apache-2.0, approved, #13671 -maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.17.1, Apache-2.0, approved, #13671 +maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.17.2, Apache-2.0, approved, #13671 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.16.2, Apache-2.0, approved, #11855 -maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.17.1, Apache-2.0, approved, #13669 -maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jakarta-jsonp/2.17.1, Apache-2.0, approved, #14161 +maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.17.2, Apache-2.0, approved, #13669 +maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jakarta-jsonp/2.17.2, Apache-2.0, approved, #14161 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.16.2, Apache-2.0, approved, #11853 -maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.17.1, Apache-2.0, approved, #14160 -maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-base/2.17.1, Apache-2.0, approved, #14194 +maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.17.2, Apache-2.0, approved, #14160 +maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-base/2.17.2, Apache-2.0, approved, #14194 maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.16.2, Apache-2.0, approved, #11858 -maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.17.1, Apache-2.0, approved, #14195 +maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.17.2, Apache-2.0, approved, #14195 maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.17.0, Apache-2.0, approved, #13668 -maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.17.1, Apache-2.0, approved, #13668 +maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.17.2, Apache-2.0, approved, #13668 maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.16.2, Apache-2.0, approved, #11852 -maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.17.1, Apache-2.0, approved, #14162 +maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.17.2, Apache-2.0, approved, #14162 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.4, Apache-2.0, approved, #10346 maven/mavencentral/com.github.docker-java/docker-java-api/3.3.6, Apache-2.0, approved, #10346 -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, #15251 maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.6, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #15251 -maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.4, Apache-2.0, approved, #7942 maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.6, Apache-2.0, approved, #7942 -maven/mavencentral/com.github.java-json-tools/btf/1.3, Apache-2.0 AND GPL-1.0-or-later AND LGPL-3.0-only AND Apache-2.0 AND LGPL-3.0-only, restricted, #15201 +maven/mavencentral/com.github.java-json-tools/btf/1.3, Apache-2.0 OR LGPL-3.0-only, approved, #15201 maven/mavencentral/com.github.java-json-tools/jackson-coreutils-equivalence/1.0, LGPL-3.0 OR Apache-2.0, approved, clearlydefined maven/mavencentral/com.github.java-json-tools/jackson-coreutils/2.0, Apache-2.0 OR LGPL-3.0-or-later, approved, #15186 maven/mavencentral/com.github.java-json-tools/json-patch/1.13, Apache-2.0 OR LGPL-3.0-or-later, approved, CQ23929 -maven/mavencentral/com.github.java-json-tools/json-schema-core/1.2.14, Apache-2.0 AND LGPL-2.1-or-later AND LGPL-3.0-only AND (Apache-2.0 AND GPL-1.0-or-later AND LGPL-3.0-only) AND Apache-2.0 AND LGPL-3.0-only, restricted, #15282 -maven/mavencentral/com.github.java-json-tools/json-schema-validator/2.2.14, Apache-2.0 OR LGPL-3.0-or-later, approved, CQ20779 -maven/mavencentral/com.github.java-json-tools/msg-simple/1.2, Apache-2.0 AND LGPL-2.1-or-later AND LGPL-3.0-only AND (Apache-2.0 AND GPL-1.0-or-later AND LGPL-3.0-only) AND Apache-2.0 AND LGPL-3.0-only, restricted, #15239 -maven/mavencentral/com.github.java-json-tools/uri-template/0.10, Apache-2.0 AND LGPL-3.0-only AND (Apache-2.0 AND GPL-1.0-or-later AND LGPL-3.0-only), restricted, #15288 +maven/mavencentral/com.github.java-json-tools/json-schema-core/1.2.14, Apache-2.0 OR LGPL-3.0-or-later, approved, #15282 +maven/mavencentral/com.github.java-json-tools/json-schema-validator/2.2.14, Apache-2.0 OR LGPL-3.0-or-later, approved, #15263 +maven/mavencentral/com.github.java-json-tools/msg-simple/1.2, Apache-2.0 OR LGPL-3.0-or-later, approved, #15239 +maven/mavencentral/com.github.java-json-tools/uri-template/0.10, , approved, #15288 maven/mavencentral/com.google.code.findbugs/jsr305/2.0.1, BSD-3-Clause AND CC-BY-2.5 AND LGPL-2.1+, approved, CQ13390 maven/mavencentral/com.google.code.findbugs/jsr305/3.0.2, CC-BY-2.5, approved, #15220 maven/mavencentral/com.google.code.gson/gson/2.10.1, Apache-2.0, approved, #6159 -maven/mavencentral/com.google.crypto.tink/tink/1.12.0, Apache-2.0, approved, #12041 maven/mavencentral/com.google.crypto.tink/tink/1.13.0, Apache-2.0, approved, #14502 +maven/mavencentral/com.google.crypto.tink/tink/1.14.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.errorprone/error_prone_annotations/2.11.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.errorprone/error_prone_annotations/2.22.0, Apache-2.0, approved, #10661 maven/mavencentral/com.google.errorprone/error_prone_annotations/2.26.1, Apache-2.0, approved, #13657 @@ -67,8 +64,8 @@ maven/mavencentral/com.google.guava/guava/31.1-jre, Apache-2.0, approved, clearl maven/mavencentral/com.google.guava/guava/33.2.0-jre, Apache-2.0 AND CC0-1.0 AND (Apache-2.0 AND CC-PDDC), approved, #14607 maven/mavencentral/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava, Apache-2.0, approved, CQ22657 maven/mavencentral/com.google.j2objc/j2objc-annotations/1.3, Apache-2.0, approved, CQ21195 -maven/mavencentral/com.google.protobuf/protobuf-java/3.24.3, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.protobuf/protobuf-java/3.25.1, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/com.google.protobuf/protobuf-java/4.27.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.googlecode.libphonenumber/libphonenumber/8.11.1, Apache-2.0, approved, clearlydefined maven/mavencentral/com.jayway.jsonpath/json-path/2.7.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.jcraft/jzlib/1.1.3, BSD-2-Clause, approved, CQ6218 @@ -77,7 +74,7 @@ maven/mavencentral/com.networknt/json-schema-validator/1.0.76, Apache-2.0, appro maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.28, Apache-2.0, approved, clearlydefined maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.40, Apache-2.0, approved, #15156 maven/mavencentral/com.puppycrawl.tools/checkstyle/10.17.0, LGPL-2.1-or-later AND (Apache-2.0 AND LGPL-2.1-or-later) AND Apache-2.0, approved, #15077 -maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause, approved, clearlydefined +maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause AND BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.12.0, Apache-2.0, approved, #11159 maven/mavencentral/com.squareup.okhttp3/okhttp/4.12.0, Apache-2.0, approved, #15227 maven/mavencentral/com.squareup.okhttp3/okhttp/4.9.3, Apache-2.0 AND MPL-2.0, approved, #3225 @@ -121,23 +118,28 @@ maven/mavencentral/io.prometheus/simpleclient_httpserver/0.16.0, Apache-2.0, app maven/mavencentral/io.prometheus/simpleclient_tracer_common/0.16.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.prometheus/simpleclient_tracer_otel/0.16.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.prometheus/simpleclient_tracer_otel_agent/0.16.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.rest-assured/json-path/5.4.0, Apache-2.0, approved, #12042 -maven/mavencentral/io.rest-assured/rest-assured-common/5.4.0, Apache-2.0, approved, #12039 -maven/mavencentral/io.rest-assured/rest-assured/5.4.0, Apache-2.0, approved, #15190 -maven/mavencentral/io.rest-assured/xml-path/5.4.0, Apache-2.0, approved, #12038 +maven/mavencentral/io.rest-assured/json-path/5.5.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.rest-assured/rest-assured-common/5.5.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.rest-assured/rest-assured/5.5.0, Apache-2.0, approved, #15676 +maven/mavencentral/io.rest-assured/xml-path/5.5.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.setl/rdf-urdna/1.1, Apache-2.0, approved, clearlydefined maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.2.21, Apache-2.0, approved, #5947 -maven/mavencentral/io.swagger.core.v3/swagger-annotations/2.2.21, Apache-2.0, approved, #11362 +maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.2.22, Apache-2.0, approved, #5947 +maven/mavencentral/io.swagger.core.v3/swagger-annotations/2.2.22, Apache-2.0, approved, #11362 maven/mavencentral/io.swagger.core.v3/swagger-annotations/2.2.8, Apache-2.0, approved, #11362 maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.21, Apache-2.0, approved, #5929 -maven/mavencentral/io.swagger.core.v3/swagger-core/2.2.21, Apache-2.0, approved, #9265 +maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.22, Apache-2.0, approved, #5929 +maven/mavencentral/io.swagger.core.v3/swagger-core/2.2.22, Apache-2.0, approved, #9265 maven/mavencentral/io.swagger.core.v3/swagger-core/2.2.8, Apache-2.0, approved, #9265 maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.2.21, Apache-2.0, approved, #11475 -maven/mavencentral/io.swagger.core.v3/swagger-integration/2.2.21, Apache-2.0, approved, #10352 +maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.2.22, Apache-2.0, approved, #11475 +maven/mavencentral/io.swagger.core.v3/swagger-integration/2.2.22, Apache-2.0, approved, #10352 maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.2.21, Apache-2.0, approved, #11477 -maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2/2.2.21, Apache-2.0, approved, #9814 +maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.2.22, Apache-2.0, approved, #11477 +maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2/2.2.22, Apache-2.0, approved, #9814 maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.21, Apache-2.0, approved, #5919 -maven/mavencentral/io.swagger.core.v3/swagger-models/2.2.21, Apache-2.0, approved, #10353 +maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.22, Apache-2.0, approved, #5919 +maven/mavencentral/io.swagger.core.v3/swagger-models/2.2.22, Apache-2.0, approved, #10353 maven/mavencentral/io.swagger.core.v3/swagger-models/2.2.8, Apache-2.0, approved, #10353 maven/mavencentral/io.swagger.parser.v3/swagger-parser-core/2.1.10, Apache-2.0, approved, #11478 maven/mavencentral/io.swagger.parser.v3/swagger-parser-v2-converter/2.1.10, Apache-2.0, approved, #9330 @@ -174,6 +176,7 @@ maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.15, Apache-2.0, approved, maven/mavencentral/net.bytebuddy/byte-buddy/1.14.1, Apache-2.0 AND BSD-3-Clause, approved, #7163 maven/mavencentral/net.bytebuddy/byte-buddy/1.14.15, Apache-2.0 AND BSD-3-Clause, approved, #7163 maven/mavencentral/net.bytebuddy/byte-buddy/1.14.16, Apache-2.0 AND BSD-3-Clause, approved, #7163 +maven/mavencentral/net.bytebuddy/byte-buddy/1.14.18, Apache-2.0 AND BSD-3-Clause, approved, #7163 maven/mavencentral/net.java.dev.jna/jna/5.13.0, Apache-2.0 AND LGPL-2.1-or-later, approved, #15196 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 @@ -189,12 +192,13 @@ maven/mavencentral/org.apache.commons/commons-lang3/3.12.0, Apache-2.0, approved maven/mavencentral/org.apache.commons/commons-lang3/3.14.0, Apache-2.0, approved, #11677 maven/mavencentral/org.apache.commons/commons-lang3/3.7, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apache.commons/commons-lang3/3.8.1, Apache-2.0, approved, #815 +maven/mavencentral/org.apache.commons/commons-pool2/2.12.0, Apache-2.0 AND LicenseRef-Public-Domain, approved, #10843 maven/mavencentral/org.apache.commons/commons-text/1.10.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apache.commons/commons-text/1.3, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.apache.groovy/groovy-bom/4.0.16, Apache-2.0, approved, #9266 -maven/mavencentral/org.apache.groovy/groovy-json/4.0.16, Apache-2.0, approved, #7411 -maven/mavencentral/org.apache.groovy/groovy-xml/4.0.16, Apache-2.0, approved, #10179 -maven/mavencentral/org.apache.groovy/groovy/4.0.16, Apache-2.0 AND BSD-3-Clause AND MIT, approved, #1742 +maven/mavencentral/org.apache.groovy/groovy-bom/4.0.22, Apache-2.0, approved, #9266 +maven/mavencentral/org.apache.groovy/groovy-json/4.0.22, Apache-2.0, approved, #7411 +maven/mavencentral/org.apache.groovy/groovy-xml/4.0.22, Apache-2.0, approved, #10179 +maven/mavencentral/org.apache.groovy/groovy/4.0.22, Apache-2.0 AND BSD-3-Clause AND MIT, approved, #1742 maven/mavencentral/org.apache.httpcomponents.client5/httpclient5/5.1.3, Apache-2.0, approved, #6276 maven/mavencentral/org.apache.httpcomponents.core5/httpcore5-h2/5.1.3, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apache.httpcomponents.core5/httpcore5/5.1.3, Apache-2.0, approved, clearlydefined @@ -208,10 +212,11 @@ maven/mavencentral/org.apache.maven.doxia/doxia-module-xdoc/1.12.0, Apache-2.0, maven/mavencentral/org.apache.maven.doxia/doxia-sink-api/1.12.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apache.velocity.tools/velocity-tools-generic/3.1, Apache-2.0, approved, #9331 maven/mavencentral/org.apache.velocity/velocity-engine-core/2.3, Apache-2.0, approved, #2478 -maven/mavencentral/org.apache.velocity/velocity-engine-scripting/2.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.apache.velocity/velocity-engine-scripting/2.3, Apache-2.0, restricted, clearlydefined maven/mavencentral/org.apache.xbean/xbean-reflect/3.7, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apiguardian/apiguardian-api/1.1.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.assertj/assertj-core/3.26.0, Apache-2.0, approved, #14886 +maven/mavencentral/org.assertj/assertj-core/3.26.3, Apache-2.0, approved, #14886 maven/mavencentral/org.awaitility/awaitility/4.2.1, Apache-2.0, approved, #14178 maven/mavencentral/org.bouncycastle/bcpkix-jdk18on/1.72, MIT, approved, #3789 maven/mavencentral/org.bouncycastle/bcpkix-jdk18on/1.78.1, MIT, approved, #14434 @@ -276,11 +281,13 @@ maven/mavencentral/org.eclipse.edc/policy-spi/0.8.1-SNAPSHOT, Apache-2.0, approv maven/mavencentral/org.eclipse.edc/query-lib/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/runtime-metamodel/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/sql-core/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/sql-pool-apache-commons/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/state-machine-lib/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/store-lib/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/token-core/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/token-spi/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transaction-datasource-spi/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/transaction-local/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transaction-spi/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transfer-spi/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transform-lib/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc @@ -293,26 +300,26 @@ maven/mavencentral/org.eclipse.edc/verifiable-credentials/0.8.1-SNAPSHOT, Apache maven/mavencentral/org.eclipse.edc/web-spi/0.8.1-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.jetty.toolchain/jetty-jakarta-servlet-api/5.0.2, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.eclipse.jetty.toolchain/jetty-jakarta-websocket-api/2.0.0, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.websocket/websocket-core-client/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.websocket/websocket-core-common/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.websocket/websocket-core-server/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.websocket/websocket-jakarta-client/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.websocket/websocket-jakarta-common/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.websocket/websocket-jakarta-server/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.websocket/websocket-servlet/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-alpn-client/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-annotations/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-client/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-http/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-io/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-jndi/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-plus/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-security/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-server/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-servlet/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-util/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-webapp/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-xml/11.0.21, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.websocket/websocket-core-client/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.websocket/websocket-core-common/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.websocket/websocket-core-server/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.websocket/websocket-jakarta-client/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.websocket/websocket-jakarta-common/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.websocket/websocket-jakarta-server/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.websocket/websocket-servlet/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-alpn-client/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-annotations/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-client/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-http/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-io/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-jndi/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-plus/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-security/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-server/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-servlet/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-util/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-webapp/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-xml/11.0.22, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.eclipse.parsson/parsson/1.1.6, EPL-2.0, approved, ee4j.parsson maven/mavencentral/org.glassfish.hk2.external/aopalliance-repackaged/3.0.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish maven/mavencentral/org.glassfish.hk2/hk2-api/3.0.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish @@ -348,21 +355,12 @@ 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.1.0, Apache-2.0, approved, 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-api/5.10.2, EPL-2.0, approved, #9714 maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.10.3, 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-engine/5.10.3, EPL-2.0, approved, #9711 -maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.10.1, EPL-2.0, approved, #15304 maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.10.3, EPL-2.0, approved, #15250 -maven/mavencentral/org.junit.platform/junit-platform-commons/1.10.1, EPL-2.0, approved, #9715 -maven/mavencentral/org.junit.platform/junit-platform-commons/1.10.2, EPL-2.0, approved, #9715 maven/mavencentral/org.junit.platform/junit-platform-commons/1.10.3, EPL-2.0, approved, #9715 -maven/mavencentral/org.junit.platform/junit-platform-engine/1.10.1, EPL-2.0, approved, #9709 maven/mavencentral/org.junit.platform/junit-platform-engine/1.10.3, EPL-2.0, approved, #9709 maven/mavencentral/org.junit.platform/junit-platform-launcher/1.10.3, EPL-2.0, approved, #15216 -maven/mavencentral/org.junit/junit-bom/5.10.1, EPL-2.0, approved, #9844 -maven/mavencentral/org.junit/junit-bom/5.10.2, EPL-2.0, approved, #9844 maven/mavencentral/org.junit/junit-bom/5.10.3, EPL-2.0, approved, #9844 maven/mavencentral/org.junit/junit-bom/5.9.2, EPL-2.0, approved, #4711 maven/mavencentral/org.jvnet.mimepull/mimepull/1.9.15, CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0, approved, CQ21484 @@ -394,11 +392,9 @@ 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/database-commons/1.19.8, Apache-2.0, approved, #10345 maven/mavencentral/org.testcontainers/jdbc/1.19.8, Apache-2.0, approved, #10348 -maven/mavencentral/org.testcontainers/junit-jupiter/1.19.3, MIT, approved, #10344 maven/mavencentral/org.testcontainers/junit-jupiter/1.19.8, MIT, approved, #10344 maven/mavencentral/org.testcontainers/postgresql/1.19.8, MIT, approved, #10350 -maven/mavencentral/org.testcontainers/testcontainers/1.19.3, Apache-2.0 AND MIT, approved, #10347 -maven/mavencentral/org.testcontainers/testcontainers/1.19.8, Apache-2.0 AND MIT, approved, #10347 +maven/mavencentral/org.testcontainers/testcontainers/1.19.8, MIT, approved, #15203 maven/mavencentral/org.xmlresolver/xmlresolver/5.2.2, Apache-2.0, approved, clearlydefined 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/e2e-tests/api-tests/build.gradle.kts b/e2e-tests/api-tests/build.gradle.kts index 691b573ed..235af7af4 100644 --- a/e2e-tests/api-tests/build.gradle.kts +++ b/e2e-tests/api-tests/build.gradle.kts @@ -11,10 +11,15 @@ dependencies { testImplementation(libs.restAssured) testImplementation(libs.awaitility) testImplementation(libs.testcontainers.junit) + testImplementation(libs.testcontainers.postgres) + // needed for the Participant testImplementation(project(":core:lib:credential-query-lib")) testImplementation(testFixtures(project(":spi:verifiable-credential-spi"))) testImplementation(testFixtures(libs.edc.testfixtures.managementapi)) + testImplementation(testFixtures(libs.edc.core.sql)) + testImplementation(libs.edc.ext.transaction.local) + testImplementation(libs.edc.sql.pool) testImplementation(libs.nimbus.jwt) testImplementation(libs.jakarta.rsApi) } diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java index 4c60524e4..5e0429607 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java @@ -20,13 +20,18 @@ import org.eclipse.edc.identithub.spi.did.events.DidDocumentUnpublished; import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndExtension; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndTestContext; import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; import org.eclipse.edc.spi.event.EventRouter; import org.eclipse.edc.spi.event.EventSubscriber; import org.eclipse.edc.spi.query.QuerySpec; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import java.util.Arrays; @@ -39,253 +44,266 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; -@EndToEndTest -public class DidManagementApiEndToEndTest extends IdentityApiEndToEndTest { - - - @AfterEach - void tearDown() { - // purge all users - var store = RUNTIME.getService(ParticipantContextService.class); - store.query(QuerySpec.max()).getContent() - .forEach(pc -> store.deleteParticipantContext(pc.getParticipantId())); - } - - @Test - void publishDid_notOwner_expect403() { - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(DidDocumentPublished.class, subscriber); - - var user1 = "user1"; - createParticipant(user1); - - - // create second user - var user2 = "user2"; - var user2Context = ParticipantContext.Builder.newInstance() - .participantId(user2) - .did("did:web:" + user2) - .apiTokenAlias(user2 + "-alias") - .build(); - var user2Token = storeParticipant(user2Context); - - reset(subscriber); // need to reset here, to ignore a previous interaction - - // attempt to publish user1's DID document, which should fail - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", user2Token)) - .body(""" - { - "did": "did:web:user1" - } - """) - .post("/v1alpha/participants/%s/dids/publish".formatted(user1)) - .then() - .log().ifValidationFails() - .statusCode(403) - .body(Matchers.notNullValue()); - - verifyNoInteractions(subscriber); - } - - @Test - void publishDid() { - var superUserKey = createSuperUser(); - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(DidDocumentPublished.class, subscriber); - - var user = "test-user"; - var token = createParticipant(user); - - assertThat(Arrays.asList(token, superUserKey)) - .allSatisfy(t -> { - reset(subscriber); - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", t)) - .body(""" - { - "did": "did:web:test-user" - } - """) - .post("/v1alpha/participants/%s/dids/publish".formatted(user)) - .then() - .log().ifValidationFails() - .statusCode(204) - .body(Matchers.notNullValue()); - - // verify that the publish event was fired twice - verify(subscriber).on(argThat(env -> { - if (env.getPayload() instanceof DidDocumentPublished event) { - return event.getDid().equals("did:web:test-user"); - } - return false; - })); - - }); +public class DidManagementApiEndToEndTest { + + abstract static class Tests { + + @AfterEach + void tearDown(ParticipantContextService store) { + // purge all users + store.query(QuerySpec.max()).getContent() + .forEach(pc -> store.deleteParticipantContext(pc.getParticipantId())); + } + + @Test + void publishDid_notOwner_expect403(IdentityHubEndToEndTestContext context, EventRouter router) { + var subscriber = mock(EventSubscriber.class); + router.registerSync(DidDocumentPublished.class, subscriber); + + var user1 = "user1"; + context.createParticipant(user1); + + + // create second user + var user2 = "user2"; + var user2Context = ParticipantContext.Builder.newInstance() + .participantId(user2) + .did("did:web:" + user2) + .apiTokenAlias(user2 + "-alias") + .build(); + var user2Token = context.storeParticipant(user2Context); + + reset(subscriber); // need to reset here, to ignore a previous interaction + + // attempt to publish user1's DID document, which should fail + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", user2Token)) + .body(""" + { + "did": "did:web:user1" + } + """) + .post("/v1alpha/participants/%s/dids/publish".formatted(user1)) + .then() + .log().ifValidationFails() + .statusCode(403) + .body(Matchers.notNullValue()); + + verifyNoInteractions(subscriber); + } + + @Test + void publishDid(IdentityHubEndToEndTestContext context, EventRouter router) { + var superUserKey = context.createSuperUser(); + var subscriber = mock(EventSubscriber.class); + router.registerSync(DidDocumentPublished.class, subscriber); + + var user = "test-user"; + var token = context.createParticipant(user); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + reset(subscriber); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(""" + { + "did": "did:web:test-user" + } + """) + .post("/v1alpha/participants/%s/dids/publish".formatted(user)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(Matchers.notNullValue()); + + // verify that the publish event was fired twice + verify(subscriber).on(argThat(env -> { + if (env.getPayload() instanceof DidDocumentPublished event) { + return event.getDid().equals("did:web:test-user"); + } + return false; + })); + + }); + + } + + @Test + void unpublishDid_notOwner_expect403(IdentityHubEndToEndTestContext context, EventRouter router) { + var subscriber = mock(EventSubscriber.class); + router.registerSync(DidDocumentPublished.class, subscriber); + + var user1 = "user1"; + context.createParticipant(user1); + + + // create second user + var user2 = "user2"; + var user2Context = ParticipantContext.Builder.newInstance() + .participantId(user2) + .did("did:web:" + user2) + .apiTokenAlias(user2 + "-alias") + .build(); + var user2Token = context.storeParticipant(user2Context); + + reset(subscriber); // need to reset here, to ignore a previous interaction + + // attempt to publish user1's DID document, which should fail + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", user2Token)) + .body(""" + { + "did": "did:web:user1" + } + """) + .post("/v1alpha/participants/%s/dids/unpublish".formatted(user1)) + .then() + .log().ifValidationFails() + .statusCode(403) + .body(Matchers.notNullValue()); + + verifyNoInteractions(subscriber); + } + + @Test + void unpublishDid(IdentityHubEndToEndTestContext context, EventRouter router) { + var superUserKey = context.createSuperUser(); + var subscriber = mock(EventSubscriber.class); + router.registerSync(DidDocumentUnpublished.class, subscriber); + + var user = "test-user"; + var token = context.createParticipant(user); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + reset(subscriber); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(""" + { + "did": "did:web:test-user" + } + """) + .post("/v1alpha/participants/%s/dids/unpublish".formatted(user)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(Matchers.notNullValue()); + + // verify that the publish event was fired twice + verify(subscriber).on(argThat(env -> { + if (env.getPayload() instanceof DidDocumentUnpublished event) { + return event.getDid().equals("did:web:test-user"); + } + return false; + })); + }); + + } + + @Test + void getState_nowOwner_expect403(IdentityHubEndToEndTestContext context) { + var user1 = "user1"; + context.createParticipant(user1); + + var user2 = "user2"; + var token2 = context.createParticipant(user2); + + context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", token2)) + .contentType(JSON) + .body(""" + { + "did": "did:web:user1" + } + """) + .post("/v1alpha/participants/%s/dids/state".formatted(user1)) + .then() + .log().ifValidationFails() + .statusCode(403); + } + + @Test + void getAll(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + range(0, 20).forEach(i -> context.createParticipant("user-" + i)); + + var docs = context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", superUserKey)) + .get("/v1alpha/dids") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(DidDocument[].class); + + assertThat(docs).hasSize(21); //includes the super-user's DID doc + } + + @Test + void getAll_withDefaultPaging(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + range(0, 70).forEach(i -> context.createParticipant("user-" + i)); + + var docs = context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", superUserKey)) + .get("/v1alpha/dids") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(DidDocument[].class); + + assertThat(docs).hasSize(50); //includes the super-user's DID doc + } + + @Test + void getAll_withPaging(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + range(0, 20).forEach(i -> context.createParticipant("user-" + i)); + + var docs = context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", superUserKey)) + .get("/v1alpha/dids?offset=5&limit=10") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(DidDocument[].class); + + assertThat(docs).hasSize(10); + } + + @Test + void getAll_notAuthorized(IdentityHubEndToEndTestContext context) { + + var attackerToken = context.createParticipant("attacker"); + + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", attackerToken)) + .get("/v1alpha/dids") + .then() + .log().ifValidationFails() + .statusCode(403); + } } - @Test - void unpublishDid_notOwner_expect403() { - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(DidDocumentPublished.class, subscriber); - - var user1 = "user1"; - createParticipant(user1); - - - // create second user - var user2 = "user2"; - var user2Context = ParticipantContext.Builder.newInstance() - .participantId(user2) - .did("did:web:" + user2) - .apiTokenAlias(user2 + "-alias") - .build(); - var user2Token = storeParticipant(user2Context); - - reset(subscriber); // need to reset here, to ignore a previous interaction - - // attempt to publish user1's DID document, which should fail - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", user2Token)) - .body(""" - { - "did": "did:web:user1" - } - """) - .post("/v1alpha/participants/%s/dids/unpublish".formatted(user1)) - .then() - .log().ifValidationFails() - .statusCode(403) - .body(Matchers.notNullValue()); - - verifyNoInteractions(subscriber); + @Nested + @EndToEndTest + @ExtendWith(IdentityHubEndToEndExtension.InMemory.class) + class InMemory extends Tests { } - @Test - void unpublishDid() { - var superUserKey = createSuperUser(); - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(DidDocumentUnpublished.class, subscriber); - - var user = "test-user"; - var token = createParticipant(user); - - assertThat(Arrays.asList(token, superUserKey)) - .allSatisfy(t -> { - reset(subscriber); - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", t)) - .body(""" - { - "did": "did:web:test-user" - } - """) - .post("/v1alpha/participants/%s/dids/unpublish".formatted(user)) - .then() - .log().ifValidationFails() - .statusCode(204) - .body(Matchers.notNullValue()); - - // verify that the publish event was fired twice - verify(subscriber).on(argThat(env -> { - if (env.getPayload() instanceof DidDocumentUnpublished event) { - return event.getDid().equals("did:web:test-user"); - } - return false; - })); - }); - - } - - @Test - void getState_nowOwner_expect403() { - var user1 = "user1"; - createParticipant(user1); - - var user2 = "user2"; - var token2 = createParticipant(user2); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", token2)) - .contentType(JSON) - .body(""" - { - "did": "did:web:user1" - } - """) - .post("/v1alpha/participants/%s/dids/state".formatted(user1)) - .then() - .log().ifValidationFails() - .statusCode(403); - } - - @Test - void getAll() { - var superUserKey = createSuperUser(); - range(0, 20).forEach(i -> createParticipant("user-" + i)); - - var docs = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", superUserKey)) - .get("/v1alpha/dids") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(DidDocument[].class); - - assertThat(docs).hasSize(21); //includes the super-user's DID doc - } - - @Test - void getAll_withDefaultPaging() { - var superUserKey = createSuperUser(); - range(0, 70).forEach(i -> createParticipant("user-" + i)); - - var docs = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", superUserKey)) - .get("/v1alpha/dids") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(DidDocument[].class); - - assertThat(docs).hasSize(50); //includes the super-user's DID doc - } - - @Test - void getAll_withPaging() { - var superUserKey = createSuperUser(); - range(0, 20).forEach(i -> createParticipant("user-" + i)); - - var docs = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", superUserKey)) - .get("/v1alpha/dids?offset=5&limit=10") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(DidDocument[].class); - - assertThat(docs).hasSize(10); - } - - @Test - void getAll_notAuthorized() { - - var attackerToken = createParticipant("attacker"); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", attackerToken)) - .get("/v1alpha/dids") - .then() - .log().ifValidationFails() - .statusCode(403); + @Nested + @PostgresqlIntegrationTest + @ExtendWith(IdentityHubEndToEndExtension.Postgres.class) + class Postgres extends Tests { } } diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java index 0cc48a401..c23d3b62f 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java @@ -14,28 +14,27 @@ package org.eclipse.edc.identityhub.tests; -import com.nimbusds.jose.jwk.Curve; import io.restassured.http.Header; -import org.eclipse.edc.identityhub.spi.keypair.KeyPairService; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairAdded; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRotated; import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairState; import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; -import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndExtension; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndTestContext; import org.eclipse.edc.junit.annotations.EndToEndTest; -import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; import org.eclipse.edc.spi.event.EventRouter; import org.eclipse.edc.spi.event.EventSubscriber; import org.eclipse.edc.spi.query.QuerySpec; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import java.util.Arrays; import java.util.Base64; -import java.util.Map; -import java.util.UUID; import static io.restassured.http.ContentType.JSON; import static java.util.stream.IntStream.range; @@ -47,471 +46,466 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; -@EndToEndTest -public class KeyPairResourceApiEndToEndTest extends IdentityApiEndToEndTest { - - @AfterEach - void tearDown() { - // purge all users - var pcService = RUNTIME.getService(ParticipantContextService.class); - pcService.query(QuerySpec.max()).getContent() - .forEach(pc -> pcService.deleteParticipantContext(pc.getParticipantId()).getContent()); - } - - @Test - void findById_notAuthorized() { - var user1 = "user1"; - createParticipant(user1); - - // create second user - var user2 = "user2"; - var user2Context = ParticipantContext.Builder.newInstance() - .participantId(user2) - .did("did:web:" + user2) - .apiTokenAlias(user2 + "-alias") - .build(); - var user2Token = storeParticipant(user2Context); - - var key = createKeyPair(user1); - - // attempt to publish user1's DID document, which should fail - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", user2Token)) - .get("/v1alpha/participants/%s/keypairs/%s".formatted(toBase64(user1), key)) - .then() - .log().ifValidationFails() - .statusCode(403) - .body(notNullValue()); - } - - @Test - void findById() { - var superUserKey = createSuperUser(); - var user1 = "user1"; - var token = createParticipant(user1); - - var key = createKeyPair(user1); - - assertThat(Arrays.asList(token, superUserKey)) - .allSatisfy(t -> RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", t)) - .get("/v1alpha/participants/%s/keypairs/%s".formatted(toBase64(user1), key)) - .then() - .log().ifValidationFails() - .statusCode(200) - .body(notNullValue())); - } - - @Test - void findForParticipant_notAuthorized() { - var user1 = "user1"; - createParticipant(user1); - - // create second user - var user2 = "user2"; - var user2Context = ParticipantContext.Builder.newInstance() - .participantId(user2) - .did("did:web:" + user2) - .apiTokenAlias(user2 + "-alias") - .build(); - var user2Token = storeParticipant(user2Context); - - createKeyPair(user1); - - // attempt to publish user1's DID document, which should fail - var res = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", user2Token)) - .get("/v1alpha/participants/%s/keypairs".formatted(toBase64(user1))) - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(KeyPairResource[].class); - - assertThat(res).isEmpty(); - - } - - @Test - void findForParticipant() { - var superUserKey = createSuperUser(); - var user1 = "user1"; - var token = createParticipant(user1); - createKeyPair(user1); - - assertThat(Arrays.asList(token, superUserKey)) - .allSatisfy(t -> RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", t)) - .get("/v1alpha/participants/%s/keypairs".formatted(toBase64(user1))) - .then() - .log().ifValidationFails() - .statusCode(200) - .body(notNullValue())); - - } - - @Test - void addKeyPair() { - var superUserKey = createSuperUser(); - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(KeyPairAdded.class, subscriber); - - var user1 = "user1"; - var token = createParticipant(user1); - - assertThat(Arrays.asList(token, superUserKey)) - .allSatisfy(t -> { - var keyDesc = createKeyDescriptor(user1).build(); - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() +public class KeyPairResourceApiEndToEndTest { + + abstract static class Tests { + + @AfterEach + void tearDown(ParticipantContextService store) { + // purge all users + store.query(QuerySpec.max()).getContent() + .forEach(pc -> store.deleteParticipantContext(pc.getParticipantId()).getContent()); + } + + @Test + void findById_notAuthorized(IdentityHubEndToEndTestContext context) { + var user1 = "user1"; + context.createParticipant(user1); + + // create second user + var user2 = "user2"; + var user2Context = ParticipantContext.Builder.newInstance() + .participantId(user2) + .did("did:web:" + user2) + .apiTokenAlias(user2 + "-alias") + .build(); + var user2Token = context.storeParticipant(user2Context); + + var key = context.createKeyPair(user1); + + // attempt to publish user1's DID document, which should fail + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", user2Token)) + .get("/v1alpha/participants/%s/keypairs/%s".formatted(toBase64(user1), key)) + .then() + .log().ifValidationFails() + .statusCode(403) + .body(notNullValue()); + } + + @Test + void findById(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user1 = "user1"; + var token = context.createParticipant(user1); + + var key = context.createKeyPair(user1); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> context.getIdentityApiEndpoint().baseRequest() .contentType(JSON) .header(new Header("x-api-key", t)) - .body(keyDesc) - .put("/v1alpha/participants/%s/keypairs".formatted(toBase64(user1))) + .get("/v1alpha/participants/%s/keypairs/%s".formatted(toBase64(user1), key)) .then() .log().ifValidationFails() - .statusCode(204) - .body(notNullValue()); - - verify(subscriber).on(argThat(env -> { - var evt = (KeyPairAdded) env.getPayload(); - return evt.getParticipantId().equals(user1) && - evt.getKeyPairResourceId().equals(keyDesc.getResourceId()) && - evt.getKeyId().equals(keyDesc.getKeyId()); - })); - }); - } - - @Test - void addKeyPair_notAuthorized() { - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(KeyPairAdded.class, subscriber); - - var user1 = "user1"; - var token = createParticipant(user1); - - var user2 = "user2"; - var token2 = createParticipant(user2); - - - // attempt to publish user1's DID document, which should fail - var keyDesc = createKeyDescriptor(user1).build(); - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token2)) - .body(keyDesc) - .put("/v1alpha/participants/%s/keypairs".formatted(toBase64(user1))) - .then() - .log().ifValidationFails() - .statusCode(403) - .body(notNullValue()); - - verify(subscriber, never()).on(argThat(env -> { - if (env.getPayload() instanceof KeyPairAdded evt) { - return evt.getKeyPairResourceId().equals(keyDesc.getKeyId()); - } - return false; - })); - } - - @Test - void rotate() { - var superUserKey = createSuperUser(); - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(KeyPairRotated.class, subscriber); - getService(EventRouter.class).registerSync(KeyPairAdded.class, subscriber); - - var user1 = "user1"; - var token = createParticipant(user1); - - var keyPairId = createKeyPair(user1); - - assertThat(Arrays.asList(token, superUserKey)) - .allSatisfy(t -> { - reset(subscriber); - // attempt to publish user1's DID document, which should fail - var keyDesc = createKeyDescriptor(user1).build(); - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() + .statusCode(200) + .body(notNullValue())); + } + + @Test + void findForParticipant_notAuthorized(IdentityHubEndToEndTestContext context) { + var user1 = "user1"; + context.createParticipant(user1); + + // create second user + var user2 = "user2"; + var user2Context = ParticipantContext.Builder.newInstance() + .participantId(user2) + .did("did:web:" + user2) + .apiTokenAlias(user2 + "-alias") + .build(); + var user2Token = context.storeParticipant(user2Context); + + context.createKeyPair(user1); + + // attempt to publish user1's DID document, which should fail + var res = context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", user2Token)) + .get("/v1alpha/participants/%s/keypairs".formatted(toBase64(user1))) + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(KeyPairResource[].class); + + assertThat(res).isEmpty(); + + } + + @Test + void findForParticipant(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user1 = "user1"; + var token = context.createParticipant(user1); + context.createKeyPair(user1); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> context.getIdentityApiEndpoint().baseRequest() .contentType(JSON) .header(new Header("x-api-key", t)) - .body(keyDesc) - .post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(toBase64(user1), keyPairId)) + .get("/v1alpha/participants/%s/keypairs".formatted(toBase64(user1))) .then() .log().ifValidationFails() - .statusCode(204) - .body(notNullValue()); - - // verify that the "rotated" event fired once - verify(subscriber).on(argThat(env -> { - if (env.getPayload() instanceof KeyPairRotated evt) { - return evt.getParticipantId().equals(user1); - } - return false; - })); - // verify that the correct "added" event fired - verify(subscriber).on(argThat(env -> { - if (env.getPayload() instanceof KeyPairAdded evt) { - return evt.getKeyPairResourceId().equals(keyDesc.getResourceId()) && + .statusCode(200) + .body(notNullValue())); + + } + + @Test + void addKeyPair(IdentityHubEndToEndTestContext context, EventRouter router) { + var superUserKey = context.createSuperUser(); + var subscriber = mock(EventSubscriber.class); + router.registerSync(KeyPairAdded.class, subscriber); + + var user1 = "user1"; + var token = context.createParticipant(user1); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + var keyDesc = context.createKeyDescriptor(user1).build(); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(keyDesc) + .put("/v1alpha/participants/%s/keypairs".formatted(toBase64(user1))) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + verify(subscriber).on(argThat(env -> { + var evt = (KeyPairAdded) env.getPayload(); + return evt.getParticipantId().equals(user1) && + evt.getKeyPairResourceId().equals(keyDesc.getResourceId()) && evt.getKeyId().equals(keyDesc.getKeyId()); - } - return false; - })); - }); - } + })); + }); + } + + @Test + void addKeyPair_notAuthorized(IdentityHubEndToEndTestContext context, EventRouter router) { + var subscriber = mock(EventSubscriber.class); + router.registerSync(KeyPairAdded.class, subscriber); + + var user1 = "user1"; + var token = context.createParticipant(user1); + + var user2 = "user2"; + var token2 = context.createParticipant(user2); + + + // attempt to publish user1's DID document, which should fail + var keyDesc = context.createKeyDescriptor(user1).build(); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", token2)) + .body(keyDesc) + .put("/v1alpha/participants/%s/keypairs".formatted(toBase64(user1))) + .then() + .log().ifValidationFails() + .statusCode(403) + .body(notNullValue()); + + verify(subscriber, never()).on(argThat(env -> { + if (env.getPayload() instanceof KeyPairAdded evt) { + return evt.getKeyPairResourceId().equals(keyDesc.getKeyId()); + } + return false; + })); + } + + @Test + void rotate(IdentityHubEndToEndTestContext context, EventRouter router) { + var superUserKey = context.createSuperUser(); + var subscriber = mock(EventSubscriber.class); + router.registerSync(KeyPairRotated.class, subscriber); + router.registerSync(KeyPairAdded.class, subscriber); + + var user1 = "user1"; + var token = context.createParticipant(user1); + + var keyPairId = context.createKeyPair(user1); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + reset(subscriber); + // attempt to publish user1's DID document, which should fail + var keyDesc = context.createKeyDescriptor(user1).build(); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(keyDesc) + .post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(toBase64(user1), keyPairId)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + // verify that the "rotated" event fired once + verify(subscriber).on(argThat(env -> { + if (env.getPayload() instanceof KeyPairRotated evt) { + return evt.getParticipantId().equals(user1); + } + return false; + })); + // verify that the correct "added" event fired + verify(subscriber).on(argThat(env -> { + if (env.getPayload() instanceof KeyPairAdded evt) { + return evt.getKeyPairResourceId().equals(keyDesc.getResourceId()) && + evt.getKeyId().equals(keyDesc.getKeyId()); + } + return false; + })); + }); + } + + @Test + void rotate_notAuthorized(IdentityHubEndToEndTestContext context, EventRouter router) { + var subscriber = mock(EventSubscriber.class); + router.registerSync(KeyPairRotated.class, subscriber); + + var user1 = "user1"; + var token = context.createParticipant(user1); + + var user2 = "user2"; + var token2 = context.createParticipant(user2); + + var keyId = context.createKeyPair(user1); + + // attempt to publish user1's DID document, which should fail + var keyDesc = context.createKeyDescriptor(user1).build(); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", token2)) + .body(keyDesc) + .post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(user1, keyId)) + .then() + .log().ifValidationFails() + .statusCode(403) + .body(notNullValue()); + + // make sure that the event to add the _new_ keypair was never fired + verify(subscriber, never()).on(argThat(env -> { + if (env.getPayload() instanceof KeyPairRotated evt) { + return evt.getParticipantId().equals(user1) && evt.getKeyPairResourceId().equals(keyDesc.getKeyId()); + } + return false; + })); + } + + @Test + void revoke(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user1 = "user1"; + var token = context.createParticipant(user1); + + var keyId = context.createKeyPair(user1); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + var keyDesc = context.createKeyDescriptor(user1).build(); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(keyDesc) + .post("/v1alpha/participants/%s/keypairs/%s/revoke".formatted(toBase64(user1), keyId)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + assertThat(context.getDidForParticipant(user1)).hasSize(1) + .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).noneMatch(vm -> vm.getId().equals(keyId))); + }); + } + + @Test + void revoke_notAuthorized(IdentityHubEndToEndTestContext context) { + var user1 = "user1"; + var token1 = context.createParticipant(user1); + + var user2 = "user2"; + var token2 = context.createParticipant(user2); + + var keyId = context.createKeyPair(user1); + + // attempt to publish user1's DID document, which should fail + var keyDesc = context.createKeyDescriptor(user1).build(); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", token2)) + .body(keyDesc) + .post("/v1alpha/participants/%s/keypairs/%s/revoke".formatted(toBase64(user1), keyId)) + .then() + .log().ifValidationFails() + .statusCode(403) + .body(notNullValue()); + } + + @Test + void getAll(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + range(0, 10) + .forEach(i -> { + var participantId = "user" + i; + context.createParticipant(participantId); // implicitly creates a keypair + }); + var found = context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", superUserKey)) + .get("/v1alpha/keypairs") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(KeyPairResource[].class); + assertThat(found).hasSize(11); //10 + 1 for the super user + } + + @Test + void getAll_withPaging(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + range(0, 10) + .forEach(i -> { + var participantId = "user" + i; + context.createParticipant(participantId); // implicitly creates a keypair + }); + var found = context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", superUserKey)) + .get("/v1alpha/keypairs?offset=2&limit=4") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(KeyPairResource[].class); + assertThat(found).hasSize(4); + } + + @Test + void getAll_withDefaultPaging(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + range(0, 70) + .forEach(i -> { + var participantId = "user" + i; + context.createParticipant(participantId); // implicitly creates a keypair + }); + var found = context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", superUserKey)) + .get("/v1alpha/keypairs") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(KeyPairResource[].class); + assertThat(found).hasSize(50); + } + + @Test + void getAll_notAuthorized(IdentityHubEndToEndTestContext context) { + var attackerToken = context.createParticipant("attacker"); + + range(0, 10) + .forEach(i -> { + var participantId = "user" + i; + context.createParticipant(participantId); // implicitly creates a keypair + }); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", attackerToken)) + .get("/v1alpha/keypairs") + .then() + .log().ifValidationFails() + .statusCode(403); + } + + @Test + void activate(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user1 = "user1"; + var token = context.createParticipant(user1); + var keyPairId = context.createKeyPair(user1); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .post("/v1alpha/participants/%s/keypairs/%s/activate".formatted(toBase64(user1), keyPairId)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + assertThat(context.getDidForParticipant(user1)) + .hasSize(1) + .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).noneMatch(vm -> vm.getId().equals(keyPairId))); + }); + } + + @Test + void activate_notAuthorized(IdentityHubEndToEndTestContext context) { + var user1 = "user1"; + context.createParticipant(user1); + var keyId = context.createKeyPair(user1); + var attackerToken = context.createParticipant("attacker"); + + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", attackerToken)) + .post("/v1alpha/participants/%s/keypairs/%s/activate".formatted(toBase64(user1), keyId)) + .then() + .log().ifValidationFails() + .statusCode(403) + .body(notNullValue()); + + assertThat(context.getKeyPairsForParticipant(user1)) + .hasSize(2) + .allMatch(keyPairResource -> keyPairResource.getState() == KeyPairState.ACTIVE.code()); + } + + @Test + void activate_illegalState(IdentityHubEndToEndTestContext context) { + var user1 = "user1"; + var token = context.createParticipant(user1); + var keyPairId = context.createKeyPair(user1); + + // first revoke the key, which puts it in the REVOKED state + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", token)) + .post("/v1alpha/participants/%s/keypairs/%s/revoke".formatted(toBase64(user1), keyPairId)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + // now attempt to activate + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", token)) + .post("/v1alpha/participants/%s/keypairs/%s/activate".formatted(toBase64(user1), keyPairId)) + .then() + .log().ifValidationFails() + .statusCode(400) + .body(notNullValue()); + } + + + private String toBase64(String s) { + return Base64.getUrlEncoder().encodeToString(s.getBytes()); + } - @Test - void rotate_notAuthorized() { - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(KeyPairRotated.class, subscriber); - - var user1 = "user1"; - var token = createParticipant(user1); - - var user2 = "user2"; - var token2 = createParticipant(user2); - - var keyId = createKeyPair(user1); - - // attempt to publish user1's DID document, which should fail - var keyDesc = createKeyDescriptor(user1).build(); - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token2)) - .body(keyDesc) - .post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(user1, keyId)) - .then() - .log().ifValidationFails() - .statusCode(403) - .body(notNullValue()); - - // make sure that the event to add the _new_ keypair was never fired - verify(subscriber, never()).on(argThat(env -> { - if (env.getPayload() instanceof KeyPairRotated evt) { - return evt.getParticipantId().equals(user1) && evt.getKeyPairResourceId().equals(keyDesc.getKeyId()); - } - return false; - })); } - @Test - void revoke() { - var superUserKey = createSuperUser(); - var user1 = "user1"; - var token = createParticipant(user1); - - var keyId = createKeyPair(user1); - - assertThat(Arrays.asList(token, superUserKey)) - .allSatisfy(t -> { - var keyDesc = createKeyDescriptor(user1).build(); - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", t)) - .body(keyDesc) - .post("/v1alpha/participants/%s/keypairs/%s/revoke".formatted(toBase64(user1), keyId)) - .then() - .log().ifValidationFails() - .statusCode(204) - .body(notNullValue()); - - assertThat(getDidForParticipant(user1)).hasSize(1) - .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).noneMatch(vm -> vm.getId().equals(keyId))); - }); + @Nested + @EndToEndTest + @ExtendWith(IdentityHubEndToEndExtension.InMemory.class) + class InMemory extends Tests { } - @Test - void revoke_notAuthorized() { - var user1 = "user1"; - var token1 = createParticipant(user1); - - var user2 = "user2"; - var token2 = createParticipant(user2); - - var keyId = createKeyPair(user1); - - // attempt to publish user1's DID document, which should fail - var keyDesc = createKeyDescriptor(user1).build(); - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token2)) - .body(keyDesc) - .post("/v1alpha/participants/%s/keypairs/%s/revoke".formatted(toBase64(user1), keyId)) - .then() - .log().ifValidationFails() - .statusCode(403) - .body(notNullValue()); + @Nested + @PostgresqlIntegrationTest + @ExtendWith(IdentityHubEndToEndExtension.Postgres.class) + class Postgres extends Tests { } - - @Test - void getAll() { - var superUserKey = createSuperUser(); - range(0, 10) - .forEach(i -> { - var participantId = "user" + i; - createParticipant(participantId); // implicitly creates a keypair - }); - var found = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", superUserKey)) - .get("/v1alpha/keypairs") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(KeyPairResource[].class); - assertThat(found).hasSize(11); //10 + 1 for the super user - } - - @Test - void getAll_withPaging() { - var superUserKey = createSuperUser(); - range(0, 10) - .forEach(i -> { - var participantId = "user" + i; - createParticipant(participantId); // implicitly creates a keypair - }); - var found = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", superUserKey)) - .get("/v1alpha/keypairs?offset=2&limit=4") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(KeyPairResource[].class); - assertThat(found).hasSize(4); - } - - @Test - void getAll_withDefaultPaging() { - var superUserKey = createSuperUser(); - range(0, 70) - .forEach(i -> { - var participantId = "user" + i; - createParticipant(participantId); // implicitly creates a keypair - }); - var found = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", superUserKey)) - .get("/v1alpha/keypairs") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(KeyPairResource[].class); - assertThat(found).hasSize(50); - } - - @Test - void getAll_notAuthorized() { - var attackerToken = createParticipant("attacker"); - - range(0, 10) - .forEach(i -> { - var participantId = "user" + i; - createParticipant(participantId); // implicitly creates a keypair - }); - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", attackerToken)) - .get("/v1alpha/keypairs") - .then() - .log().ifValidationFails() - .statusCode(403); - } - - @Test - void activate() { - var superUserKey = createSuperUser(); - var user1 = "user1"; - var token = createParticipant(user1); - var keyPairId = createKeyPair(user1); - - assertThat(Arrays.asList(token, superUserKey)) - .allSatisfy(t -> { - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", t)) - .post("/v1alpha/participants/%s/keypairs/%s/activate".formatted(toBase64(user1), keyPairId)) - .then() - .log().ifValidationFails() - .statusCode(204) - .body(notNullValue()); - - assertThat(getDidForParticipant(user1)) - .hasSize(1) - .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).noneMatch(vm -> vm.getId().equals(keyPairId))); - }); - } - - @Test - void activate_notAuthorized() { - var user1 = "user1"; - createParticipant(user1); - var keyId = createKeyPair(user1); - var attackerToken = createParticipant("attacker"); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", attackerToken)) - .post("/v1alpha/participants/%s/keypairs/%s/activate".formatted(toBase64(user1), keyId)) - .then() - .log().ifValidationFails() - .statusCode(403) - .body(notNullValue()); - - assertThat(getKeyPairsForParticipant(user1)) - .hasSize(2) - .allMatch(keyPairResource -> keyPairResource.getState() == KeyPairState.ACTIVE.code()); - } - - @Test - void activate_illegalState() { - var user1 = "user1"; - var token = createParticipant(user1); - var keyPairId = createKeyPair(user1); - - // first revoke the key, which puts it in the REVOKED state - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token)) - .post("/v1alpha/participants/%s/keypairs/%s/revoke".formatted(toBase64(user1), keyPairId)) - .then() - .log().ifValidationFails() - .statusCode(204) - .body(notNullValue()); - - // now attempt to activate - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token)) - .post("/v1alpha/participants/%s/keypairs/%s/activate".formatted(toBase64(user1), keyPairId)) - .then() - .log().ifValidationFails() - .statusCode(400) - .body(notNullValue()); - } - - private KeyDescriptor.Builder createKeyDescriptor(String participantId) { - var keyId = UUID.randomUUID().toString(); - return KeyDescriptor.Builder.newInstance() - .keyId(keyId) - .resourceId(UUID.randomUUID().toString()) - .keyGeneratorParams(Map.of("algorithm", "EC", "curve", Curve.P_384.getStdName())) - .privateKeyAlias("%s-%s-alias".formatted(participantId, keyId)); - } - - private String createKeyPair(String participantId) { - - var descriptor = createKeyDescriptor(participantId).build(); - - var service = RUNTIME.getService(KeyPairService.class); - service.addKeyPair(participantId, descriptor, true) - .orElseThrow(f -> new EdcException(f.getFailureDetail())); - return descriptor.getResourceId(); - } - - private String toBase64(String s) { - return Base64.getUrlEncoder().encodeToString(s.getBytes()); - } - } diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java index 8529b5f99..290952011 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java @@ -22,13 +22,18 @@ import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextUpdated; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContextState; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndExtension; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndTestContext; import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.event.EventRouter; import org.eclipse.edc.spi.event.EventSubscriber; import org.eclipse.edc.spi.query.QuerySpec; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -41,6 +46,7 @@ import static io.restassured.http.ContentType.JSON; import static java.util.stream.IntStream.range; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndTestContext.SUPER_USER; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; @@ -49,345 +55,358 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; -@EndToEndTest -public class ParticipantContextApiEndToEndTest extends IdentityApiEndToEndTest { - - @AfterEach - void tearDown() { - // purge all users - var pcService = RUNTIME.getService(ParticipantContextService.class); - pcService.query(QuerySpec.max()).getContent() - .forEach(pc -> pcService.deleteParticipantContext(pc.getParticipantId()).getContent()); - } - - @Test - void getUserById() { - var apikey = createSuperUser(); - - var su = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", apikey)) - .get("/v1alpha/participants/" + toBase64(SUPER_USER)) - .then() - .statusCode(200) - .extract().body().as(ParticipantContext.class); - assertThat(su.getParticipantId()).isEqualTo(SUPER_USER); - } - - @Test - void getUserById_notOwner_expect403() { - var user1 = "user1"; - var user1Context = ParticipantContext.Builder.newInstance() - .participantId(user1) - .did("did:web:" + user1) - .apiTokenAlias(user1 + "-alias") - .build(); - var apiToken1 = storeParticipant(user1Context); - - var user2 = "user2"; - var user2Context = ParticipantContext.Builder.newInstance() - .participantId(user2) - .did("did:web:" + user2) - .apiTokenAlias(user2 + "-alias") - .build(); - var apiToken2 = storeParticipant(user2Context); - - //user1 attempts to read user2 -> fail - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", apiToken1)) - .contentType(ContentType.JSON) - .get("/v1alpha/participants/" + toBase64(user2)) - .then() - .log().ifValidationFails() - .statusCode(403); - } - - @Test - void createNewUser_principalIsSuperser() { - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(ParticipantContextCreated.class, subscriber); - var apikey = createSuperUser(); - - var manifest = createNewParticipant().build(); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", apikey)) - .contentType(ContentType.JSON) - .body(manifest) - .post("/v1alpha/participants/") - .then() - .log().ifError() - .statusCode(anyOf(equalTo(200), equalTo(204))) - .body(notNullValue()); - - verify(subscriber).on(argThat(env -> ((ParticipantContextCreated) env.getPayload()).getParticipantId().equals(manifest.getParticipantId()))); - - assertThat(getKeyPairsForParticipant(manifest.getParticipantId())).hasSize(1); - assertThat(getDidForParticipant(manifest.getParticipantId())).hasSize(1) - .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).hasSize(1)); - } - - @ParameterizedTest(name = "Create participant with key pair active = {0}") - @ValueSource(booleans = { true, false }) - void createNewUser_verifyKeyPairActive(boolean isActive) { - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(ParticipantContextCreated.class, subscriber); - var apikey = createSuperUser(); - - var participantId = UUID.randomUUID().toString(); - var manifest = createNewParticipant() - .participantId(participantId) - .did("did:web:" + participantId) - .key(createKeyDescriptor().active(isActive).build()) - .build(); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", apikey)) - .contentType(ContentType.JSON) - .body(manifest) - .post("/v1alpha/participants/") - .then() - .log().ifError() - .statusCode(anyOf(equalTo(200), equalTo(204))) - .body(notNullValue()); - - verify(subscriber).on(argThat(env -> ((ParticipantContextCreated) env.getPayload()).getParticipantId().equals(manifest.getParticipantId()))); - - assertThat(getKeyPairsForParticipant(manifest.getParticipantId())).hasSize(1) - .allSatisfy(kpr -> assertThat(kpr.getState()).isEqualTo(isActive ? KeyPairState.ACTIVE.code() : KeyPairState.CREATED.code())); - assertThat(getDidForParticipant(manifest.getParticipantId())).hasSize(1) - .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).hasSize(1)); - - } - - - @Test - void createNewUser_principalIsNotSuperuser_expect403() { - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(ParticipantContextCreated.class, subscriber); - - var principal = "another-user"; - var anotherUser = ParticipantContext.Builder.newInstance() - .participantId(principal) - .did("did:web:" + principal) - .apiTokenAlias(principal + "-alias") - .build(); - var apiToken = storeParticipant(anotherUser); - var manifest = createNewParticipant().build(); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", apiToken)) - .contentType(ContentType.JSON) - .body(manifest) - .post("/v1alpha/participants/") - .then() - .log().ifError() - .statusCode(403) - .body(notNullValue()); - verifyNoInteractions(subscriber); - - assertThat(getKeyPairsForParticipant(manifest.getParticipantId())).isEmpty(); - } - - @Test - void createNewUser_principalIsKnown_expect401() { - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(ParticipantContextCreated.class, subscriber); - var principal = "another-user"; - - var manifest = createNewParticipant().build(); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", createTokenFor(principal))) - .contentType(ContentType.JSON) - .body(manifest) - .post("/v1alpha/participants/") - .then() - .log().ifError() - .statusCode(401) - .body(notNullValue()); - verifyNoInteractions(subscriber); - assertThat(getKeyPairsForParticipant(manifest.getParticipantId())).isEmpty(); - } - - @Test - void activateParticipant_principalIsSuperser() { - var superUserKey = createSuperUser(); - var subscriber = mock(EventSubscriber.class); - getService(EventRouter.class).registerSync(ParticipantContextUpdated.class, subscriber); - - var participantId = "another-user"; - var anotherUser = ParticipantContext.Builder.newInstance() - .participantId(participantId) - .did("did:web:" + participantId) - .apiTokenAlias(participantId + "-alias") - .state(ParticipantContextState.CREATED) - .build(); - storeParticipant(anotherUser); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", superUserKey)) - .contentType(ContentType.JSON) - .post("/v1alpha/participants/%s/state?isActive=true".formatted(toBase64(participantId))) - .then() - .log().ifError() - .statusCode(204); - - var updatedParticipant = RUNTIME.getService(ParticipantContextService.class).getParticipantContext(participantId).orElseThrow(f -> new EdcException(f.getFailureDetail())); - assertThat(updatedParticipant.getState()).isEqualTo(ParticipantContextState.ACTIVATED.ordinal()); - // verify the correct event was emitted - verify(subscriber).on(argThat(env -> { - var evt = (ParticipantContextUpdated) env.getPayload(); - return evt.getParticipantId().equals(participantId) && evt.getNewState() == ParticipantContextState.ACTIVATED; - })); - - } - - @Test - void deleteParticipant() { - var superUserKey = createSuperUser(); - var participantId = "another-user"; - createParticipant(participantId); - - assertThat(getDidForParticipant(participantId)).hasSize(1); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", superUserKey)) - .contentType(ContentType.JSON) - .delete("/v1alpha/participants/%s".formatted(toBase64(participantId))) - .then() - .log().ifError() - .statusCode(204); - - assertThat(getDidForParticipant(participantId)).isEmpty(); - } - - @Test - void regenerateToken() { - var superUserKey = createSuperUser(); - var participantId = "another-user"; - var userToken = createParticipant(participantId); - - assertThat(Arrays.asList(userToken, superUserKey)) - .allSatisfy(t -> RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", t)) - .contentType(ContentType.JSON) - .post("/v1alpha/participants/%s/token".formatted(toBase64(participantId))) - .then() - .log().ifError() - .statusCode(200) - .body(notNullValue())); - } - - @Test - void updateRoles() { - var superUserKey = createSuperUser(); - var participantId = "some-user"; - createParticipant(participantId); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", superUserKey)) - .contentType(ContentType.JSON) - .body(List.of("role1", "role2", "admin")) - .put("/v1alpha/participants/%s/roles".formatted(toBase64(participantId))) - .then() - .log().ifError() - .statusCode(204); - - assertThat(getParticipant(participantId).getRoles()).containsExactlyInAnyOrder("role1", "role2", "admin"); - } - - @ParameterizedTest(name = "Expect 403, role = {0}") - @ValueSource(strings = { "some-role", "admin" }) - void updateRoles_whenNotSuperuser(String role) { - var participantId = "some-user"; - var userToken = createParticipant(participantId); - - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .header(new Header("x-api-key", userToken)) - .contentType(ContentType.JSON) - .body(List.of(role)) - .put("/v1alpha/participants/%s/roles".formatted(toBase64(participantId))) - .then() - .log().ifError() - .statusCode(403); - } - - @Test - void getAll() { - var superUserKey = createSuperUser(); - range(0, 10) - .forEach(i -> { - var participantId = "user" + i; - createParticipant(participantId); - }); - var found = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", superUserKey)) - .get("/v1alpha/participants") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(ParticipantContext[].class); - assertThat(found).hasSize(11); //10 + 1 for the super user - } - - @Test - void getAll_withPaging() { - var superUserKey = createSuperUser(); - range(0, 10) - .forEach(i -> { - var participantId = "user" + i; - createParticipant(participantId); // implicitly creates a keypair - }); - var found = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", superUserKey)) - .get("/v1alpha/participants?offset=2&limit=4") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(ParticipantContext[].class); - assertThat(found).hasSize(4); - } - - @Test - void getAll_withDefaultPaging() { - var superUserKey = createSuperUser(); - IntStream.range(0, 70) - .forEach(i -> { - var participantId = "user" + i; - createParticipant(participantId); // implicitly creates a keypair - }); - var found = RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", superUserKey)) - .get("/v1alpha/participants") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(ParticipantContext[].class); - assertThat(found).hasSize(50); +public class ParticipantContextApiEndToEndTest { + + abstract static class Tests { + + @AfterEach + void tearDown(ParticipantContextService pcService) { + // purge all users + pcService.query(QuerySpec.max()).getContent() + .forEach(pc -> pcService.deleteParticipantContext(pc.getParticipantId()).getContent()); + } + + @Test + void getUserById(IdentityHubEndToEndTestContext context) { + var apikey = context.createSuperUser(); + + var su = context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", apikey)) + .get("/v1alpha/participants/" + toBase64(SUPER_USER)) + .then() + .statusCode(200) + .extract().body().as(ParticipantContext.class); + assertThat(su.getParticipantId()).isEqualTo(SUPER_USER); + } + + @Test + void getUserById_notOwner_expect403(IdentityHubEndToEndTestContext context) { + var user1 = "user1"; + var user1Context = ParticipantContext.Builder.newInstance() + .participantId(user1) + .did("did:web:" + user1) + .apiTokenAlias(user1 + "-alias") + .build(); + var apiToken1 = context.storeParticipant(user1Context); + + var user2 = "user2"; + var user2Context = ParticipantContext.Builder.newInstance() + .participantId(user2) + .did("did:web:" + user2) + .apiTokenAlias(user2 + "-alias") + .build(); + var apiToken2 = context.storeParticipant(user2Context); + + //user1 attempts to read user2 -> fail + context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", apiToken1)) + .contentType(ContentType.JSON) + .get("/v1alpha/participants/" + toBase64(user2)) + .then() + .log().ifValidationFails() + .statusCode(403); + } + + @Test + void createNewUser_principalIsSuperser(IdentityHubEndToEndTestContext context, EventRouter router) { + var subscriber = mock(EventSubscriber.class); + router.registerSync(ParticipantContextCreated.class, subscriber); + var apikey = context.createSuperUser(); + + var manifest = context.createNewParticipant().build(); + + context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", apikey)) + .contentType(ContentType.JSON) + .body(manifest) + .post("/v1alpha/participants/") + .then() + .log().ifError() + .statusCode(anyOf(equalTo(200), equalTo(204))) + .body(notNullValue()); + + verify(subscriber).on(argThat(env -> ((ParticipantContextCreated) env.getPayload()).getParticipantId().equals(manifest.getParticipantId()))); + + assertThat(context.getKeyPairsForParticipant(manifest.getParticipantId())).hasSize(1); + assertThat(context.getDidForParticipant(manifest.getParticipantId())).hasSize(1) + .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).hasSize(1)); + } + + @ParameterizedTest(name = "Create participant with key pair active = {0}") + @ValueSource(booleans = { true, false }) + void createNewUser_verifyKeyPairActive(boolean isActive, IdentityHubEndToEndTestContext context, EventRouter router) { + var subscriber = mock(EventSubscriber.class); + router.registerSync(ParticipantContextCreated.class, subscriber); + var apikey = context.createSuperUser(); + + var participantId = UUID.randomUUID().toString(); + var manifest = context.createNewParticipant() + .participantId(participantId) + .did("did:web:" + participantId) + .key(context.createKeyDescriptor().active(isActive).build()) + .build(); + + context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", apikey)) + .contentType(ContentType.JSON) + .body(manifest) + .post("/v1alpha/participants/") + .then() + .log().ifError() + .statusCode(anyOf(equalTo(200), equalTo(204))) + .body(notNullValue()); + + verify(subscriber).on(argThat(env -> ((ParticipantContextCreated) env.getPayload()).getParticipantId().equals(manifest.getParticipantId()))); + + assertThat(context.getKeyPairsForParticipant(manifest.getParticipantId())).hasSize(1) + .allSatisfy(kpr -> assertThat(kpr.getState()).isEqualTo(isActive ? KeyPairState.ACTIVE.code() : KeyPairState.CREATED.code())); + assertThat(context.getDidForParticipant(manifest.getParticipantId())).hasSize(1) + .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).hasSize(1)); + + } + + + @Test + void createNewUser_principalIsNotSuperuser_expect403(IdentityHubEndToEndTestContext context, EventRouter router) { + var subscriber = mock(EventSubscriber.class); + router.registerSync(ParticipantContextCreated.class, subscriber); + + var principal = "another-user"; + var anotherUser = ParticipantContext.Builder.newInstance() + .participantId(principal) + .did("did:web:" + principal) + .apiTokenAlias(principal + "-alias") + .build(); + var apiToken = context.storeParticipant(anotherUser); + var manifest = context.createNewParticipant().build(); + + context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", apiToken)) + .contentType(ContentType.JSON) + .body(manifest) + .post("/v1alpha/participants/") + .then() + .log().ifError() + .statusCode(403) + .body(notNullValue()); + verifyNoInteractions(subscriber); + + assertThat(context.getKeyPairsForParticipant(manifest.getParticipantId())).isEmpty(); + } + + @Test + void createNewUser_principalIsKnown_expect401(IdentityHubEndToEndTestContext context, EventRouter router) { + var subscriber = mock(EventSubscriber.class); + router.registerSync(ParticipantContextCreated.class, subscriber); + var principal = "another-user"; + + var manifest = context.createNewParticipant().build(); + + context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", context.createTokenFor(principal))) + .contentType(ContentType.JSON) + .body(manifest) + .post("/v1alpha/participants/") + .then() + .log().ifError() + .statusCode(401) + .body(notNullValue()); + verifyNoInteractions(subscriber); + assertThat(context.getKeyPairsForParticipant(manifest.getParticipantId())).isEmpty(); + } + + @Test + void activateParticipant_principalIsSuperser(IdentityHubEndToEndTestContext context, ParticipantContextService participantContextService, EventRouter router) { + var superUserKey = context.createSuperUser(); + var subscriber = mock(EventSubscriber.class); + router.registerSync(ParticipantContextUpdated.class, subscriber); + + var participantId = "another-user"; + var anotherUser = ParticipantContext.Builder.newInstance() + .participantId(participantId) + .did("did:web:" + participantId) + .apiTokenAlias(participantId + "-alias") + .state(ParticipantContextState.CREATED) + .build(); + context.storeParticipant(anotherUser); + + context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", superUserKey)) + .contentType(ContentType.JSON) + .post("/v1alpha/participants/%s/state?isActive=true".formatted(toBase64(participantId))) + .then() + .log().ifError() + .statusCode(204); + + var updatedParticipant = participantContextService.getParticipantContext(participantId).orElseThrow(f -> new EdcException(f.getFailureDetail())); + assertThat(updatedParticipant.getState()).isEqualTo(ParticipantContextState.ACTIVATED.ordinal()); + // verify the correct event was emitted + verify(subscriber).on(argThat(env -> { + var evt = (ParticipantContextUpdated) env.getPayload(); + return evt.getParticipantId().equals(participantId) && evt.getNewState() == ParticipantContextState.ACTIVATED; + })); + + } + + @Test + void deleteParticipant(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var participantId = "another-user"; + context.createParticipant(participantId); + + assertThat(context.getDidForParticipant(participantId)).hasSize(1); + + context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", superUserKey)) + .contentType(ContentType.JSON) + .delete("/v1alpha/participants/%s".formatted(toBase64(participantId))) + .then() + .log().ifError() + .statusCode(204); + + assertThat(context.getDidForParticipant(participantId)).isEmpty(); + } + + @Test + void regenerateToken(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var participantId = "another-user"; + var userToken = context.createParticipant(participantId); + + assertThat(Arrays.asList(userToken, superUserKey)) + .allSatisfy(t -> context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", t)) + .contentType(ContentType.JSON) + .post("/v1alpha/participants/%s/token".formatted(toBase64(participantId))) + .then() + .log().ifError() + .statusCode(200) + .body(notNullValue())); + } + + @Test + void updateRoles(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var participantId = "some-user"; + context.createParticipant(participantId); + + context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", superUserKey)) + .contentType(ContentType.JSON) + .body(List.of("role1", "role2", "admin")) + .put("/v1alpha/participants/%s/roles".formatted(toBase64(participantId))) + .then() + .log().ifError() + .statusCode(204); + + assertThat(context.getParticipant(participantId).getRoles()).containsExactlyInAnyOrder("role1", "role2", "admin"); + } + + @ParameterizedTest(name = "Expect 403, role = {0}") + @ValueSource(strings = { "some-role", "admin" }) + void updateRoles_whenNotSuperuser(String role, IdentityHubEndToEndTestContext context) { + var participantId = "some-user"; + var userToken = context.createParticipant(participantId); + + context.getIdentityApiEndpoint().baseRequest() + .header(new Header("x-api-key", userToken)) + .contentType(ContentType.JSON) + .body(List.of(role)) + .put("/v1alpha/participants/%s/roles".formatted(toBase64(participantId))) + .then() + .log().ifError() + .statusCode(403); + } + + @Test + void getAll(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + range(0, 10) + .forEach(i -> { + var participantId = "user" + i; + context.createParticipant(participantId); + }); + var found = context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", superUserKey)) + .get("/v1alpha/participants") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(ParticipantContext[].class); + assertThat(found).hasSize(11); //10 + 1 for the super user + } + + @Test + void getAll_withPaging(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + range(0, 10) + .forEach(i -> { + var participantId = "user" + i; + context.createParticipant(participantId); // implicitly creates a keypair + }); + var found = context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", superUserKey)) + .get("/v1alpha/participants?offset=2&limit=4") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(ParticipantContext[].class); + assertThat(found).hasSize(4); + } + + @Test + void getAll_withDefaultPaging(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + IntStream.range(0, 70) + .forEach(i -> { + var participantId = "user" + i; + context.createParticipant(participantId); // implicitly creates a keypair + }); + var found = context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", superUserKey)) + .get("/v1alpha/participants") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(ParticipantContext[].class); + assertThat(found).hasSize(50); + } + + @Test + void getAll_notAuthorized(IdentityHubEndToEndTestContext context) { + var attackerToken = context.createParticipant("attacker"); + + range(0, 10) + .forEach(i -> { + var participantId = "user" + i; + context.createParticipant(participantId); // implicitly creates a keypair + }); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", attackerToken)) + .get("/v1alpha/participants") + .then() + .log().ifValidationFails() + .statusCode(403); + } + + private String toBase64(String s) { + return Base64.getUrlEncoder().encodeToString(s.getBytes()); + } } - @Test - void getAll_notAuthorized() { - var attackerToken = createParticipant("attacker"); - - range(0, 10) - .forEach(i -> { - var participantId = "user" + i; - createParticipant(participantId); // implicitly creates a keypair - }); - RUNTIME_CONFIGURATION.getIdentityApiEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", attackerToken)) - .get("/v1alpha/participants") - .then() - .log().ifValidationFails() - .statusCode(403); + @Nested + @EndToEndTest + @ExtendWith(IdentityHubEndToEndExtension.InMemory.class) + class InMemory extends Tests { } - private String toBase64(String s) { - return Base64.getUrlEncoder().encodeToString(s.getBytes()); + @Nested + @PostgresqlIntegrationTest + @ExtendWith(IdentityHubEndToEndExtension.Postgres.class) + class Postgres extends Tests { } } diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiEndToEndTest.java index 0c93b44a8..a4a5a2a2a 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiEndToEndTest.java @@ -37,19 +37,23 @@ import org.eclipse.edc.identityhub.spi.store.CredentialStore; import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VcStatus; import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource; -import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubRuntimeConfiguration; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubCustomizableEndToEndExtension; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndExtension; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndTestContext; +import org.eclipse.edc.identityhub.tests.fixtures.PostgresSqlService; import org.eclipse.edc.identityhub.tests.fixtures.TestData; import org.eclipse.edc.jsonld.util.JacksonJsonLd; import org.eclipse.edc.junit.annotations.EndToEndTest; -import org.eclipse.edc.junit.extensions.EmbeddedRuntime; -import org.eclipse.edc.junit.extensions.RuntimeExtension; -import org.eclipse.edc.junit.extensions.RuntimePerClassExtension; +import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.security.Vault; import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -73,434 +77,467 @@ import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.JwtCreationUtil.generateJwt; import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.JwtCreationUtil.generateSiToken; import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.VerifiableCredentialTestUtil.generateEcKey; +import static org.eclipse.edc.util.io.Ports.getFreePort; import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@EndToEndTest public class PresentationApiEndToEndTest { - protected static final IdentityHubRuntimeConfiguration IDENTITY_HUB_PARTICIPANT = IdentityHubRuntimeConfiguration.Builder.newInstance() - .name("identity-hub") - .id("identity-hub") - .build(); - private static final String VALID_QUERY_WITH_SCOPE = """ - { - "@context": [ - "https://identity.foundation/presentation-exchange/submission/v1", - "https://w3id.org/tractusx-trust/v0.8" - ], - "@type": "PresentationQueryMessage", - "scope":[ - "org.eclipse.edc.vc.type:AlumniCredential:read" - ] - } - """; - private static final String VALID_QUERY_WITH_ADDITIONAL_SCOPE = """ - { - "@context": [ - "https://identity.foundation/presentation-exchange/submission/v1", - "https://w3id.org/tractusx-trust/v0.8" - ], - "@type": "PresentationQueryMessage", - "scope":[ - "org.eclipse.edc.vc.type:AlumniCredential:read", - "org.eclipse.edc.vc.type:SuperSecretCredential:*" - ] - } - """; - private static final String TEST_PARTICIPANT_CONTEXT_ID = "consumer"; - private static final String TEST_PARTICIPANT_CONTEXT_ID_ENCODED = Base64.getUrlEncoder().encodeToString(TEST_PARTICIPANT_CONTEXT_ID.getBytes()); - private static final DidPublicKeyResolver DID_PUBLIC_KEY_RESOLVER = mock(); - private static final RevocationListService REVOCATION_LIST_SERVICE = mock(); - private static final ObjectMapper OBJECT_MAPPER = JacksonJsonLd.createObjectMapper() - .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - .enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT); - @RegisterExtension - static RuntimeExtension runtime; - - static { - runtime = new RuntimePerClassExtension(new EmbeddedRuntime("identity-hub", IDENTITY_HUB_PARTICIPANT.controlPlaneConfiguration(), ":launcher")); - runtime.registerServiceMock(DidPublicKeyResolver.class, DID_PUBLIC_KEY_RESOLVER); - runtime.registerServiceMock(RevocationListService.class, REVOCATION_LIST_SERVICE); - } - - @BeforeEach - void setup() { - createParticipant(); - } - - @AfterEach - void teardown() { - // purge all participant contexts - runtime.getService(ParticipantContextService.class).deleteParticipantContext(TEST_PARTICIPANT_CONTEXT_ID) - .orElseThrow(f -> new RuntimeException(f.getFailureDetail())); - - // purge all VCs - var store = runtime.getService(CredentialStore.class); - store.query(QuerySpec.none()) - .map(creds -> creds.stream().map(cred -> store.deleteById(cred.getId())).toList()) - .orElseThrow(f -> new RuntimeException(f.getFailureDetail())); - } - - @Test - void query_tokenNotPresent_shouldReturn401() { + abstract static class Tests { - IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() - .contentType("application/json") - .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) - .then() - .statusCode(401) - .extract().body().asString(); - } - - @Test - void query_validationError_shouldReturn400() { - - var query = """ + protected static final DidPublicKeyResolver DID_PUBLIC_KEY_RESOLVER = mock(); + protected static final RevocationListService REVOCATION_LIST_SERVICE = mock(); + private static final String VALID_QUERY_WITH_SCOPE = """ { "@context": [ - "https://identity.foundation/participants/test-participant/presentation-exchange/submission/v1", + "https://identity.foundation/presentation-exchange/submission/v1", "https://w3id.org/tractusx-trust/v0.8" ], - "@type": "PresentationQueryMessage" + "@type": "PresentationQueryMessage", + "scope":[ + "org.eclipse.edc.vc.type:AlumniCredential:read" + ] } """; - IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() - .contentType(JSON) - .header(AUTHORIZATION, generateSiToken()) - .body(query) - .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) - .then() - .statusCode(400) - .extract().body().asString(); - - } - - @Test - void query_withPresentationDefinition_shouldReturn503() { - - var query = """ + private static final String VALID_QUERY_WITH_ADDITIONAL_SCOPE = """ { "@context": [ "https://identity.foundation/presentation-exchange/submission/v1", "https://w3id.org/tractusx-trust/v0.8" ], "@type": "PresentationQueryMessage", - "presentationDefinition":{ - } + "scope":[ + "org.eclipse.edc.vc.type:AlumniCredential:read", + "org.eclipse.edc.vc.type:SuperSecretCredential:*" + ] } """; - IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() - .contentType(JSON) - .header(AUTHORIZATION, generateSiToken()) - .body(query) - .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) - .then() - .statusCode(503) - .extract().body().asString(); - } + private static final String TEST_PARTICIPANT_CONTEXT_ID = "consumer"; + private static final String TEST_PARTICIPANT_CONTEXT_ID_ENCODED = Base64.getUrlEncoder().encodeToString(TEST_PARTICIPANT_CONTEXT_ID.getBytes()); + private static final ObjectMapper OBJECT_MAPPER = JacksonJsonLd.createObjectMapper() + .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + .enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT); - @Test - void query_tokenVerificationFails_shouldReturn401() throws JOSEException { + @BeforeEach + void setup(IdentityHubEndToEndTestContext context) { + createParticipant(context); + } - var spoofedKey = new ECKeyGenerator(Curve.P_256).keyID("did:web:provider#key1").generate(); - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(spoofedKey.toPublicKey())); + @AfterEach + void teardown(ParticipantContextService contextService, CredentialStore store) { + // purge all participant contexts + contextService.query(QuerySpec.none()) + .onSuccess(participantContexts -> participantContexts.forEach(participant -> { + contextService.deleteParticipantContext(participant.getParticipantId()); + })) + .orElseThrow(f -> new RuntimeException(f.getFailureDetail())); + + // purge all VCs + store.query(QuerySpec.none()) + .map(creds -> creds.stream().map(cred -> store.deleteById(cred.getId())).toList()) + .orElseThrow(f -> new RuntimeException(f.getFailureDetail())); + } - var token = generateSiToken(); - IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() - .contentType(JSON) - .header(AUTHORIZATION, token) - .body(VALID_QUERY_WITH_SCOPE) - .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) - .then() - .statusCode(401) - .log().ifValidationFails() - .body("[0].type", equalTo("AuthenticationFailed")) - .body("[0].message", equalTo("ID token verification failed: Token verification failed")); - } + @Test + void query_tokenNotPresent_shouldReturn401(IdentityHubEndToEndTestContext context) { - @Test - void query_credentialQueryResolverFails_shouldReturn403() throws JOSEException, JsonProcessingException { - - var token = generateSiToken(); - - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); - - // create the credential in the store - var store = runtime.getService(CredentialStore.class); - var cred = OBJECT_MAPPER.readValue(TestData.VC_EXAMPLE, VerifiableCredential.class); - var res = VerifiableCredentialResource.Builder.newInstance() - .state(VcStatus.ISSUED) - .credential(new VerifiableCredentialContainer(TestData.VC_EXAMPLE, CredentialFormat.JWT, cred)) - .issuerId("https://example.edu/issuers/565049") - .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") - .participantId(TEST_PARTICIPANT_CONTEXT_ID) - .build(); - store.create(res); - - // create another credential in the store - var cred2 = OBJECT_MAPPER.readValue(TestData.VC_EXAMPLE_2, VerifiableCredential.class); - var res2 = VerifiableCredentialResource.Builder.newInstance() - .state(VcStatus.ISSUED) - .credential(new VerifiableCredentialContainer(TestData.VC_EXAMPLE_2, CredentialFormat.JWT, cred2)) - .issuerId("https://example.edu/issuers/12345") - .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") - .participantId(TEST_PARTICIPANT_CONTEXT_ID) - .build(); - store.create(res2); - - - IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() - .contentType(JSON) - .header(AUTHORIZATION, token) - .body(VALID_QUERY_WITH_ADDITIONAL_SCOPE) - .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) - .then() - .log().ifError() - .statusCode(403) - .body("[0].type", equalTo("NotAuthorized")) - .body("[0].message", equalTo("Invalid query: requested Credentials outside of scope.")); - } + context.getResolutionEndpoint().baseRequest() + .contentType("application/json") + .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) + .then() + .statusCode(401) + .extract().body().asString(); + } - @Test - void query_success_noCredentials() throws JOSEException { + @Test + void query_validationError_shouldReturn400(IdentityHubEndToEndTestContext context) { + + var query = """ + { + "@context": [ + "https://identity.foundation/participants/test-participant/presentation-exchange/submission/v1", + "https://w3id.org/tractusx-trust/v0.8" + ], + "@type": "PresentationQueryMessage" + } + """; + context.getResolutionEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, generateSiToken()) + .body(query) + .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) + .then() + .statusCode(400) + .extract().body().asString(); - var token = generateSiToken(); + } - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); + @Test + void query_withPresentationDefinition_shouldReturn503(IdentityHubEndToEndTestContext context) { + + var query = """ + { + "@context": [ + "https://identity.foundation/presentation-exchange/submission/v1", + "https://w3id.org/tractusx-trust/v0.8" + ], + "@type": "PresentationQueryMessage", + "presentationDefinition":{ + } + } + """; + context.getResolutionEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, generateSiToken()) + .body(query) + .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) + .then() + .statusCode(503) + .extract().body().asString(); + } - var response = IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() - .contentType(JSON) - .header(AUTHORIZATION, token) - .body(VALID_QUERY_WITH_SCOPE) - .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) - .then() - .statusCode(200) - .log().ifError() - .extract().body().as(JsonObject.class); + @Test + void query_tokenVerificationFails_shouldReturn401(IdentityHubEndToEndTestContext context) throws JOSEException { - assertThat(response) - .hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage")) - .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(2)) - .hasEntrySatisfying("presentation", jsonValue -> assertThat(extractCredentials(((JsonString) jsonValue).getString())).isEmpty()); - } + var spoofedKey = new ECKeyGenerator(Curve.P_256).keyID("did:web:provider#key1").generate(); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(spoofedKey.toPublicKey())); - @Test - void query_success_containsCredential() throws JOSEException, JsonProcessingException { - - var store = runtime.getService(CredentialStore.class); - var cred = OBJECT_MAPPER.readValue(TestData.VC_EXAMPLE, VerifiableCredential.class); - var res = VerifiableCredentialResource.Builder.newInstance() - .state(VcStatus.ISSUED) - .credential(new VerifiableCredentialContainer(TestData.VC_EXAMPLE, CredentialFormat.JWT, cred)) - .issuerId("https://example.edu/issuers/565049") - .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") - .participantId(TEST_PARTICIPANT_CONTEXT_ID) - .build(); - - store.create(res); - var token = generateSiToken(); - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); - - var response = IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() - .contentType(JSON) - .header(AUTHORIZATION, token) - .body(VALID_QUERY_WITH_SCOPE) - .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) - .then() - .statusCode(200) - .log().ifError() - .extract().body().as(JsonObject.class); - - assertThat(response) - .hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage")) - .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(2)) - .hasEntrySatisfying("presentation", jsonValue -> { - assertThat(jsonValue.getValueType()).isEqualTo(JsonValue.ValueType.STRING); - var vpToken = ((JsonString) jsonValue).getString(); - assertThat(vpToken).isNotNull(); - assertThat(extractCredentials(vpToken)).isNotEmpty(); - }); + var token = generateSiToken(); + context.getResolutionEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_QUERY_WITH_SCOPE) + .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) + .then() + .statusCode(401) + .log().ifValidationFails() + .body("[0].type", equalTo("AuthenticationFailed")) + .body("[0].message", equalTo("ID token verification failed: Token verification failed")); + } - } + @Test + void query_credentialQueryResolverFails_shouldReturn403(IdentityHubEndToEndTestContext context, CredentialStore store) throws JOSEException, JsonProcessingException { + + var token = generateSiToken(); + + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); + + // create the credential in the store + var cred = OBJECT_MAPPER.readValue(TestData.VC_EXAMPLE, VerifiableCredential.class); + var res = VerifiableCredentialResource.Builder.newInstance() + .state(VcStatus.ISSUED) + .credential(new VerifiableCredentialContainer(TestData.VC_EXAMPLE, CredentialFormat.JWT, cred)) + .issuerId("https://example.edu/issuers/565049") + .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") + .participantId(TEST_PARTICIPANT_CONTEXT_ID) + .build(); + store.create(res); + + // create another credential in the store + var cred2 = OBJECT_MAPPER.readValue(TestData.VC_EXAMPLE_2, VerifiableCredential.class); + var res2 = VerifiableCredentialResource.Builder.newInstance() + .state(VcStatus.ISSUED) + .credential(new VerifiableCredentialContainer(TestData.VC_EXAMPLE_2, CredentialFormat.JWT, cred2)) + .issuerId("https://example.edu/issuers/12345") + .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") + .participantId(TEST_PARTICIPANT_CONTEXT_ID) + .build(); + store.create(res2); + + + context.getResolutionEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_QUERY_WITH_ADDITIONAL_SCOPE) + .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) + .then() + .log().ifError() + .statusCode(403) + .body("[0].type", equalTo("NotAuthorized")) + .body("[0].message", equalTo("Invalid query: requested Credentials outside of scope.")); + } + + @Test + void query_success_noCredentials(IdentityHubEndToEndTestContext context) throws JOSEException { - @ParameterizedTest(name = "VcState code: {0}") - @ValueSource(ints = { 600, 700, 800, 900 }) - void query_shouldFilterOutInvalidCreds(int vcStateCode) throws JOSEException, JsonProcessingException { + var token = generateSiToken(); - var store = runtime.getService(CredentialStore.class); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); + + var response = context.getResolutionEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_QUERY_WITH_SCOPE) + .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) + .then() + .statusCode(200) + .log().ifError() + .extract().body().as(JsonObject.class); + + assertThat(response) + .hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage")) + .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(2)) + .hasEntrySatisfying("presentation", jsonValue -> assertThat(extractCredentials(((JsonString) jsonValue).getString())).isEmpty()); - // modify VC content, so that it becomes either not-yet-valid or expired - var vcContent = TestData.VC_EXAMPLE; - if (vcStateCode == VcStatus.EXPIRED.code()) { - var expirationInPast = Instant.now().minus(1, ChronoUnit.DAYS).toString(); - vcContent = vcContent.replaceAll("\"expirationDate\": \"2999-01-01T19:23:24Z\",", "\"expirationDate\": \"" + expirationInPast + "\","); - } else if (vcStateCode == VcStatus.NOT_YET_VALID.code()) { - var futureIssuance = Instant.now().plus(1, ChronoUnit.DAYS).toString(); - vcContent = vcContent.replaceAll("\"issuanceDate\": \".*\",", "\"issuanceDate\": \"" + futureIssuance + "\","); } + @Test + void query_success_containsCredential(IdentityHubEndToEndTestContext context, CredentialStore store) throws JOSEException, JsonProcessingException { + + var cred = OBJECT_MAPPER.readValue(TestData.VC_EXAMPLE, VerifiableCredential.class); + var res = VerifiableCredentialResource.Builder.newInstance() + .state(VcStatus.ISSUED) + .credential(new VerifiableCredentialContainer(TestData.VC_EXAMPLE, CredentialFormat.JWT, cred)) + .issuerId("https://example.edu/issuers/565049") + .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") + .participantId(TEST_PARTICIPANT_CONTEXT_ID) + .build(); + + store.create(res); + var token = generateSiToken(); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); + + var response = context.getResolutionEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_QUERY_WITH_SCOPE) + .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) + .then() + .statusCode(200) + .log().ifError() + .extract().body().as(JsonObject.class); + + assertThat(response) + .hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage")) + .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(2)) + .hasEntrySatisfying("presentation", jsonValue -> { + assertThat(jsonValue.getValueType()).isEqualTo(JsonValue.ValueType.STRING); + var vpToken = ((JsonString) jsonValue).getString(); + assertThat(vpToken).isNotNull(); + assertThat(extractCredentials(vpToken)).isNotEmpty(); + }); - var cred = OBJECT_MAPPER.readValue(vcContent, VerifiableCredential.class); - // inject a CredentialStatus object, that triggers the revocation check - if (vcStateCode == VcStatus.SUSPENDED.code()) { - cred.getCredentialStatus().add(new CredentialStatus("test-cred-stat-id", "StatusList2021Entry", - Map.of("statusListCredential", "https://university.example/credentials/status/3", - "statusPurpose", "suspension", - "statusListIndex", 69))); - when(REVOCATION_LIST_SERVICE.checkValidity(any(VerifiableCredential.class))) - .thenReturn(Result.failure("suspended")); } - // create the credential in the store - var res = VerifiableCredentialResource.Builder.newInstance() - .state(VcStatus.from(vcStateCode)) - .credential(new VerifiableCredentialContainer(vcContent, CredentialFormat.JWT, cred)) - .issuerId("https://example.edu/issuers/565049") - .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") - .participantId(TEST_PARTICIPANT_CONTEXT_ID) - .build(); - store.create(res); - - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); - - var token = generateSiToken(); - var response = IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() - .contentType(JSON) - .header(AUTHORIZATION, token) - .body(VALID_QUERY_WITH_SCOPE) - .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) - .then() - .statusCode(200) - .log().ifError() - .extract().body().as(JsonObject.class); - - assertThat(response) - .hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage")) - .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(2)) - .hasEntrySatisfying("presentation", jsonValue -> { - assertThat(jsonValue.getValueType()).isEqualTo(JsonValue.ValueType.STRING); - var vpToken = ((JsonString) jsonValue).getString(); - assertThat(vpToken).isNotNull(); - assertThat(extractCredentials(vpToken)).isEmpty(); // credential should be filtered out - }); - } + @ParameterizedTest(name = "VcState code: {0}") + @ValueSource(ints = { 600, 700, 800, 900 }) + void query_shouldFilterOutInvalidCreds(int vcStateCode, IdentityHubEndToEndTestContext context, CredentialStore store) throws JOSEException, JsonProcessingException { + + // modify VC content, so that it becomes either not-yet-valid or expired + var vcContent = TestData.VC_EXAMPLE; + if (vcStateCode == VcStatus.EXPIRED.code()) { + var expirationInPast = Instant.now().minus(1, ChronoUnit.DAYS).toString(); + vcContent = vcContent.replaceAll("\"expirationDate\": \"2999-01-01T19:23:24Z\",", "\"expirationDate\": \"" + expirationInPast + "\","); + } else if (vcStateCode == VcStatus.NOT_YET_VALID.code()) { + var futureIssuance = Instant.now().plus(1, ChronoUnit.DAYS).toString(); + vcContent = vcContent.replaceAll("\"issuanceDate\": \".*\",", "\"issuanceDate\": \"" + futureIssuance + "\","); + } - @Test - void query_accessTokenKeyIdDoesNotBelongToParticipant_shouldReturn401() throws JsonProcessingException, JOSEException { - - createParticipant("attacker", generateEcKey("did:web:attacker#key-1")); - - var store = runtime.getService(CredentialStore.class); - var cred = OBJECT_MAPPER.readValue(TestData.VC_EXAMPLE, VerifiableCredential.class); - var res = VerifiableCredentialResource.Builder.newInstance() - .state(VcStatus.ISSUED) - .credential(new VerifiableCredentialContainer(TestData.VC_EXAMPLE, CredentialFormat.JWT, cred)) - .issuerId("https://example.edu/issuers/565049") - .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") - .participantId(TEST_PARTICIPANT_CONTEXT_ID) - .build(); - - store.create(res); - var token = generateSiToken(); - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); - - IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() - .contentType(JSON) - .header(AUTHORIZATION, token) - .body(VALID_QUERY_WITH_SCOPE) - // attempt to request the presentation for a different participant than the one who issued the access token - .post("/v1/participants/%s/presentations/query".formatted(Base64.getUrlEncoder().encodeToString("attacker".getBytes()))) - .then() - .statusCode(401) - .log().ifValidationFails(); - } + var cred = OBJECT_MAPPER.readValue(vcContent, VerifiableCredential.class); + // inject a CredentialStatus object, that triggers the revocation check + if (vcStateCode == VcStatus.SUSPENDED.code()) { + cred.getCredentialStatus().add(new CredentialStatus("test-cred-stat-id", "StatusList2021Entry", + Map.of("statusListCredential", "https://university.example/credentials/status/3", + "statusPurpose", "suspension", + "statusListIndex", 69))); + when(REVOCATION_LIST_SERVICE.checkValidity(any(VerifiableCredential.class))) + .thenReturn(Result.failure("suspended")); + } + // create the credential in the store + var res = VerifiableCredentialResource.Builder.newInstance() + .state(VcStatus.from(vcStateCode)) + .credential(new VerifiableCredentialContainer(vcContent, CredentialFormat.JWT, cred)) + .issuerId("https://example.edu/issuers/565049") + .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") + .participantId(TEST_PARTICIPANT_CONTEXT_ID) + .build(); + store.create(res); + + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); + + var token = generateSiToken(); + var response = context.getResolutionEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_QUERY_WITH_SCOPE) + .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) + .then() + .statusCode(200) + .log().ifError() + .extract().body().as(JsonObject.class); + + assertThat(response) + .hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage")) + .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(2)) + .hasEntrySatisfying("presentation", jsonValue -> { + assertThat(jsonValue.getValueType()).isEqualTo(JsonValue.ValueType.STRING); + var vpToken = ((JsonString) jsonValue).getString(); + assertThat(vpToken).isNotNull(); + assertThat(extractCredentials(vpToken)).isEmpty(); // credential should be filtered out + }); - @Test - void query_accessTokenAudienceDoesNotBelongToParticipant_shouldReturn401() throws JsonProcessingException, JOSEException { - - var store = runtime.getService(CredentialStore.class); - var cred = OBJECT_MAPPER.readValue(TestData.VC_EXAMPLE, VerifiableCredential.class); - var res = VerifiableCredentialResource.Builder.newInstance() - .state(VcStatus.ISSUED) - .credential(new VerifiableCredentialContainer(TestData.VC_EXAMPLE, CredentialFormat.JWT, cred)) - .issuerId("https://example.edu/issuers/565049") - .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") - .participantId(TEST_PARTICIPANT_CONTEXT_ID) - .build(); - - store.create(res); - - var accessToken = generateJwt("did:web:someone_else", "did:web:someone_else", PROVIDER_DID, Map.of("scope", TEST_SCOPE), CONSUMER_KEY); - var token = generateJwt(CONSUMER_DID, PROVIDER_DID, PROVIDER_DID, Map.of("client_id", PROVIDER_DID, "token", accessToken), PROVIDER_KEY); - - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); - when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); - - IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() - .contentType(JSON) - .header(AUTHORIZATION, token) - .body(VALID_QUERY_WITH_SCOPE) - // attempt to request the presentation for a different participant than the one who issued the access token - .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) - .then() - .statusCode(401) - .log().ifValidationFails() - .body(Matchers.containsString("The DID associated with the Participant Context ID of this request ('did:web:consumer') must match 'aud' claim in 'access_token' ([did:web:someone_else]).")); - } + } - /** - * extracts a (potentially empty) list of verifiable credentials from a JWT-VP - */ - @SuppressWarnings("unchecked") - private List extractCredentials(String vpToken) { - try { - var jwt = SignedJWT.parse(vpToken); - var vpClaim = jwt.getJWTClaimsSet().getClaim("vp"); - if (vpClaim == null) return List.of(); + @Test + void query_accessTokenKeyIdDoesNotBelongToParticipant_shouldReturn401(IdentityHubEndToEndTestContext context, CredentialStore store) throws JsonProcessingException, JOSEException { + + createParticipant(context, "attacker", generateEcKey("did:web:attacker#key-1")); + + var cred = OBJECT_MAPPER.readValue(TestData.VC_EXAMPLE, VerifiableCredential.class); + var res = VerifiableCredentialResource.Builder.newInstance() + .state(VcStatus.ISSUED) + .credential(new VerifiableCredentialContainer(TestData.VC_EXAMPLE, CredentialFormat.JWT, cred)) + .issuerId("https://example.edu/issuers/565049") + .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") + .participantId(TEST_PARTICIPANT_CONTEXT_ID) + .build(); + + store.create(res); + var token = generateSiToken(); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); + + context.getResolutionEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_QUERY_WITH_SCOPE) + // attempt to request the presentation for a different participant than the one who issued the access token + .post("/v1/participants/%s/presentations/query".formatted(Base64.getUrlEncoder().encodeToString("attacker".getBytes()))) + .then() + .statusCode(401) + .log().ifValidationFails(); - Map map = (Map) OBJECT_MAPPER.convertValue(vpClaim, Map.class); + } + + @Test + void query_accessTokenAudienceDoesNotBelongToParticipant_shouldReturn401(IdentityHubEndToEndTestContext context, CredentialStore store) throws JsonProcessingException, JOSEException { + + var cred = OBJECT_MAPPER.readValue(TestData.VC_EXAMPLE, VerifiableCredential.class); + var res = VerifiableCredentialResource.Builder.newInstance() + .state(VcStatus.ISSUED) + .credential(new VerifiableCredentialContainer(TestData.VC_EXAMPLE, CredentialFormat.JWT, cred)) + .issuerId("https://example.edu/issuers/565049") + .holderId("did:example:ebfeb1f712ebc6f1c276e12ec21") + .participantId(TEST_PARTICIPANT_CONTEXT_ID) + .build(); + + store.create(res); + + var accessToken = generateJwt("did:web:someone_else", "did:web:someone_else", PROVIDER_DID, Map.of("scope", TEST_SCOPE), CONSUMER_KEY); + var token = generateJwt(CONSUMER_DID, PROVIDER_DID, PROVIDER_DID, Map.of("client_id", PROVIDER_DID, "token", accessToken), PROVIDER_KEY); + + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey())); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey())); + + context.getResolutionEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_QUERY_WITH_SCOPE) + // attempt to request the presentation for a different participant than the one who issued the access token + .post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID_ENCODED)) + .then() + .statusCode(401) + .log().ifValidationFails() + .body(Matchers.containsString("The DID associated with the Participant Context ID of this request ('did:web:consumer') must match 'aud' claim in 'access_token' ([did:web:someone_else]).")); + } + + /** + * extracts a (potentially empty) list of verifiable credentials from a JWT-VP + */ + @SuppressWarnings("unchecked") + private List extractCredentials(String vpToken) { + try { + var jwt = SignedJWT.parse(vpToken); + var vpClaim = jwt.getJWTClaimsSet().getClaim("vp"); + if (vpClaim == null) return List.of(); - return (List) map.get("verifiableCredential"); + Map map = (Map) OBJECT_MAPPER.convertValue(vpClaim, Map.class); - } catch (ParseException e) { - throw new RuntimeException(e); + return (List) map.get("verifiableCredential"); + + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + private void createParticipant(IdentityHubEndToEndTestContext context) { + createParticipant(context, TEST_PARTICIPANT_CONTEXT_ID, CONSUMER_KEY); + } + + private void createParticipant(IdentityHubEndToEndTestContext context, String participantContextId, ECKey participantKey) { + var service = context.getRuntime().getService(ParticipantContextService.class); + var vault = context.getRuntime().getService(Vault.class); + + var privateKeyAlias = "%s-privatekey-alias".formatted(participantContextId); + vault.storeSecret(privateKeyAlias, participantKey.toJSONString()); + var manifest = ParticipantManifest.Builder.newInstance() + .participantId(participantContextId) + .did("did:web:%s".formatted(participantContextId.replace("did:web:", ""))) + .active(true) + .key(KeyDescriptor.Builder.newInstance() + .publicKeyJwk(participantKey.toPublicJWK().toJSONObject()) + .privateKeyAlias(privateKeyAlias) + .keyId(participantKey.getKeyID()) + .build()) + .build(); + service.createParticipantContext(manifest) + .orElseThrow(f -> new RuntimeException(f.getFailureDetail())); } - } - private void createParticipant() { - createParticipant(TEST_PARTICIPANT_CONTEXT_ID, CONSUMER_KEY); } - private void createParticipant(String participantContextId, ECKey participantKey) { - var service = runtime.getService(ParticipantContextService.class); - var vault = runtime.getService(Vault.class); - - var privateKeyAlias = "%s-privatekey-alias".formatted(participantContextId); - vault.storeSecret(privateKeyAlias, participantKey.toJSONString()); - var manifest = ParticipantManifest.Builder.newInstance() - .participantId(participantContextId) - .did("did:web:%s".formatted(participantContextId.replace("did:web:", ""))) - .active(true) - .key(KeyDescriptor.Builder.newInstance() - .publicKeyJwk(participantKey.toPublicJWK().toJSONObject()) - .privateKeyAlias(privateKeyAlias) - .keyId(participantKey.getKeyID()) - .build()) - .build(); - service.createParticipantContext(manifest) - .orElseThrow(f -> new RuntimeException(f.getFailureDetail())); + @Nested + @EndToEndTest + class InMemory extends Tests { + + @RegisterExtension + static IdentityHubCustomizableEndToEndExtension runtime; + + static { + var ctx = IdentityHubEndToEndExtension.InMemory.context(); + ctx.getRuntime().registerServiceMock(DidPublicKeyResolver.class, DID_PUBLIC_KEY_RESOLVER); + ctx.getRuntime().registerServiceMock(RevocationListService.class, REVOCATION_LIST_SERVICE); + runtime = new IdentityHubCustomizableEndToEndExtension(ctx); + } } + @Nested + @PostgresqlIntegrationTest + class Postgres extends Tests { + + private static final String DB_NAME = "runtime"; + private static final Integer DB_PORT = getFreePort(); + + @RegisterExtension + static IdentityHubCustomizableEndToEndExtension runtime; + static PostgresSqlService server = new PostgresSqlService(DB_NAME, DB_PORT); + + static { + var ctx = IdentityHubEndToEndExtension.Postgres.context(DB_NAME, DB_PORT); + ctx.getRuntime().registerServiceMock(DidPublicKeyResolver.class, DID_PUBLIC_KEY_RESOLVER); + ctx.getRuntime().registerServiceMock(RevocationListService.class, REVOCATION_LIST_SERVICE); + runtime = new IdentityHubCustomizableEndToEndExtension(ctx); + } + + @BeforeAll + static void beforeAll() { + server.start(); + } + + @AfterAll + static void afterAll() { + server.stop(); + } + + } } diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubCustomizableEndToEndExtension.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubCustomizableEndToEndExtension.java new file mode 100644 index 000000000..1244c379b --- /dev/null +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubCustomizableEndToEndExtension.java @@ -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 + * + */ + +package org.eclipse.edc.identityhub.tests.fixtures; + +/** + * Variant of {@link IdentityHubEndToEndExtension} where the context {@link IdentityHubEndToEndTestContext} + * is provided + */ +public class IdentityHubCustomizableEndToEndExtension extends IdentityHubEndToEndExtension { + + public IdentityHubCustomizableEndToEndExtension(IdentityHubEndToEndTestContext context) { + super(context); + } + +} diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndExtension.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndExtension.java new file mode 100644 index 000000000..b422f59d0 --- /dev/null +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndExtension.java @@ -0,0 +1,149 @@ +/* + * 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.tests.fixtures; + +import org.eclipse.edc.junit.extensions.EmbeddedRuntime; +import org.eclipse.edc.junit.extensions.RuntimePerClassExtension; +import org.eclipse.edc.sql.testfixtures.PostgresqlEndToEndInstance; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; + +import java.util.HashMap; +import java.util.Map; + +import static org.eclipse.edc.identityhub.tests.fixtures.PostgresSqlService.jdbcUrl; +import static org.eclipse.edc.util.io.Ports.getFreePort; + +/** + * Base extension of {@link RuntimePerClassExtension} that injects the {@link IdentityHubEndToEndTestContext} + * when required. + */ +public abstract class IdentityHubEndToEndExtension extends RuntimePerClassExtension { + + private final IdentityHubEndToEndTestContext context; + + protected IdentityHubEndToEndExtension(IdentityHubEndToEndTestContext context) { + super(context.getRuntime()); + this.context = context; + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + var type = parameterContext.getParameter().getParameterizedType(); + if (type.equals(IdentityHubEndToEndTestContext.class)) { + return true; + } + return super.supportsParameter(parameterContext, extensionContext); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + var type = parameterContext.getParameter().getParameterizedType(); + if (type.equals(IdentityHubEndToEndTestContext.class)) { + return context; + } + return super.resolveParameter(parameterContext, extensionContext); + } + + /** + * In-Memory variant of {@link IdentityHubEndToEndExtension} + */ + public static class InMemory extends IdentityHubEndToEndExtension { + + protected InMemory() { + super(context()); + } + + public static IdentityHubEndToEndTestContext context() { + var configuration = IdentityHubRuntimeConfiguration.Builder.newInstance() + .name("identity-hub") + .id("identity-hub") + .build(); + + var runtime = new EmbeddedRuntime( + "identity-hub", + configuration.config(), + ":launcher" + ); + + return new IdentityHubEndToEndTestContext(runtime, configuration); + } + + } + + /** + * PG variant of {@link IdentityHubEndToEndExtension} + */ + public static class Postgres extends IdentityHubEndToEndExtension { + + private static final String DB_NAME = "runtime"; + private static final Integer DB_PORT = getFreePort(); + private final PostgresSqlService postgresSqlService; + + protected Postgres() { + super(context(DB_NAME, DB_PORT)); + postgresSqlService = new PostgresSqlService(DB_NAME, DB_PORT); + + } + + public static IdentityHubEndToEndTestContext context(String dbName, Integer port) { + + var configuration = IdentityHubRuntimeConfiguration.Builder.newInstance() + .name("identity-hub") + .id("identity-hub") + .build(); + + var cfg = new HashMap<>(configuration.config()); + cfg.putAll(postgresqlConfiguration(dbName, port)); + + var runtime = new EmbeddedRuntime( + "control-plane", + cfg, + ":launcher", + ":extensions:store:sql:identity-hub-credentials-store-sql", + ":extensions:store:sql:identity-hub-did-store-sql", + ":extensions:store:sql:identity-hub-keypair-store-sql", + ":extensions:store:sql:identity-hub-participantcontext-store-sql" + + ); + + return new IdentityHubEndToEndTestContext(runtime, configuration); + } + + private static Map postgresqlConfiguration(String dbName, Integer port) { + var jdbcUrl = jdbcUrl(dbName, port); + return new HashMap<>() { + { + put("edc.datasource.default.url", jdbcUrl); + put("edc.datasource.default.user", PostgresqlEndToEndInstance.USER); + put("edc.datasource.default.password", PostgresqlEndToEndInstance.PASSWORD); + } + }; + } + + @Override + public void beforeAll(ExtensionContext extensionContext) { + postgresSqlService.start(); + super.beforeAll(extensionContext); + } + + @Override + public void afterAll(ExtensionContext extensionContext) { + super.afterAll(extensionContext); + postgresSqlService.stop(); + } + } +} diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/IdentityApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java similarity index 57% rename from e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/IdentityApiEndToEndTest.java rename to e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java index b11d45299..d0ae6f8db 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/IdentityApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java @@ -12,13 +12,15 @@ * */ -package org.eclipse.edc.identityhub.tests; +package org.eclipse.edc.identityhub.tests.fixtures; +import com.nimbusds.jose.jwk.Curve; import org.eclipse.edc.iam.did.spi.document.DidDocument; import org.eclipse.edc.iam.did.spi.document.Service; import org.eclipse.edc.identithub.spi.did.DidDocumentService; import org.eclipse.edc.identityhub.participantcontext.ApiTokenGenerator; import org.eclipse.edc.identityhub.spi.authentication.ServicePrincipal; +import org.eclipse.edc.identityhub.spi.keypair.KeyPairService; import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; @@ -27,38 +29,42 @@ import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource; import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; -import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubRuntimeConfiguration; import org.eclipse.edc.junit.extensions.EmbeddedRuntime; -import org.eclipse.edc.junit.extensions.RuntimeExtension; -import org.eclipse.edc.junit.extensions.RuntimePerClassExtension; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.security.Vault; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.extension.RegisterExtension; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.UUID; /** - * Base class for all Identity API tests + * Identity Hub end to end context used in tests extended with {@link IdentityHubEndToEndExtension} */ -public abstract class IdentityApiEndToEndTest { +public class IdentityHubEndToEndTestContext { + public static final String SUPER_USER = "super-user"; - protected static final IdentityHubRuntimeConfiguration RUNTIME_CONFIGURATION = IdentityHubRuntimeConfiguration.Builder.newInstance() - .name("identity-hub") - .id("identity-hub") - .build(); - @RegisterExtension - protected static final RuntimeExtension RUNTIME = new RuntimePerClassExtension(new EmbeddedRuntime("identity-hub", RUNTIME_CONFIGURATION.controlPlaneConfiguration(), ":launcher")); - - protected static String createSuperUser() { - return createParticipant(SUPER_USER, List.of(ServicePrincipal.ROLE_ADMIN)); + + private final EmbeddedRuntime runtime; + private final IdentityHubRuntimeConfiguration configuration; + + public IdentityHubEndToEndTestContext(EmbeddedRuntime runtime, IdentityHubRuntimeConfiguration configuration) { + this.runtime = runtime; + this.configuration = configuration; + } + + public EmbeddedRuntime getRuntime() { + return runtime; + } + + public String createParticipant(String participantId) { + return createParticipant(participantId, List.of()); } - private static String createParticipant(String participantId, List roles) { + + public String createParticipant(String participantId, List roles) { var manifest = ParticipantManifest.Builder.newInstance() .participantId(participantId) .active(true) @@ -72,63 +78,87 @@ private static String createParticipant(String participantId, List roles .keyGeneratorParams(Map.of("algorithm", "EC", "curve", "secp256r1")) .build()) .build(); - var srv = RUNTIME.getService(ParticipantContextService.class); + var srv = runtime.getService(ParticipantContextService.class); return srv.createParticipantContext(manifest).orElseThrow(f -> new EdcException(f.getFailureDetail())); } - @NotNull - public KeyDescriptor.Builder createKeyDescriptor() { - return KeyDescriptor.Builder.newInstance() - .privateKeyAlias("another-alias") - .keyGeneratorParams(Map.of("algorithm", "EdDSA", "curve", "Ed25519")) - .keyId("another-keyid"); - } - - protected ParticipantManifest.Builder createNewParticipant() { - return ParticipantManifest.Builder.newInstance() - .participantId("another-participant") - .active(false) - .did("did:web:another:participant") - .serviceEndpoint(new Service("test-service", "test-service-type", "https://test.com")) - .key(createKeyDescriptor().build()); + public String createSuperUser() { + return createParticipant(SUPER_USER, List.of(ServicePrincipal.ROLE_ADMIN)); } - protected String storeParticipant(ParticipantContext pc) { - var store = RUNTIME.getService(ParticipantContextStore.class); + public String storeParticipant(ParticipantContext pc) { + var store = runtime.getService(ParticipantContextStore.class); - var vault = RUNTIME.getService(Vault.class); + var vault = runtime.getService(Vault.class); var token = createTokenFor(pc.getParticipantId()); vault.storeSecret(pc.getApiTokenAlias(), token); store.create(pc).orElseThrow(f -> new RuntimeException(f.getFailureDetail())); return token; } - protected String createParticipant(String participantId) { - return createParticipant(participantId, List.of()); + public IdentityHubRuntimeConfiguration.Endpoint getIdentityApiEndpoint() { + return configuration.getIdentityApiEndpoint(); } - protected String createTokenFor(String userId) { - return new ApiTokenGenerator().generate(userId); + public IdentityHubRuntimeConfiguration.Endpoint getResolutionEndpoint() { + return configuration.getResolutionEndpoint(); } - protected T getService(Class type) { - return RUNTIME.getService(type); + + public Collection getDidForParticipant(String participantId) { + return runtime.getService(DidDocumentService.class).queryDocuments(QuerySpec.Builder.newInstance() + .filter(new Criterion("participantId", "=", participantId)) + .build()).getContent(); } - protected Collection getKeyPairsForParticipant(String participantId) { - return getService(KeyPairResourceStore.class).query(ParticipantResource.queryByParticipantId(participantId).build()) + public Collection getKeyPairsForParticipant(String participantId) { + return runtime.getService(KeyPairResourceStore.class).query(ParticipantResource.queryByParticipantId(participantId).build()) .getContent(); } - protected Collection getDidForParticipant(String participantId) { - return getService(DidDocumentService.class).queryDocuments(QuerySpec.Builder.newInstance() - .filter(new Criterion("participantId", "=", participantId)) - .build()).getContent(); + public String createKeyPair(String participantId) { + + var descriptor = createKeyDescriptor(participantId).build(); + + var service = runtime.getService(KeyPairService.class); + service.addKeyPair(participantId, descriptor, true) + .orElseThrow(f -> new EdcException(f.getFailureDetail())); + return descriptor.getResourceId(); + } + + public KeyDescriptor.Builder createKeyDescriptor(String participantId) { + var keyId = UUID.randomUUID().toString(); + return KeyDescriptor.Builder.newInstance() + .keyId(keyId) + .resourceId(UUID.randomUUID().toString()) + .keyGeneratorParams(Map.of("algorithm", "EC", "curve", Curve.P_384.getStdName())) + .privateKeyAlias("%s-%s-alias".formatted(participantId, keyId)); + } + + public ParticipantManifest.Builder createNewParticipant() { + return ParticipantManifest.Builder.newInstance() + .participantId("another-participant") + .active(false) + .did("did:web:another:participant") + .serviceEndpoint(new Service("test-service", "test-service-type", "https://test.com")) + .key(createKeyDescriptor().build()); + } + + public KeyDescriptor.Builder createKeyDescriptor() { + return KeyDescriptor.Builder.newInstance() + .privateKeyAlias("another-alias") + .keyGeneratorParams(Map.of("algorithm", "EdDSA", "curve", "Ed25519")) + .keyId("another-keyid"); } - protected ParticipantContext getParticipant(String participantId) { - return getService(ParticipantContextService.class) + public String createTokenFor(String userId) { + return new ApiTokenGenerator().generate(userId); + } + + public ParticipantContext getParticipant(String participantId) { + return runtime.getService(ParticipantContextService.class) .getParticipantContext(participantId) .orElseThrow(f -> new EdcException(f.getFailureDetail())); } + } diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubRuntimeConfiguration.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubRuntimeConfiguration.java index b99b04893..289e4e358 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubRuntimeConfiguration.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubRuntimeConfiguration.java @@ -38,7 +38,7 @@ public Endpoint getResolutionEndpoint() { return resolutionEndpoint; } - public Map controlPlaneConfiguration() { + public Map config() { return new HashMap<>() { { put(PARTICIPANT_ID, id); diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/PostgresSqlService.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/PostgresSqlService.java new file mode 100644 index 000000000..900408605 --- /dev/null +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/PostgresSqlService.java @@ -0,0 +1,116 @@ +/* + * 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.tests.fixtures; + +import com.github.dockerjava.api.model.ExposedPort; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.PortBinding; +import com.github.dockerjava.api.model.Ports; +import org.eclipse.edc.junit.testfixtures.TestUtils; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.sql.testfixtures.PostgresqlEndToEndInstance; +import org.eclipse.edc.sql.testfixtures.PostgresqlLocalInstance; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.IOException; +import java.nio.file.Files; +import java.sql.SQLException; +import java.util.stream.Stream; + +import static java.lang.String.format; + +/** + * Wrapper for {@link PostgreSQLContainer} + */ +public class PostgresSqlService { + + private static final String POSTGRES_IMAGE_NAME = "postgres:16.2"; + + private final PostgreSQLContainer postgreSqlContainer; + private final String dbName; + private final Integer hostPort; + + public PostgresSqlService(Integer port) { + this("runtime", port); + } + + public PostgresSqlService(String dbName, Integer port) { + this.hostPort = port; + this.dbName = dbName; + var portBinding = new PortBinding(Ports.Binding.bindPort(hostPort), new ExposedPort(5432)); + postgreSqlContainer = new PostgreSQLContainer<>(POSTGRES_IMAGE_NAME) + .withLabel("runtime", dbName) + .withExposedPorts(5432) + .withUsername(PostgresqlEndToEndInstance.USER) + .withPassword(PostgresqlEndToEndInstance.PASSWORD) + .withDatabaseName(dbName) + .withCreateContainerCmdModifier(e -> e.withHostConfig(new HostConfig().withPortBindings(portBinding))); + } + + public static String baseJdbcUrl(Integer hostPort) { + return format("jdbc:postgresql://%s:%s/", "localhost", hostPort); + } + + public static String jdbcUrl(String name, Integer hostPort) { + return baseJdbcUrl(hostPort) + name; + } + + public void start() { + postgreSqlContainer.start(); + postgreSqlContainer.waitingFor(Wait.forHealthcheck()); + createDatabase(); + } + + public void stop() { + postgreSqlContainer.stop(); + postgreSqlContainer.close(); + } + + public String jdbcUrl() { + return jdbcUrl(dbName, hostPort); + } + + private void createDatabase() { + var postgres = new PostgresqlLocalInstance(PostgresqlEndToEndInstance.USER, PostgresqlEndToEndInstance.PASSWORD, baseJdbcUrl(hostPort), dbName); + postgres.createDatabase(); + + var extensionsFolder = TestUtils.findBuildRoot().toPath().resolve("extensions"); + var scripts = Stream.of( + "store/sql/identity-hub-credentials-store-sql", + "store/sql/identity-hub-did-store-sql", + "store/sql/identity-hub-keypair-store-sql", + "store/sql/identity-hub-participantcontext-store-sql" + ) + .map(extensionsFolder::resolve) + .map(it -> it.resolve("docs")) + .map(it -> it.resolve("schema.sql")) + .toList(); + + try (var connection = postgres.getConnection(dbName)) { + for (var script : scripts) { + var sql = Files.readString(script); + + try (var statement = connection.createStatement()) { + statement.execute(sql); + } catch (Exception exception) { + throw new EdcPersistenceException(exception.getMessage(), exception); + } + } + } catch (SQLException | IOException e) { + throw new EdcPersistenceException(e); + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 048a668c7..ad40a944f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ postgres = "42.7.1" restAssured = "5.5.0" swagger = "2.2.21" rsApi = "4.0.0" -testcontainers = "1.19.3" +testcontainers = "1.19.8" tink = "1.14.0" [libraries] @@ -53,7 +53,9 @@ edc-vc-ldp = { module = "org.eclipse.edc:ldp-verifiable-credentials", version.re edc-ext-http = { module = "org.eclipse.edc:http", version.ref = "edc" } edc-ext-jsonld = { module = "org.eclipse.edc:json-ld", version.ref = "edc" } edc-ext-observability = { module = "org.eclipse.edc:api-observability", version.ref = "edc" } +edc-ext-transaction-local = { module = "org.eclipse.edc:transaction-local", version.ref = "edc" } edc-testfixtures-managementapi = { module = "org.eclipse.edc:management-api-test-fixtures", version.ref = "edc" } +edc-sql-pool = { module = "org.eclipse.edc:sql-pool-apache-commons", version.ref = "edc" } # EDC libs edc-lib-keys = { "module" = "org.eclipse.edc:keys-lib", version.ref = "edc" } @@ -82,7 +84,7 @@ 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" } +testcontainers-postgres = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } jakarta-annotation = { module = "jakarta.annotation:jakarta.annotation-api", version.ref = "jakarta-annotation" } jersey-common = { module = "org.glassfish.jersey.core:jersey-common", version.ref = "jersey" } tink = { module = "com.google.crypto.tink:tink", version.ref = "tink" }