diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b14905dbbf..4ec94cfb7a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,7 +36,7 @@ jobs: - name: MegaLinter id: ml - uses: oxsecurity/megalinter@v7.1.0 + uses: oxsecurity/megalinter@v8 - name: Archive production artifacts if: success() || failure() diff --git a/.mega-linter.yml b/.mega-linter.yml index 2e4d3c64b2..d065f68b12 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -12,6 +12,7 @@ DISABLE_LINTERS: - REPOSITORY_CHECKOV - REPOSITORY_SECRETLINT - REPOSITORY_KICS + - REPOSITORY_GRYPE - SCALA_SCALAFIX - SQL_TSQLLINT - C_CPPLINT # For pollux/lib/anoncreds/src/main/c @@ -30,6 +31,7 @@ DISABLE_LINTERS: DISABLE_ERRORS_LINTERS: - KOTLIN_KTLINT + - KOTLIN_DETEKT - PROTOBUF_PROTOLINT - MARKDOWN_MARKDOWN_LINK_CHECK - ACTION_ACTIONLINT diff --git a/.sbtopts b/.sbtopts index 2872000dd8..398fe87e6a 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1,3 @@ -Dquill.macro.log=false +-J-Xmx4G +-J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index e96b09c440..0df784ff60 100644 --- a/build.sbt +++ b/build.sbt @@ -36,7 +36,8 @@ inThisBuild( // scalacOptions += "-Yexplicit-nulls", // scalacOptions += "-Ysafe-init", // scalacOptions += "-Werror", // <=> "-Xfatal-warnings" - scalacOptions += "-Dquill.macro.log=false", // disable quill macro logs // TODO https://github.com/zio/zio-protoquill/issues/470 + scalacOptions += "-Dquill.macro.log=false", // disable quill macro logs // TODO https://github.com/zio/zio-protoquill/issues/470, + scalacOptions ++= Seq("-Xmax-inlines", "50") // manually increase max-inlines above 32 (https://github.com/circe/circe/issues/2162) ) ) @@ -49,10 +50,11 @@ lazy val V = new { val zioConfig = "4.0.2" val zioLogging = "2.3.1" val zioJson = "0.7.3" - val zioHttp = "3.0.0" + val zioHttp = "3.0.1" val zioCatsInterop = "3.3.0" // TODO "23.1.0.2" // https://mvnrepository.com/artifact/dev.zio/zio-interop-cats val zioMetricsConnector = "2.3.1" val zioMock = "1.0.0-RC12" + val zioKafka = "2.7.5" val mockito = "3.2.18.0" val monocle = "3.2.0" @@ -102,7 +104,11 @@ lazy val D = new { val zioLog: ModuleID = "dev.zio" %% "zio-logging" % V.zioLogging val zioSLF4J: ModuleID = "dev.zio" %% "zio-logging-slf4j" % V.zioLogging val zioJson: ModuleID = "dev.zio" %% "zio-json" % V.zioJson + val zioConcurrent: ModuleID = "dev.zio" %% "zio-concurrent" % V.zio val zioHttp: ModuleID = "dev.zio" %% "zio-http" % V.zioHttp + val zioKafka: ModuleID = "dev.zio" %% "zio-kafka" % V.zioKafka excludeAll ( + ExclusionRule("dev.zio", "zio_3"), ExclusionRule("dev.zio", "zio-streams_3") + ) val zioCatsInterop: ModuleID = "dev.zio" %% "zio-interop-cats" % V.zioCatsInterop val zioMetricsConnectorMicrometer: ModuleID = "dev.zio" %% "zio-metrics-connectors-micrometer" % V.zioMetricsConnector val tapirPrometheusMetrics: ModuleID = "com.softwaremill.sttp.tapir" %% "tapir-prometheus-metrics" % V.tapir @@ -185,7 +191,9 @@ lazy val D_Shared = new { D.typesafeConfig, D.scalaPbGrpc, D.zio, + D.zioConcurrent, D.zioHttp, + D.zioKafka, D.scalaUri, D.zioPrelude, // FIXME: split shared DB stuff as subproject? @@ -341,12 +349,11 @@ lazy val D_Pollux_VC_JWT = new { lazy val D_EventNotification = new { val zio = "dev.zio" %% "zio" % V.zio - val zioConcurrent = "dev.zio" %% "zio-concurrent" % V.zio val zioTest = "dev.zio" %% "zio-test" % V.zio % Test val zioTestSbt = "dev.zio" %% "zio-test-sbt" % V.zio % Test val zioTestMagnolia = "dev.zio" %% "zio-test-magnolia" % V.zio % Test - val zioDependencies: Seq[ModuleID] = Seq(zio, zioConcurrent, zioTest, zioTestSbt, zioTestMagnolia) + val zioDependencies: Seq[ModuleID] = Seq(zio, zioTest, zioTestSbt, zioTestMagnolia) val baseDependencies: Seq[ModuleID] = zioDependencies } diff --git a/cloud-agent/client/generator/openapitools.json b/cloud-agent/client/generator/openapitools.json index 5571688218..f227cf2df3 100644 --- a/cloud-agent/client/generator/openapitools.json +++ b/cloud-agent/client/generator/openapitools.json @@ -2,6 +2,6 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "7.4.0" + "version": "7.7.0" } } diff --git a/cloud-agent/client/generator/package.json b/cloud-agent/client/generator/package.json index 79c2bf504e..f9fb3d43dc 100644 --- a/cloud-agent/client/generator/package.json +++ b/cloud-agent/client/generator/package.json @@ -13,7 +13,7 @@ "publish:clients": "./publish-clients.sh" }, "dependencies": { - "@openapitools/openapi-generator-cli": "2.7.0", + "@openapitools/openapi-generator-cli": "2.13.13", "npm-run-all": "^4.1.5" } } diff --git a/cloud-agent/client/generator/yarn.lock b/cloud-agent/client/generator/yarn.lock index faf654ad56..b9236b8e57 100644 --- a/cloud-agent/client/generator/yarn.lock +++ b/cloud-agent/client/generator/yarn.lock @@ -14,33 +14,31 @@ resolved "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz" integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== -"@nestjs/axios@0.1.0": - version "0.1.0" - resolved "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.0.tgz" - integrity sha512-b2TT2X6BFbnNoeteiaxCIiHaFcSbVW+S5yygYqiIq5i6H77yIU3IVuLdpQkHq8/EqOWFwMopLN8jdkUT71Am9w== - dependencies: - axios "0.27.2" +"@nestjs/axios@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-3.0.3.tgz#a663cb13cff07ea6b9a7107263de2ae472d41118" + integrity sha512-h6TCn3yJwD6OKqqqfmtRS5Zo4E46Ip2n+gK1sqwzNBC+qxQ9xpCu+ODVRFur6V3alHSCSBxb3nNtt73VEdluyA== -"@nestjs/common@9.3.11": - version "9.3.11" - resolved "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz" - integrity sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A== +"@nestjs/common@10.4.3": + version "10.4.3" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.3.tgz#b9059313d928aea335a4a185a621e32c1858c845" + integrity sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg== dependencies: - uid "2.0.1" + uid "2.0.2" iterare "1.2.1" - tslib "2.5.0" + tslib "2.7.0" -"@nestjs/core@9.3.11": - version "9.3.11" - resolved "https://registry.npmjs.org/@nestjs/core/-/core-9.3.11.tgz" - integrity sha512-CI27a2JFd5rvvbgkalWqsiwQNhcP4EAG5BUK8usjp29wVp1kx30ghfBT8FLqIgmkRVo65A0IcEnWsxeXMntkxQ== +"@nestjs/core@10.4.3": + version "10.4.3" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.3.tgz#b2a3dcfc6a948a74618feeee8affc3186afe52da" + integrity sha512-6OQz+5C8mT8yRtfvE5pPCq+p6w5jDot+oQku1KzQ24ABn+lay1KGuJwcKZhdVNuselx+8xhdMxknZTA8wrGLIg== dependencies: - uid "2.0.1" + uid "2.0.2" "@nuxtjs/opencollective" "0.3.2" fast-safe-stringify "2.1.1" iterare "1.2.1" - path-to-regexp "3.2.0" - tslib "2.5.0" + path-to-regexp "3.3.0" + tslib "2.7.0" "@nuxtjs/opencollective@0.3.2": version "0.3.2" @@ -51,27 +49,36 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@openapitools/openapi-generator-cli@2.7.0": - version "2.7.0" - resolved "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.7.0.tgz" - integrity sha512-ieEpHTA/KsDz7ANw03lLPYyjdedDEXYEyYoGBRWdduqXWSX65CJtttjqa8ZaB1mNmIjMtchUHwAYQmTLVQ8HYg== +"@openapitools/openapi-generator-cli@2.13.13": + version "2.13.13" + resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.13.13.tgz#380fd9556500b558f066a9ee0c46678f7803422b" + integrity sha512-uioqbxB6TfiLoOEE3T8kqTn/ffaRzOwS3ATMQnoMvh2lwADKMT6bDLfE3YO3XTEj+HflXcsLXQGK6PLiqa8Mmw== dependencies: - "@nestjs/axios" "0.1.0" - "@nestjs/common" "9.3.11" - "@nestjs/core" "9.3.11" + "@nestjs/axios" "3.0.3" + "@nestjs/common" "10.4.3" + "@nestjs/core" "10.4.3" "@nuxtjs/opencollective" "0.3.2" + axios "1.7.7" chalk "4.1.2" commander "8.3.0" compare-versions "4.1.4" concurrently "6.5.1" console.table "0.10.0" fs-extra "10.1.0" - glob "7.1.6" - inquirer "8.2.5" + glob "9.3.5" + https-proxy-agent "7.0.5" + inquirer "8.2.6" lodash "4.17.21" reflect-metadata "0.1.13" - rxjs "7.8.0" - tslib "2.0.3" + rxjs "7.8.1" + tslib "2.7.0" + +agent-base@^7.0.2: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" ansi-escapes@^4.2.1: version "4.3.2" @@ -129,13 +136,14 @@ available-typed-arrays@^1.0.5: resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -axios@0.27.2: - version "0.27.2" - resolved "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== +axios@1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== dependencies: - follow-redirects "^1.14.9" + follow-redirects "^1.15.6" form-data "^4.0.0" + proxy-from-env "^1.1.0" balanced-match@^1.0.0: version "1.0.2" @@ -164,6 +172,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + buffer@^5.5.0: version "5.7.1" resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" @@ -323,6 +338,13 @@ date-fns@^2.16.1: dependencies: "@babel/runtime" "^7.21.0" +debug@4, debug@^4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + defaults@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz" @@ -456,10 +478,10 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" -follow-redirects@^1.14.9: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== for-each@^0.3.3: version "0.3.3" @@ -534,17 +556,15 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" -glob@7.1.6: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@9.3.5: + version "9.3.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" + integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== dependencies: fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" + minimatch "^8.0.2" + minipass "^4.2.4" + path-scurry "^1.6.1" globalthis@^1.0.3: version "1.0.3" @@ -616,6 +636,14 @@ hosted-git-info@^2.1.4: resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +https-proxy-agent@7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" @@ -628,23 +656,15 @@ ieee754@^1.1.13: resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3, inherits@^2.0.4: +inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inquirer@8.2.5: - version "8.2.5" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz" - integrity sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ== +inquirer@8.2.6: + version "8.2.6" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" + integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg== dependencies: ansi-escapes "^4.2.1" chalk "^4.1.1" @@ -660,7 +680,7 @@ inquirer@8.2.5: string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" - wrap-ansi "^7.0.0" + wrap-ansi "^6.0.1" internal-slot@^1.0.5: version "1.0.5" @@ -841,6 +861,11 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz" @@ -870,6 +895,28 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^8.0.2: + version "8.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" + integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== + dependencies: + brace-expansion "^2.0.1" + +minipass@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + mute-stream@0.0.8: version "0.0.8" resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" @@ -932,13 +979,6 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -once@^1.3.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - onetime@^5.1.0: version "5.1.2" resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" @@ -974,11 +1014,6 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - path-key@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz" @@ -989,10 +1024,18 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz" - integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== +path-scurry@^1.6.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== path-type@^3.0.0: version "3.0.0" @@ -1011,6 +1054,11 @@ pify@^3.0.0: resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz" @@ -1075,10 +1123,10 @@ run-async@^2.4.0: resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== -rxjs@7.8.0: - version "7.8.0" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz" - integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== +rxjs@7.8.1, rxjs@^7.5.5: + version "7.8.1" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" @@ -1089,13 +1137,6 @@ rxjs@^6.6.3: dependencies: tslib "^1.9.0" -rxjs@^7.5.5: - version "7.8.1" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - safe-array-concat@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz" @@ -1304,15 +1345,10 @@ tree-kill@^1.2.2: resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -tslib@2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz" - integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== - -tslib@2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== +tslib@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== tslib@^1.9.0: version "1.14.1" @@ -1368,10 +1404,10 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -uid@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/uid/-/uid-2.0.1.tgz" - integrity sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A== +uid@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/uid/-/uid-2.0.2.tgz#4b5782abf0f2feeefc00fa88006b2b3b7af3e3b9" + integrity sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g== dependencies: "@lukeed/csprng" "^1.0.0" @@ -1452,6 +1488,15 @@ which@^1.2.9: dependencies: isexe "^2.0.0" +wrap-ansi@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" @@ -1461,11 +1506,6 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" diff --git a/cloud-agent/client/kotlin/.openapi-generator-ignore b/cloud-agent/client/kotlin/.openapi-generator-ignore index 2f78a69926..658834d27c 100644 --- a/cloud-agent/client/kotlin/.openapi-generator-ignore +++ b/cloud-agent/client/kotlin/.openapi-generator-ignore @@ -2,7 +2,8 @@ settings.gradle build.gradle docs -# igore broken files +# ignore broken files + src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceAction.kt src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceActionType.kt @@ -15,3 +16,19 @@ src/main/kotlin/org/hyperledger/identus/client/models/CredentialSubject.kt src/main/kotlin/org/hyperledger/identus/client/models/DateTimeParameter.kt src/main/kotlin/org/hyperledger/identus/client/models/DidParameter.kt src/main/kotlin/org/hyperledger/identus/client/models/VcVerificationParameter.kt + +src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequest.kt +src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequestSchemaId.kt + +src/test/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceActionTest.kt +src/test/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceActionTypeTest.kt + +src/test/kotlin/org/hyperledger/identus/client/models/ServiceTest.kt +src/test/kotlin/org/hyperledger/identus/client/models/ServiceTypeTest.kt + +src/test/kotlin/org/hyperledger/identus/client/models/StatusPurposeTest.kt +src/test/kotlin/org/hyperledger/identus/client/models/CredentialSubjectTest.kt + +src/test/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequestTest.kt +src/test/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequestSchemaIdTest.kt + diff --git a/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.jar b/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.jar index c1962a79e2..d64cd49177 100644 Binary files a/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.jar and b/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.properties b/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.properties index 8707e8b506..e7646dead0 100644 --- a/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.properties +++ b/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/cloud-agent/client/kotlin/gradlew b/cloud-agent/client/kotlin/gradlew index aeb74cbb43..9d0ce634cb 100755 --- a/cloud-agent/client/kotlin/gradlew +++ b/cloud-agent/client/kotlin/gradlew @@ -69,34 +69,35 @@ app_path=$0 # Need this for daisy-chained symlinks. while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] +APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path +[ -h "$app_path" ] do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac +ls=$( ls -ld "$app_path" ) +link=${ls#*' -> '} +case $link in #( +/*) app_path=$link ;; #( +*) app_path=$APP_HOME$link ;; +esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { - echo "$*" +echo "$*" } >&2 die () { - echo - echo "$*" - echo - exit 1 +echo +echo "$*" +echo +exit 1 } >&2 # OS specific support (must be 'true' or 'false'). @@ -105,10 +106,10 @@ msys=false darwin=false nonstop=false case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; +CYGWIN* ) cygwin=true ;; #( +Darwin* ) darwin=true ;; #( +MSYS* | MINGW* ) msys=true ;; #( +NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -116,43 +117,46 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME +if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# IBM's JDK on AIX uses strange locations for the executables +JAVACMD=$JAVA_HOME/jre/sh/java +else +JAVACMD=$JAVA_HOME/bin/java +fi +if [ ! -x "$JAVACMD" ] ; then +die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi +fi else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +JAVACMD=java +if ! command -v java >/dev/null 2>&1 +then +die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi +fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac +case $MAX_FD in #( +max*) +# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +MAX_FD=$( ulimit -H -n ) || +warn "Could not query maximum file descriptor limit" +esac +case $MAX_FD in #( +'' | soft) :;; #( +*) +# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +ulimit -n "$MAX_FD" || +warn "Could not set maximum file descriptor limit to $MAX_FD" +esac fi # Collect all arguments for the java command, stacking in reverse order: @@ -165,55 +169,55 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done +APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) +CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + +JAVACMD=$( cygpath --unix "$JAVACMD" ) + +# Now convert the arguments - kludge to limit ourselves to /bin/sh +for arg do +if +case $arg in #( +-*) false ;; # don't mess with options #( +/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath +[ -e "$t" ] ;; #( +*) false ;; +esac +then +arg=$( cygpath --path --ignore --mixed "$arg" ) +fi +# Roll the args list around exactly as many times as the number of +# args, so each arg winds up back in the position where it started, but +# possibly modified. +# +# NB: a `for` loop captures its iteration list before it begins, so +# changing the positional parameters here affects neither the number of +# iterations, nor the values presented in `arg`. +shift # remove old arg +set -- "$@" "$arg" # push replacement arg +done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" +"-Dorg.gradle.appname=$APP_BASE_NAME" \ +-classpath "$CLASSPATH" \ +org.gradle.wrapper.GradleWrapperMain \ +"$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then - die "xargs is not available" +die "xargs is not available" fi # Use "xargs" to parse quoted args. @@ -236,10 +240,10 @@ fi # eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' +printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | +xargs -n1 | +sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | +tr '\n' ' ' +)" '"$@"' exec "$JAVACMD" "$@" diff --git a/cloud-agent/client/kotlin/gradlew.bat b/cloud-agent/client/kotlin/gradlew.bat index 93e3f59f13..9d0ce634cb 100644 --- a/cloud-agent/client/kotlin/gradlew.bat +++ b/cloud-agent/client/kotlin/gradlew.bat @@ -1,92 +1,249 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while +APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path +[ -h "$app_path" ] +do +ls=$( ls -ld "$app_path" ) +link=${ls#*' -> '} +case $link in #( +/*) app_path=$link ;; #( +*) app_path=$APP_HOME$link ;; +esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { +echo "$*" +} >&2 + +die () { +echo +echo "$*" +echo +exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( +CYGWIN* ) cygwin=true ;; #( +Darwin* ) darwin=true ;; #( +MSYS* | MINGW* ) msys=true ;; #( +NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then +if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# IBM's JDK on AIX uses strange locations for the executables +JAVACMD=$JAVA_HOME/jre/sh/java +else +JAVACMD=$JAVA_HOME/bin/java +fi +if [ ! -x "$JAVACMD" ] ; then +die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +else +JAVACMD=java +if ! command -v java >/dev/null 2>&1 +then +die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then +case $MAX_FD in #( +max*) +# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +MAX_FD=$( ulimit -H -n ) || +warn "Could not query maximum file descriptor limit" +esac +case $MAX_FD in #( +'' | soft) :;; #( +*) +# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +ulimit -n "$MAX_FD" || +warn "Could not set maximum file descriptor limit to $MAX_FD" +esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then +APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) +CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + +JAVACMD=$( cygpath --unix "$JAVACMD" ) + +# Now convert the arguments - kludge to limit ourselves to /bin/sh +for arg do +if +case $arg in #( +-*) false ;; # don't mess with options #( +/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath +[ -e "$t" ] ;; #( +*) false ;; +esac +then +arg=$( cygpath --path --ignore --mixed "$arg" ) +fi +# Roll the args list around exactly as many times as the number of +# args, so each arg winds up back in the position where it started, but +# possibly modified. +# +# NB: a `for` loop captures its iteration list before it begins, so +# changing the positional parameters here affects neither the number of +# iterations, nor the values presented in `arg`. +shift # remove old arg +set -- "$@" "$arg" # push replacement arg +done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ +"-Dorg.gradle.appname=$APP_BASE_NAME" \ +-classpath "$CLASSPATH" \ +org.gradle.wrapper.GradleWrapperMain \ +"$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then +die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( +printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | +xargs -n1 | +sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | +tr '\n' ' ' +)" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/cloud-agent/client/kotlin/settings.gradle b/cloud-agent/client/kotlin/settings.gradle index b5dc286913..4765fb4704 100644 --- a/cloud-agent/client/kotlin/settings.gradle +++ b/cloud-agent/client/kotlin/settings.gradle @@ -1,2 +1 @@ - -rootProject.name = 'cloud-agent-client-kotlin' \ No newline at end of file +rootProject.name = 'cloud-agent-client-kotlin' diff --git a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/adapters/StringOrStringArrayAdapter.kt b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/adapters/StringOrStringArrayAdapter.kt new file mode 100644 index 0000000000..cebdbbe604 --- /dev/null +++ b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/adapters/StringOrStringArrayAdapter.kt @@ -0,0 +1,33 @@ +package org.hyperledger.identus.client.adapters + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonSerializer +import com.google.gson.JsonNull +import com.google.gson.JsonParseException +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import java.lang.reflect.Type + +class StringOrStringArrayAdapter : JsonSerializer>, JsonDeserializer> { + + // Deserialize logic: String or Array of Strings to List + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): List { + return when { + json.isJsonArray -> context.deserialize(json, typeOfT) + json.isJsonPrimitive -> listOf(json.asString) + json.isJsonNull -> emptyList() + else -> throw JsonParseException("Unexpected type for field") + } + } + + // Serialize logic: List to String or Array of Strings + override fun serialize(src: List?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { + return when { + src.isNullOrEmpty() -> JsonNull.INSTANCE + src.size == 1 -> JsonPrimitive(src[0]) // If only one string, serialize as a single string + else -> context!!.serialize(src) // Otherwise, serialize as a list + } + } +} \ No newline at end of file diff --git a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequest.kt b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequest.kt new file mode 100644 index 0000000000..7518b6a52e --- /dev/null +++ b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequest.kt @@ -0,0 +1,86 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package org.hyperledger.identus.client.models + +import com.google.gson.annotations.JsonAdapter +import com.google.gson.annotations.SerializedName +import org.hyperledger.identus.client.adapters.StringOrStringArrayAdapter + +/** + * + * + * @param claims The set of claims that will be included in the issued credential. The JSON object should comply with the schema applicable for this offer (i.e. 'schemaId' or 'credentialDefinitionId'). + * @param issuingDID The issuer Prism DID by which the verifiable credential will be issued. DID can be short for or long form. + * @param validityPeriod The validity period in seconds of the verifiable credential that will be issued. + * @param schemaId + * @param credentialDefinitionId The unique identifier (UUID) of the credential definition that will be used for this offer. It should be the identifier of a credential definition that exists in the issuer agent's database. Note that this parameter only applies when the offer is of type 'AnonCreds'. + * @param credentialFormat The credential format for this offer (defaults to 'JWT') + * @param automaticIssuance Specifies whether or not the credential should be automatically generated and issued when receiving the `CredentialRequest` from the holder. If set to `false`, a manual approval by the issuer via another API call will be required for the VC to be issued. + * @param issuingKid Specified the key ID (kid) of the DID, it will be used to sign credential. User should specify just the partial identifier of the key. The full id of the kid MUST be \"#\" Note the cryto algorithm used with depend type of the key. + * @param connectionId The unique identifier of a DIDComm connection that already exists between the this issuer agent and the holder cloud or edeg agent. It should be the identifier of a connection that exists in the issuer agent's database. This connection will be used to execute the issue credential protocol. Note: connectionId is only required when the offer is from existing connection. connectionId is not required when the offer is from invitation for connectionless issuance. + * @param goalCode A self-attested code the receiver may want to display to the user or use in automatically deciding what to do with the out-of-band message. goalcode is optional and can be provided when the offer is from invitation for connectionless issuance. + * @param goal A self-attested string that the receiver may want to display to the user about the context-specific goal of the out-of-band message. goal is optional and can be provided when the offer is from invitation for connectionless issuance. + */ + + +data class CreateIssueCredentialRecordRequest( + + /* The set of claims that will be included in the issued credential. The JSON object should comply with the schema applicable for this offer (i.e. 'schemaId' or 'credentialDefinitionId'). */ + @SerializedName("claims") + val claims: kotlin.Any?, + + /* The issuer Prism DID by which the verifiable credential will be issued. DID can be short for or long form. */ + @SerializedName("issuingDID") + val issuingDID: kotlin.String, + + /* The validity period in seconds of the verifiable credential that will be issued. */ + @SerializedName("validityPeriod") + val validityPeriod: kotlin.Double? = null, + + @SerializedName("schemaId") + @JsonAdapter(StringOrStringArrayAdapter::class) + val schemaId: kotlin.collections.List? = null, + + /* The unique identifier (UUID) of the credential definition that will be used for this offer. It should be the identifier of a credential definition that exists in the issuer agent's database. Note that this parameter only applies when the offer is of type 'AnonCreds'. */ + @SerializedName("credentialDefinitionId") + val credentialDefinitionId: java.util.UUID? = null, + + /* The credential format for this offer (defaults to 'JWT') */ + @SerializedName("credentialFormat") + val credentialFormat: kotlin.String? = null, + + /* Specifies whether or not the credential should be automatically generated and issued when receiving the `CredentialRequest` from the holder. If set to `false`, a manual approval by the issuer via another API call will be required for the VC to be issued. */ + @SerializedName("automaticIssuance") + val automaticIssuance: kotlin.Boolean? = null, + + /* Specified the key ID (kid) of the DID, it will be used to sign credential. User should specify just the partial identifier of the key. The full id of the kid MUST be \"#\" Note the cryto algorithm used with depend type of the key. */ + @SerializedName("issuingKid") + val issuingKid: kotlin.String? = null, + + /* The unique identifier of a DIDComm connection that already exists between the this issuer agent and the holder cloud or edeg agent. It should be the identifier of a connection that exists in the issuer agent's database. This connection will be used to execute the issue credential protocol. Note: connectionId is only required when the offer is from existing connection. connectionId is not required when the offer is from invitation for connectionless issuance. */ + @SerializedName("connectionId") + val connectionId: java.util.UUID? = null, + + /* A self-attested code the receiver may want to display to the user or use in automatically deciding what to do with the out-of-band message. goalcode is optional and can be provided when the offer is from invitation for connectionless issuance. */ + @SerializedName("goalCode") + val goalCode: kotlin.String? = null, + + /* A self-attested string that the receiver may want to display to the user about the context-specific goal of the out-of-band message. goal is optional and can be provided when the offer is from invitation for connectionless issuance. */ + @SerializedName("goal") + val goal: kotlin.String? = null + +) + diff --git a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt index 9245b77e67..0ec683b89b 100644 --- a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt +++ b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt @@ -10,35 +10,27 @@ "ArrayInDataClass", "EnumEntryName", "RemoveRedundantQualifierName", - "UnusedImport" + "UnusedImport", ) package org.hyperledger.identus.client.models -import org.hyperledger.identus.client.models.Json - +import com.google.gson.JsonElement +import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName +import org.hyperledger.identus.client.adapters.StringOrStringArrayAdapter -/** - * A service expressed in the DID document. https://www.w3.org/TR/did-core/#services - * - * @param id The id of the service. Requires a URI fragment when use in create / update DID. Returns the full ID (with DID prefix) when resolving DID - * @param type - * @param serviceEndpoint - */ - - -data class Service ( +data class Service( /* The id of the service. Requires a URI fragment when use in create / update DID. Returns the full ID (with DID prefix) when resolving DID */ @SerializedName("id") val id: kotlin.String, @SerializedName("type") + @JsonAdapter(StringOrStringArrayAdapter::class) val type: kotlin.collections.List? = null, @SerializedName("serviceEndpoint") - val serviceEndpoint: Json - -) + val serviceEndpoint: JsonElement? = null, + ) diff --git a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceAction.kt b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceAction.kt index 819766835e..5c8aa24016 100644 --- a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceAction.kt +++ b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceAction.kt @@ -10,25 +10,23 @@ "ArrayInDataClass", "EnumEntryName", "RemoveRedundantQualifierName", - "UnusedImport" + "UnusedImport", ) package org.hyperledger.identus.client.models -import org.hyperledger.identus.client.models.Json - +import com.google.gson.JsonElement import com.google.gson.annotations.SerializedName /** * A patch to existing Service. 'type' and 'serviceEndpoint' cannot both be empty. * * @param id The id of the service to update - * @param type - * @param serviceEndpoint + * @param type + * @param serviceEndpoint */ - -data class UpdateManagedDIDServiceAction ( +data class UpdateManagedDIDServiceAction( /* The id of the service to update */ @SerializedName("id") @@ -38,7 +36,6 @@ data class UpdateManagedDIDServiceAction ( val type: kotlin.collections.List? = null, @SerializedName("serviceEndpoint") - val serviceEndpoint: Json? = null + val serviceEndpoint: JsonElement? = null, ) - diff --git a/cloud-agent/client/typescript/.openapi-generator-ignore b/cloud-agent/client/typescript/.openapi-generator-ignore index af11cd214d..cf11c2202e 100644 --- a/cloud-agent/client/typescript/.openapi-generator-ignore +++ b/cloud-agent/client/typescript/.openapi-generator-ignore @@ -6,3 +6,4 @@ models/CredentialRequest.ts models/Proof2.ts models/Service.ts models/UpdateManagedDIDServiceAction.ts +models/CreateIssueCredentialRecordRequest.ts diff --git a/cloud-agent/client/typescript/models/CreateIssueCredentialRecordRequest.ts b/cloud-agent/client/typescript/models/CreateIssueCredentialRecordRequest.ts new file mode 100644 index 0000000000..14190cd10d --- /dev/null +++ b/cloud-agent/client/typescript/models/CreateIssueCredentialRecordRequest.ts @@ -0,0 +1,135 @@ +/** + * Identus Cloud Agent API Reference + * The Identus Cloud Agent API facilitates the integration and management of self-sovereign identity capabilities within applications. It supports DID (Decentralized Identifiers) management, verifiable credential exchange, and secure messaging based on DIDComm standards. The API is designed to be interoperable with various blockchain and DLT (Distributed Ledger Technology) platforms, ensuring wide compatibility and flexibility. Key features include connection management, credential issuance and verification, and secure, privacy-preserving communication between entities. Additional information and the full list of capabilities can be found in the [Open Enterprise Agent documentation](https://docs.atalaprism.io/docs/category/prism-cloud-agent) + * + * OpenAPI spec version: 1.39.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { HttpFile } from '../http/http'; + +export class CreateIssueCredentialRecordRequest { + /** + * The validity period in seconds of the verifiable credential that will be issued. + */ + 'validityPeriod'?: number; + 'schemaId'?: string | Array; + /** + * The unique identifier (UUID) of the credential definition that will be used for this offer. It should be the identifier of a credential definition that exists in the issuer agent\'s database. Note that this parameter only applies when the offer is of type \'AnonCreds\'. + */ + 'credentialDefinitionId'?: string; + /** + * The credential format for this offer (defaults to \'JWT\') + */ + 'credentialFormat'?: string; + /** + * The set of claims that will be included in the issued credential. The JSON object should comply with the schema applicable for this offer (i.e. \'schemaId\' or \'credentialDefinitionId\'). + */ + 'claims': any | null; + /** + * Specifies whether or not the credential should be automatically generated and issued when receiving the `CredentialRequest` from the holder. If set to `false`, a manual approval by the issuer via another API call will be required for the VC to be issued. + */ + 'automaticIssuance'?: boolean; + /** + * The issuer Prism DID by which the verifiable credential will be issued. DID can be short for or long form. + */ + 'issuingDID': string; + /** + * Specified the key ID (kid) of the DID, it will be used to sign credential. User should specify just the partial identifier of the key. The full id of the kid MUST be \"#\" Note the cryto algorithm used with depend type of the key. + */ + 'issuingKid'?: string; + /** + * The unique identifier of a DIDComm connection that already exists between the this issuer agent and the holder cloud or edeg agent. It should be the identifier of a connection that exists in the issuer agent\'s database. This connection will be used to execute the issue credential protocol. Note: connectionId is only required when the offer is from existing connection. connectionId is not required when the offer is from invitation for connectionless issuance. + */ + 'connectionId'?: string; + /** + * A self-attested code the receiver may want to display to the user or use in automatically deciding what to do with the out-of-band message. goalcode is optional and can be provided when the offer is from invitation for connectionless issuance. + */ + 'goalCode'?: string; + /** + * A self-attested string that the receiver may want to display to the user about the context-specific goal of the out-of-band message. goal is optional and can be provided when the offer is from invitation for connectionless issuance. + */ + 'goal'?: string; + + static readonly discriminator: string | undefined = undefined; + + static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [ + { + "name": "validityPeriod", + "baseName": "validityPeriod", + "type": "number", + "format": "double" + }, + { + "name": "schemaId", + "baseName": "schemaId", + "type": "CreateIssueCredentialRecordRequestSchemaId", + "format": "" + }, + { + "name": "credentialDefinitionId", + "baseName": "credentialDefinitionId", + "type": "string", + "format": "uuid" + }, + { + "name": "credentialFormat", + "baseName": "credentialFormat", + "type": "string", + "format": "" + }, + { + "name": "claims", + "baseName": "claims", + "type": "any", + "format": "" + }, + { + "name": "automaticIssuance", + "baseName": "automaticIssuance", + "type": "boolean", + "format": "" + }, + { + "name": "issuingDID", + "baseName": "issuingDID", + "type": "string", + "format": "" + }, + { + "name": "issuingKid", + "baseName": "issuingKid", + "type": "string", + "format": "" + }, + { + "name": "connectionId", + "baseName": "connectionId", + "type": "string", + "format": "uuid" + }, + { + "name": "goalCode", + "baseName": "goalCode", + "type": "string", + "format": "" + }, + { + "name": "goal", + "baseName": "goal", + "type": "string", + "format": "" + } ]; + + static getAttributeTypeMap() { + return CreateIssueCredentialRecordRequest.attributeTypeMap; + } + + public constructor() { + } +} + diff --git a/cloud-agent/client/typescript/models/Service.ts b/cloud-agent/client/typescript/models/Service.ts index 5446e9d0e9..e8a2e6c8bd 100644 --- a/cloud-agent/client/typescript/models/Service.ts +++ b/cloud-agent/client/typescript/models/Service.ts @@ -1,17 +1,3 @@ -/** - * Prism Agent - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * OpenAPI spec version: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { Json } from '../models/Json'; - /** * A service expressed in the DID document. https://www.w3.org/TR/did-core/#services */ @@ -21,7 +7,7 @@ export class Service { */ 'id': string; 'type': Array; - 'serviceEndpoint': Json; + 'serviceEndpoint': string | Array | object; static readonly discriminator: string | undefined = undefined; diff --git a/cloud-agent/client/typescript/models/UpdateManagedDIDServiceAction.ts b/cloud-agent/client/typescript/models/UpdateManagedDIDServiceAction.ts index 987d8434fd..d7c6c9fbc2 100644 --- a/cloud-agent/client/typescript/models/UpdateManagedDIDServiceAction.ts +++ b/cloud-agent/client/typescript/models/UpdateManagedDIDServiceAction.ts @@ -1,17 +1,3 @@ -/** - * Prism Agent - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * OpenAPI spec version: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { Json } from '../models/Json'; - /** * A patch to existing Service. \'type\' and \'serviceEndpoint\' cannot both be empty. */ @@ -21,7 +7,7 @@ export class UpdateManagedDIDServiceAction { */ 'id': string; 'type'?: Array; - 'serviceEndpoint'?: Json; + 'serviceEndpoint'?: string | Array | object; static readonly discriminator: string | undefined = undefined; diff --git a/cloud-agent/service/server/src/main/resources/application.conf b/cloud-agent/service/server/src/main/resources/application.conf index e1632125f5..7b5bebb01a 100644 --- a/cloud-agent/service/server/src/main/resources/application.conf +++ b/cloud-agent/service/server/src/main/resources/application.conf @@ -34,22 +34,10 @@ pollux { publicEndpointUrl = "http://localhost:"${agent.httpEndpoint.http.port} publicEndpointUrl = ${?POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL} } - issueBgJobRecordsLimit = 25 - issueBgJobRecordsLimit = ${?ISSUE_BG_JOB_RECORDS_LIMIT} - issueBgJobRecurrenceDelay = 2 seconds - issueBgJobRecurrenceDelay = ${?ISSUE_BG_JOB_RECURRENCE_DELAY} - issueBgJobProcessingParallelism = 5 - issueBgJobProcessingParallelism = ${?ISSUE_BG_JOB_PROCESSING_PARALLELISM} - presentationBgJobRecordsLimit = 25 - presentationBgJobRecordsLimit = ${?PRESENTATION_BG_JOB_RECORDS_LIMIT} - presentationBgJobRecurrenceDelay = 2 seconds - presentationBgJobRecurrenceDelay = ${?PRESENTATION_BG_JOB_RECURRENCE_DELAY} - presentationBgJobProcessingParallelism = 5 - presentationBgJobProcessingParallelism = ${?PRESENTATION_BG_JOB_PROCESSING_PARALLELISM} - syncRevocationStatusesBgJobRecurrenceDelay = 2 seconds - syncRevocationStatusesBgJobRecurrenceDelay = ${?SYNC_REVOCATION_STATUSES_BG_JOB_RECURRENCE_DELAY} - syncRevocationStatusesBgJobProcessingParallelism = 5 - syncRevocationStatusesBgJobProcessingParallelism = ${?SYNC_REVOCATION_STATUSES_BG_JOB_PROCESSING_PARALLELISM} + statusListSyncTriggerRecurrenceDelay = 30 seconds + statusListSyncTriggerRecurrenceDelay = ${?STATUS_LIST_SYNC_TRIGGER_RECURRENCE_DELAY} + didStateSyncTriggerRecurrenceDelay = 30 seconds + didStateSyncTriggerRecurrenceDelay = ${?DID_STATE_SYNC_TRIGGER_RECURRENCE_DELAY} credential.sdJwt.expiry = 30 days credential.sdJwt.expiry = ${?CREDENTIAL_SD_JWT_EXPIRY} presentationInvitationExpiry = 300 seconds @@ -81,8 +69,6 @@ connect { connectBgJobRecordsLimit = ${?CONNECT_BG_JOB_RECORDS_LIMIT} connectBgJobRecurrenceDelay = 2 seconds connectBgJobRecurrenceDelay = ${?CONNECT_BG_JOB_RECURRENCE_DELAY} - connectBgJobProcessingParallelism = 5 - connectBgJobProcessingParallelism = ${?CONNECT_BG_JOB_PROCESSING_PARALLELISM} connectInvitationExpiry = 300 seconds connectInvitationExpiry = ${?CONNECT_INVITATION_EXPIRY} } @@ -93,6 +79,7 @@ agent { port = 8085 port =${?AGENT_HTTP_PORT} } + serviceName = "agent-base-url" publicEndpointUrl = "https://host.docker.internal:8080/cloud-agent" publicEndpointUrl = ${?REST_SERVICE_URL} } @@ -261,4 +248,49 @@ agent { authApiKey = "default" authApiKey = ${?DEFAULT_WALLET_AUTH_API_KEY} } + messagingService { + connectFlow { + consumerCount = 5 + retryStrategy { + maxRetries = 4 + initialDelay = 5.seconds + maxDelay = 40.seconds + } + } + issueFlow { + consumerCount = 5 + retryStrategy { + maxRetries = 4 + initialDelay = 5.seconds + maxDelay = 40.seconds + } + } + presentFlow { + consumerCount = 5 + retryStrategy { + maxRetries = 4 + initialDelay = 5.seconds + maxDelay = 40.seconds + } + } + didStateSync { + consumerCount = 5 + } + statusListSync { + consumerCount = 5 + } + inMemoryQueueCapacity = 1000 + kafkaEnabled = false + kafkaEnabled = ${?DEFAULT_KAFKA_ENABLED} + kafka { + bootstrapServers = "kafka:9092" + consumers { + autoCreateTopics = false, + maxPollRecords = 500 + maxPollInterval = 5.minutes + pollTimeout = 50.millis + rebalanceSafeCommits = true + } + } + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala index ced294305e..d010d4c5f6 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala @@ -5,12 +5,9 @@ import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.http.{ZHttp4sBlazeServer, ZHttpEndpoints} import org.hyperledger.identus.agent.server.jobs.* import org.hyperledger.identus.agent.walletapi.model.{Entity, Wallet, WalletSeed} -import org.hyperledger.identus.agent.walletapi.service.{EntityService, ManagedDIDService, WalletManagementService} -import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage +import org.hyperledger.identus.agent.walletapi.service.{EntityService, WalletManagementService} import org.hyperledger.identus.castor.controller.{DIDRegistrarServerEndpoints, DIDServerEndpoints} -import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.connect.controller.ConnectionServerEndpoints -import org.hyperledger.identus.connect.core.service.ConnectionService import org.hyperledger.identus.credentialstatus.controller.CredentialStatusServiceEndpoints import org.hyperledger.identus.event.controller.EventServerEndpoints import org.hyperledger.identus.event.notification.EventNotificationConfig @@ -18,107 +15,35 @@ import org.hyperledger.identus.iam.authentication.apikey.ApiKeyAuthenticator import org.hyperledger.identus.iam.entity.http.EntityServerEndpoints import org.hyperledger.identus.iam.wallet.http.WalletManagementServerEndpoints import org.hyperledger.identus.issue.controller.IssueServerEndpoints -import org.hyperledger.identus.mercury.{DidOps, HttpClient} import org.hyperledger.identus.oid4vci.CredentialIssuerServerEndpoints -import org.hyperledger.identus.pollux.core.service.{CredentialService, PresentationService} import org.hyperledger.identus.pollux.credentialdefinition.CredentialDefinitionRegistryServerEndpoints import org.hyperledger.identus.pollux.credentialschema.{ SchemaRegistryServerEndpoints, VerificationPolicyServerEndpoints } import org.hyperledger.identus.pollux.prex.PresentationExchangeServerEndpoints -import org.hyperledger.identus.pollux.vc.jwt.DidResolver as JwtDidResolver import org.hyperledger.identus.presentproof.controller.PresentProofServerEndpoints -import org.hyperledger.identus.resolvers.DIDResolver -import org.hyperledger.identus.shared.models.{HexString, WalletAccessContext, WalletAdministrationContext, WalletId} -import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds +import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.system.controller.SystemServerEndpoints import org.hyperledger.identus.verification.controller.VcVerificationServerEndpoints import zio.* -import zio.metrics.* - object CloudAgentApp { def run = for { _ <- AgentInitialization.run - _ <- issueCredentialDidCommExchangesJob.debug.fork - _ <- presentProofExchangeJob.debug.fork - _ <- connectDidCommExchangesJob.debug.fork - _ <- syncDIDPublicationStateFromDltJob.debug.fork - _ <- syncRevocationStatusListsJob.debug.fork + _ <- ConnectBackgroundJobs.connectFlowsHandler + _ <- IssueBackgroundJobs.issueFlowsHandler + _ <- PresentBackgroundJobs.presentFlowsHandler + _ <- DIDStateSyncBackgroundJobs.didStateSyncTrigger + _ <- DIDStateSyncBackgroundJobs.didStateSyncHandler + _ <- StatusListJobs.statusListsSyncTrigger + _ <- StatusListJobs.statusListSyncHandler _ <- AgentHttpServer.run.tapDefect(e => ZIO.logErrorCause("Agent HTTP Server failure", e)).fork fiber <- DidCommHttpServer.run.tapDefect(e => ZIO.logErrorCause("DIDComm HTTP Server failure", e)).fork _ <- WebhookPublisher.layer.build.map(_.get[WebhookPublisher]).flatMap(_.run.fork) _ <- fiber.join *> ZIO.log(s"Server End") _ <- ZIO.never } yield () - - private val issueCredentialDidCommExchangesJob: RIO[ - AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & CredentialService & DIDNonSecretStorage & - DIDService & ManagedDIDService & PresentationService & WalletManagementService, - Unit - ] = - for { - config <- ZIO.service[AppConfig] - _ <- (IssueBackgroundJobs.issueCredentialDidCommExchanges @@ Metric - .gauge("issuance_flow_did_com_exchange_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.pollux.issueBgJobRecurrenceDelay)) - .unit - } yield () - - private val presentProofExchangeJob: RIO[ - AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & PresentationService & CredentialService & - DIDNonSecretStorage & DIDService & ManagedDIDService, - Unit - ] = - for { - config <- ZIO.service[AppConfig] - _ <- (PresentBackgroundJobs.presentProofExchanges @@ Metric - .gauge("present_proof_flow_did_com_exchange_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.pollux.presentationBgJobRecurrenceDelay)) - .unit - } yield () - - private val connectDidCommExchangesJob: RIO[ - AppConfig & DidOps & DIDResolver & HttpClient & ConnectionService & ManagedDIDService & DIDNonSecretStorage & - WalletManagementService, - Unit - ] = - for { - config <- ZIO.service[AppConfig] - _ <- (ConnectBackgroundJobs.didCommExchanges @@ Metric - .gauge("connection_flow_did_com_exchange_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.connect.connectBgJobRecurrenceDelay)) - .unit - } yield () - - private val syncRevocationStatusListsJob = { - for { - config <- ZIO.service[AppConfig] - _ <- (StatusListJobs.syncRevocationStatuses @@ Metric - .gauge("revocation_status_list_sync_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.pollux.syncRevocationStatusesBgJobRecurrenceDelay)) - } yield () - } - - private val syncDIDPublicationStateFromDltJob: URIO[ManagedDIDService & WalletManagementService, Unit] = - ZIO - .serviceWithZIO[WalletManagementService](_.listWallets().map(_._1)) - .flatMap { wallets => - ZIO.foreach(wallets) { wallet => - DIDStateSyncBackgroundJobs.syncDIDPublicationStateFromDlt - .provideSomeLayer(ZLayer.succeed(WalletAccessContext(wallet.id))) - } - } - .catchAll(e => ZIO.logError(s"error while syncing DID publication state: $e")) - .repeat(Schedule.spaced(10.seconds)) - .unit - .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) - } object AgentHttpServer { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala index a8d2f0ddad..e02a3522c0 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala @@ -61,7 +61,7 @@ trait ControllerHelper { protected def extractPrismDIDFromString(maybeDid: String): IO[ErrorResponse, PrismDID] = ZIO .fromEither(PrismDID.fromString(maybeDid)) - .mapError(e => ErrorResponse.badRequest(detail = Some(s"Error parsing string as PrismDID: ${e}"))) + .mapError(e => ErrorResponse.badRequest(detail = Some(s"Error parsing string as PrismDID: $e"))) protected def getLongFormPrismDID( did: PrismDID, diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala index 10b4928d25..922593389a 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala @@ -7,7 +7,6 @@ import org.hyperledger.identus.agent.server.http.ZioHttpClient import org.hyperledger.identus.agent.server.sql.Migrations as AgentMigrations import org.hyperledger.identus.agent.walletapi.service.{ EntityServiceImpl, - ManagedDIDService, ManagedDIDServiceWithEventNotificationImpl, WalletManagementServiceImpl } @@ -16,8 +15,12 @@ import org.hyperledger.identus.agent.walletapi.sql.{ JdbcEntityRepository, JdbcWalletNonSecretStorage } -import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage import org.hyperledger.identus.castor.controller.{DIDControllerImpl, DIDRegistrarControllerImpl} +import org.hyperledger.identus.castor.core.model.did.{ + Service as DidDocumentService, + ServiceEndpoint as DidDocumentServiceEndpoint, + ServiceType as DidDocumentServiceType +} import org.hyperledger.identus.castor.core.service.DIDServiceImpl import org.hyperledger.identus.castor.core.util.DIDOperationValidator import org.hyperledger.identus.connect.controller.ConnectionControllerImpl @@ -31,7 +34,7 @@ import org.hyperledger.identus.iam.authentication.{DefaultAuthenticator, Oid4vci import org.hyperledger.identus.iam.authentication.apikey.JdbcAuthenticationRepository import org.hyperledger.identus.iam.authorization.core.EntityPermissionManagementService import org.hyperledger.identus.iam.authorization.DefaultPermissionManagementService -import org.hyperledger.identus.iam.entity.http.controller.{EntityController, EntityControllerImpl} +import org.hyperledger.identus.iam.entity.http.controller.EntityControllerImpl import org.hyperledger.identus.iam.wallet.http.controller.WalletManagementControllerImpl import org.hyperledger.identus.issue.controller.IssueControllerImpl import org.hyperledger.identus.mercury.* @@ -42,7 +45,6 @@ import org.hyperledger.identus.pollux.core.service.* import org.hyperledger.identus.pollux.core.service.verification.VcVerificationServiceImpl import org.hyperledger.identus.pollux.credentialdefinition.controller.CredentialDefinitionControllerImpl import org.hyperledger.identus.pollux.credentialschema.controller.{ - CredentialSchemaController, CredentialSchemaControllerImpl, VerificationPolicyControllerImpl } @@ -61,6 +63,9 @@ import org.hyperledger.identus.pollux.sql.repository.{ } import org.hyperledger.identus.presentproof.controller.PresentProofControllerImpl import org.hyperledger.identus.resolvers.DIDResolver +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.WalletIdAndRecordId +import org.hyperledger.identus.shared.models.WalletId import org.hyperledger.identus.system.controller.SystemControllerImpl import org.hyperledger.identus.verification.controller.VcVerificationControllerImpl import zio.* @@ -72,6 +77,7 @@ import zio.metrics.connectors.micrometer.MicrometerConfig import zio.metrics.jvm.DefaultJvmMetrics import java.security.Security +import java.util.UUID object MainApp extends ZIOAppDefault { @@ -142,9 +148,26 @@ object MainApp extends ZIOAppDefault { |""".stripMargin) .ignore + appConfig <- ZIO.service[AppConfig].provide(SystemModule.configLayer) + // these services are added to any DID document by default when they are created. + defaultDidDocumentServices = Set( + DidDocumentService( + id = appConfig.agent.httpEndpoint.serviceName, + serviceEndpoint = DidDocumentServiceEndpoint + .Single( + DidDocumentServiceEndpoint.UriOrJsonEndpoint + .Uri( + DidDocumentServiceEndpoint.UriValue + .fromString(appConfig.agent.httpEndpoint.publicEndpointUrl.toString) + .toOption + .get // This will fail if URL is invalid, which will prevent app from starting since public endpoint in config is invalid + ) + ), + `type` = DidDocumentServiceType.Single(DidDocumentServiceType.Name.fromStringUnsafe("LinkedResourceV1")) + ) + ) _ <- preMigrations _ <- migrations - app <- CloudAgentApp.run .provide( DidCommX.liveLayer, @@ -178,7 +201,7 @@ object MainApp extends ZIOAppDefault { AppModule.didJwtResolverLayer, DIDOperationValidator.layer(), DIDResolver.layer, - HttpURIDereferencerImpl.layer, + GenericUriResolverImpl.layer, PresentationDefinitionValidatorImpl.layer, // service ConnectionServiceImpl.layer >>> ConnectionServiceNotifier.layer, @@ -188,7 +211,7 @@ object MainApp extends ZIOAppDefault { LinkSecretServiceImpl.layer >>> CredentialServiceImpl.layer >>> CredentialServiceNotifier.layer, DIDServiceImpl.layer, EntityServiceImpl.layer, - ManagedDIDServiceWithEventNotificationImpl.layer, + ZLayer.succeed(defaultDidDocumentServices) >>> ManagedDIDServiceWithEventNotificationImpl.layer, LinkSecretServiceImpl.layer >>> PresentationServiceImpl.layer >>> PresentationServiceNotifier.layer, VerificationPolicyServiceImpl.layer, WalletManagementServiceImpl.layer, @@ -229,6 +252,11 @@ object MainApp extends ZIOAppDefault { // HTTP client SystemModule.zioHttpClientLayer, Scope.default, + // Messaging Service + ZLayer.fromZIO(ZIO.service[AppConfig].map(_.agent.messagingService)), + messaging.MessagingService.serviceLayer, + messaging.MessagingService.producerLayer[UUID, WalletIdAndRecordId], + messaging.MessagingService.producerLayer[WalletId, WalletId] ) } yield app diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala index 191a3b02cf..364ff510bc 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala @@ -4,7 +4,7 @@ import org.hyperledger.identus.castor.core.model.did.VerificationRelationship import org.hyperledger.identus.iam.authentication.AuthenticationConfig import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.db.DbConfig -import zio.config.* +import org.hyperledger.identus.shared.messaging.MessagingServiceConfig import zio.config.magnolia.* import zio.Config @@ -70,22 +70,13 @@ final case class PolluxConfig( database: DatabaseConfig, credentialSdJwtExpirationTime: Duration, statusListRegistry: StatusListRegistryConfig, - issueBgJobRecordsLimit: Int, - issueBgJobRecurrenceDelay: Duration, - issueBgJobProcessingParallelism: Int, - presentationBgJobRecordsLimit: Int, - presentationBgJobRecurrenceDelay: Duration, - presentationBgJobProcessingParallelism: Int, - syncRevocationStatusesBgJobRecurrenceDelay: Duration, - syncRevocationStatusesBgJobProcessingParallelism: Int, + statusListSyncTriggerRecurrenceDelay: Duration, + didStateSyncTriggerRecurrenceDelay: Duration, presentationInvitationExpiry: Duration, issuanceInvitationExpiry: Duration, ) final case class ConnectConfig( database: DatabaseConfig, - connectBgJobRecordsLimit: Int, - connectBgJobRecurrenceDelay: Duration, - connectBgJobProcessingParallelism: Int, connectInvitationExpiry: Duration, ) @@ -109,7 +100,7 @@ final case class DatabaseConfig( DbConfig( username = if (appUser) appUsername else username, password = if (appUser) appPassword else password, - jdbcUrl = s"jdbc:postgresql://${host}:${port}/${databaseName}", + jdbcUrl = s"jdbc:postgresql://$host:$port/${databaseName}", awaitConnectionThreads = awaitConnectionThreads ) } @@ -173,7 +164,8 @@ final case class AgentConfig( verification: VerificationConfig, secretStorage: SecretStorageConfig, webhookPublisher: WebhookPublisherConfig, - defaultWallet: DefaultWalletConfig + defaultWallet: DefaultWalletConfig, + messagingService: MessagingServiceConfig ) { def validate: Either[String, Unit] = for { @@ -187,7 +179,7 @@ final case class AgentConfig( } -final case class HttpEndpointConfig(http: HttpConfig, publicEndpointUrl: java.net.URL) +final case class HttpEndpointConfig(http: HttpConfig, serviceName: String, publicEndpointUrl: java.net.URL) final case class DidCommEndpointConfig(http: HttpConfig, publicEndpointUrl: java.net.URL) diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala index 0d73369e82..44ffa1cea8 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala @@ -1,9 +1,11 @@ package org.hyperledger.identus.agent.server.http +import org.http4s.{MediaType, Request, Response, Status} +import org.http4s.headers.`Content-Type` +import org.http4s.server.ServiceErrorHandler import org.hyperledger.identus.api.http.ErrorResponse import org.hyperledger.identus.shared.models.{Failure, StatusCode, UnmanagedFailureException} import org.log4s.* -import sttp.tapir.* import sttp.tapir.json.zio.jsonBody import sttp.tapir.server.interceptor.* import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, DefaultDecodeFailureHandler} @@ -11,6 +13,7 @@ import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler.F import sttp.tapir.server.interceptor.exception.ExceptionHandler import sttp.tapir.server.interceptor.reject.RejectHandler import sttp.tapir.server.model.ValuedEndpointOutput +import zio.{Task, ZIO} import scala.language.implicitConversions @@ -19,7 +22,7 @@ object CustomServerInterceptors { private val logger: Logger = getLogger private val endpointOutput = jsonBody[ErrorResponse] - private def defectHandler(response: ErrorResponse, maybeCause: Option[Throwable] = None) = { + private def tapirDefectHandler(response: ErrorResponse, maybeCause: Option[Throwable] = None) = { val statusCode = sttp.model.StatusCode(response.status) // Log defect as 'error' when status code matches a server error (5xx). Log other defects as 'debug'. (statusCode, maybeCause) match @@ -27,39 +30,43 @@ object CustomServerInterceptors { case (sc, None) if sc.isServerError => logger.error(endpointOutput.codec.encode(response)) case (_, Some(cause)) => logger.debug(cause)(endpointOutput.codec.encode(response)) case (_, None) => logger.debug(endpointOutput.codec.encode(response)) - Some(ValuedEndpointOutput(endpointOutput, response).prepend(sttp.tapir.statusCode, statusCode)) + ValuedEndpointOutput(endpointOutput, response).prepend(sttp.tapir.statusCode, statusCode) } - def exceptionHandler[F[_]]: ExceptionHandler[F] = ExceptionHandler.pure[F](ctx => + def tapirExceptionHandler[F[_]]: ExceptionHandler[F] = ExceptionHandler.pure[F](ctx => ctx.e match - case UnmanagedFailureException(failure: Failure) => defectHandler(failure) + case UnmanagedFailureException(failure: Failure) => Some(tapirDefectHandler(failure)) case e => - defectHandler( - ErrorResponse( - StatusCode.InternalServerError.code, - s"error:InternalServerError", - "Internal Server Error", - Some( - s"An unexpected error occurred when processing the request: " + - s"path=['${ctx.request.showShort}']" - ) - ), - Some(ctx.e) + Some( + tapirDefectHandler( + ErrorResponse( + StatusCode.InternalServerError.code, + s"error:InternalServerError", + "Internal Server Error", + Some( + s"An unexpected error occurred when processing the request: " + + s"path=['${ctx.request.showShort}']" + ) + ), + Some(ctx.e) + ) ) ) - def rejectHandler[F[_]]: RejectHandler[F] = RejectHandler.pure[F](resultFailure => - defectHandler( - ErrorResponse( - StatusCode.NotFound.code, - s"error:ResourcePathNotFound", - "Resource Path Not Found", - Some(s"The requested resource path doesn't exist.") + def tapirRejectHandler[F[_]]: RejectHandler[F] = RejectHandler.pure[F](resultFailure => + Some( + tapirDefectHandler( + ErrorResponse( + StatusCode.NotFound.code, + s"error:ResourcePathNotFound", + "Resource Path Not Found", + Some(s"The requested resource path doesn't exist.") + ) ) ) ) - def decodeFailureHandler: DecodeFailureHandler = (ctx: DecodeFailureContext) => { + def tapirDecodeFailureHandler: DecodeFailureHandler = (ctx: DecodeFailureContext) => { /** As per the Tapir Decode Failures documentation: * @@ -79,17 +86,39 @@ object CustomServerInterceptors { DefaultDecodeFailureHandler.respond(ctx) match case Some((sc, _)) => val details = FailureMessages.failureMessage(ctx) - defectHandler( - ErrorResponse( - sc.code, - s"error:RequestBodyDecodingFailure", - "Request Body Decoding Failure", - Some( - s"An error occurred when decoding the request body: " + - s"path=['${ctx.request.showShort}'], details=[$details]" + Some( + tapirDefectHandler( + ErrorResponse( + sc.code, + s"error:RequestBodyDecodingFailure", + "Request Body Decoding Failure", + Some( + s"An error occurred when decoding the request body: " + + s"path=['${ctx.request.showShort}'], details=[$details]" + ) ) ) ) case None => None } + + def http4sServiceErrorHandler: ServiceErrorHandler[Task] = (req: Request[Task]) => { case t: Throwable => + val res = tapirDefectHandler( + ErrorResponse( + StatusCode.InternalServerError.code, + s"error:InternalServerError", + "Internal Server Error", + Some( + s"An unexpected error occurred when servicing the request: " + + s"path=['${req.method.name} ${req.uri.copy(scheme = None, authority = None, fragment = None).toString}']" + ) + ), + Some(t) + ) + ZIO.succeed( + Response(Status.InternalServerError) + .withEntity(endpointOutput.codec.encode(res.value._2)) + .withContentType(`Content-Type`(MediaType.application.json)) + ) + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala index 05d56eb62b..1293185891 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala @@ -93,9 +93,9 @@ class ZHttp4sBlazeServer(micrometerRegistry: PrometheusMeterRegistry, metricsNam options <- ZIO.attempt { Http4sServerOptions .customiseInterceptors[Task] - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) .serverLog(None) .metricsInterceptor( srv.metricsInterceptor( @@ -123,6 +123,7 @@ class ZHttp4sBlazeServer(micrometerRegistry: PrometheusMeterRegistry, metricsNam ZIO.executor.flatMap(executor => BlazeServerBuilder[Task] .withExecutionContext(executor.asExecutionContext) + .withServiceErrorHandler(CustomServerInterceptors.http4sServiceErrorHandler) .bindHttp(port, "0.0.0.0") .withHttpApp(Router("/" -> http4sEndpoints).orNotFound) .serve diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala index 1708ca1517..67867bb2fb 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala @@ -19,7 +19,6 @@ import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation import org.hyperledger.identus.pollux.core.model.error.{CredentialServiceError, PresentationError} import org.hyperledger.identus.pollux.core.model.DidCommID import org.hyperledger.identus.pollux.core.service.CredentialService -import org.hyperledger.identus.pollux.sdjwt.SDJWT.* import org.hyperledger.identus.pollux.vc.jwt.{ DIDResolutionFailed, DIDResolutionSucceeded, @@ -29,8 +28,11 @@ import org.hyperledger.identus.pollux.vc.jwt.{ * } import org.hyperledger.identus.shared.crypto.* +import org.hyperledger.identus.shared.messaging.ConsumerJobConfig +import org.hyperledger.identus.shared.messaging.MessagingService.RetryStep import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext} -import zio.{ZIO, ZLayer} +import zio.{durationInt, Duration, ZIO, ZLayer} +import zio.prelude.OrdOps import java.time.Instant import java.util.Base64 @@ -229,4 +231,20 @@ trait BackgroundJobsHelper { case _ => ZIO.unit } } + + def retryStepsFromConfig(topicName: String, jobConfig: ConsumerJobConfig): Seq[RetryStep] = { + val retryTopics = jobConfig.retryStrategy match + case None => Seq.empty + case Some(rs) => + (1 to rs.maxRetries).map(i => + ( + s"$topicName-retry-$i", + rs.initialDelay.multipliedBy(Math.pow(2, i - 1).toLong).min(rs.maxDelay) + ) + ) + val topics = retryTopics prepended (topicName, 0.seconds) appended (s"$topicName-DLQ", Duration.Infinity) + (0 until topics.size - 1).map { i => + RetryStep(topics(i)._1, jobConfig.consumerCount, topics(i)._2, topics(i + 1)._1) + } + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala index 46335fd059..07cfd05a22 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala @@ -2,49 +2,63 @@ package org.hyperledger.identus.agent.server.jobs import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.jobs.BackgroundJobError.ErrorResponseReceivedFromPeerAgent -import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError -import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError.{KeyNotFoundError, WalletNotFoundError} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage -import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.{ - InvalidStateForOperation, - RecordIdNotFound -} import org.hyperledger.identus.connect.core.model.ConnectionRecord import org.hyperledger.identus.connect.core.model.ConnectionRecord.* import org.hyperledger.identus.connect.core.service.ConnectionService import org.hyperledger.identus.mercury.* -import org.hyperledger.identus.mercury.model.error.SendMessageError import org.hyperledger.identus.resolvers.DIDResolver -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* import zio.metrics.* +import java.util.UUID + object ConnectBackgroundJobs extends BackgroundJobsHelper { - val didCommExchanges = { - for { - connectionService <- ZIO.service[ConnectionService] - config <- ZIO.service[AppConfig] - records <- connectionService - .findRecordsByStatesForAllWallets( - ignoreWithZeroRetries = true, - limit = config.connect.connectBgJobRecordsLimit, - ConnectionRecord.ProtocolState.ConnectionRequestPending, - ConnectionRecord.ProtocolState.ConnectionResponsePending - ) - _ <- ZIO.foreachPar(records)(performExchange).withParallelism(config.connect.connectBgJobProcessingParallelism) - } yield () - } + private val TOPIC_NAME = "connect" + + val connectFlowsHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + ConnectBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.connectFlow) + ) + } yield () - private def performExchange( - record: ConnectionRecord - ): URIO[ + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ DidOps & DIDResolver & HttpClient & ConnectionService & ManagedDIDService & DIDNonSecretStorage & AppConfig, Unit - ] = { + ] = + (for { + _ <- ZIO.logDebug(s"!!! Handling recordId: ${message.value} via Kafka queue") + connectionService <- ZIO.service[ConnectionService] + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + record <- connectionService + .findRecordById(message.value.recordId) + .provideSome(ZLayer.succeed(walletAccessContext)) + .someOrElseZIO(ZIO.dieMessage("Record Not Found")) + _ <- performExchange(record) + .tapSomeError { case (walletAccessContext, errorResponse) => + for { + connectService <- ZIO.service[ConnectionService] + _ <- connectService + .reportProcessingFailure(record.id, Some(errorResponse)) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + } yield () + } + .catchAll { e => ZIO.fail(RuntimeException(s"Attempt failed with: ${e}")) } + } yield ()) @@ Metric + .gauge("connection_flow_did_com_exchange_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + + private def performExchange(record: ConnectionRecord) = { import ProtocolState.* import Role.* @@ -179,26 +193,10 @@ object ConnectBackgroundJobs extends BackgroundJobsHelper { @@ Metric .gauge("connection_flow_inviter_process_connection_record_ms_gauge") .trackDurationWith(_.toMetricsSeconds) - case _ => ZIO.unit + case r => ZIO.logWarning(s"Invalid candidate record received for processing: $r") *> ZIO.unit } exchange - .tapError({ - case walletNotFound: WalletNotFoundError => - ZIO.logErrorCause( - s"Connect - Error processing record: ${record.id}", - Cause.fail(walletNotFound) - ) - case ((walletAccessContext, errorResponse)) => - for { - connectService <- ZIO.service[ConnectionService] - _ <- connectService - .reportProcessingFailure(record.id, Some(errorResponse)) - .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - } yield () - }) - .catchAll(e => ZIO.logErrorCause(s"Connect - Error processing record: ${record.id} ", Cause.fail(e))) - .catchAllDefect(d => ZIO.logErrorCause(s"Connect - Defect processing record: ${record.id}", Cause.fail(d))) } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala index 5d4ff494ea..2ee44e91bc 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala @@ -1,17 +1,54 @@ package org.hyperledger.identus.agent.server.jobs -import org.hyperledger.identus.agent.walletapi.model.error.GetManagedDIDError -import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.agent.server.config.AppConfig +import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, WalletManagementService} +import org.hyperledger.identus.shared.messaging.{Message, MessagingService, Producer} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletAdministrationContext, WalletId} +import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* +import zio.metrics.Metric -object DIDStateSyncBackgroundJobs { +object DIDStateSyncBackgroundJobs extends BackgroundJobsHelper { - val syncDIDPublicationStateFromDlt: ZIO[WalletAccessContext with ManagedDIDService, GetManagedDIDError, Unit] = - for { + private val TOPIC_NAME = "sync-did-state" + + val didStateSyncTrigger = { + (for { + config <- ZIO.service[AppConfig] + producer <- ZIO.service[Producer[WalletId, WalletId]] + trigger = for { + walletManagementService <- ZIO.service[WalletManagementService] + wallets <- walletManagementService.listWallets().map(_._1) + _ <- ZIO.logInfo(s"Triggering DID state sync for '${wallets.size}' wallets") + _ <- ZIO.foreach(wallets)(w => producer.produce(TOPIC_NAME, w.id, w.id)) + } yield () + _ <- trigger + .catchAll(e => ZIO.logError(s"error while syncing DID publication state: $e")) + .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) + .repeat(Schedule.spaced(config.pollux.didStateSyncTriggerRecurrenceDelay)) + } yield ()).debug.fork + } + + val didStateSyncHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + DIDStateSyncBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.didStateSync) + ) + } yield () + + private def handleMessage(message: Message[WalletId, WalletId]): RIO[ManagedDIDService, Unit] = { + val effect = for { managedDidService <- ZIO.service[ManagedDIDService] _ <- managedDidService.syncManagedDIDState _ <- managedDidService.syncUnconfirmedUpdateOperations } yield () - + effect + .provideSomeLayer(ZLayer.succeed(WalletAccessContext(message.value))) + .catchAll(t => ZIO.logErrorCause("Unable to syncing DID publication state", Cause.fail(t))) + @@ Metric + .gauge("did_publication_state_sync_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala index ffc617df3c..3cdde2853f 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala @@ -2,40 +2,61 @@ package org.hyperledger.identus.agent.server.jobs import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.jobs.BackgroundJobError.ErrorResponseReceivedFromPeerAgent -import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError.WalletNotFoundError -import org.hyperledger.identus.castor.core.model.did.* +import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService +import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage import org.hyperledger.identus.mercury.* -import org.hyperledger.identus.mercury.protocol.issuecredential.* import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError import org.hyperledger.identus.pollux.core.service.CredentialService -import org.hyperledger.identus.shared.models.Failure +import org.hyperledger.identus.resolvers.DIDResolver +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* import zio.metrics.* +import java.util.UUID + object IssueBackgroundJobs extends BackgroundJobsHelper { - val issueCredentialDidCommExchanges = { - for { + private val TOPIC_NAME = "issue" + + val issueFlowsHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + IssueBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.issueFlow) + ) + } yield () + + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ + HttpClient & DidOps & DIDResolver & (CredentialService & DIDNonSecretStorage & (ManagedDIDService & AppConfig)), + Unit + ] = { + (for { + _ <- ZIO.logDebug(s"!!! Handling recordId: ${message.value} via Kafka queue") credentialService <- ZIO.service[CredentialService] - config <- ZIO.service[AppConfig] - records <- credentialService - .getIssueCredentialRecordsByStatesForAllWallets( - ignoreWithZeroRetries = true, - limit = config.pollux.issueBgJobRecordsLimit, - IssueCredentialRecord.ProtocolState.OfferPending, - IssueCredentialRecord.ProtocolState.RequestPending, - IssueCredentialRecord.ProtocolState.RequestGenerated, - IssueCredentialRecord.ProtocolState.RequestReceived, - IssueCredentialRecord.ProtocolState.CredentialPending, - IssueCredentialRecord.ProtocolState.CredentialGenerated - ) - _ <- ZIO - .foreachPar(records)(performIssueCredentialExchange) - .withParallelism(config.pollux.issueBgJobProcessingParallelism) - } yield () + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + record <- credentialService + .findById(DidCommID(message.value.recordId.toString)) + .provideSome(ZLayer.succeed(walletAccessContext)) + .someOrElseZIO(ZIO.dieMessage(s"Record Not Found: ${message.value.recordId}")) + _ <- performIssueCredentialExchange(record) + .tapSomeError { case (walletAccessContext, errorResponse) => + for { + credentialService <- ZIO.service[CredentialService] + _ <- credentialService + .reportProcessingFailure(record.id, Some(errorResponse)) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + } yield () + } + .catchAll { e => ZIO.fail(RuntimeException(s"Attempt failed with: ${e}")) } + } yield ()) @@ Metric + .gauge("issuance_flow_did_com_exchange_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) } private def counterMetric(key: String) = Metric @@ -136,7 +157,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { "issuance_flow_issuer_send_credential_msg_succeed_counter" ) - val aux = for { + val exchange = for { _ <- ZIO.logDebug(s"Running action with records => $record") _ <- record match { // Offer should be sent from Issuer to Holder @@ -227,8 +248,8 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { val holderPendingToGeneratedFlow = for { walletAccessContext <- ZIO .fromOption(offer.to) + .mapError(_ => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) .flatMap(buildWalletAccessContextLayer) - .mapError(e => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) result <- for { credentialService <- ZIO.service[CredentialService] _ <- credentialService @@ -273,8 +294,8 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { val holderPendingToGeneratedFlow = for { walletAccessContext <- ZIO .fromOption(offer.to) + .mapError(_ => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) .flatMap(buildWalletAccessContextLayer) - .mapError(e => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) result <- for { credentialService <- ZIO.service[CredentialService] _ <- credentialService @@ -319,8 +340,8 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { val holderPendingToGeneratedFlow = for { walletAccessContext <- ZIO .fromOption(offer.to) + .mapError(_ => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) .flatMap(buildWalletAccessContextLayer) - .mapError(e => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) result <- for { credentialService <- ZIO.service[CredentialService] @@ -629,33 +650,12 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { @@ IssuerSendCredentialAll @@ Metric .gauge("issuance_flow_issuer_send_cred_flow_ms_gauge") .trackDurationWith(_.toMetricsSeconds) - - case record: IssueCredentialRecord => - ZIO.logDebug(s"IssuanceRecord: ${record.id} - ${record.protocolState}") *> ZIO.unit + case r: IssueCredentialRecord => + ZIO.logWarning(s"Invalid candidate record received for processing: $r") *> ZIO.unit } } yield () - aux - .tapError( - { - case walletNotFound: WalletNotFoundError => ZIO.unit - case CredentialServiceError.RecordNotFound(_, _) => ZIO.unit - case CredentialServiceError.UnsupportedDidFormat(_) => ZIO.unit - case failure: Failure => ZIO.unit - case ((walletAccessContext, failure)) => - for { - credentialService <- ZIO.service[CredentialService] - _ <- credentialService - .reportProcessingFailure(record.id, Some(failure)) - .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - } yield () - } - ) - .catchAll(e => ZIO.logErrorCause(s"Issue Credential - Error processing record: ${record.id} ", Cause.fail(e))) - .catchAllDefect(d => - ZIO.logErrorCause(s"Issue Credential - Defect processing record: ${record.id}", Cause.fail(d)) - ) - + exchange } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala index 9c35fa6449..4bfb247176 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala @@ -30,17 +30,18 @@ import org.hyperledger.identus.pollux.sdjwt.{HolderPrivateKey, IssuerPublicKey, import org.hyperledger.identus.pollux.vc.jwt.{DidResolver as JwtDidResolver, Issuer as JwtIssuer, JWT, JwtPresentation} import org.hyperledger.identus.resolvers.DIDResolver import org.hyperledger.identus.shared.http.* -import org.hyperledger.identus.shared.models.* +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{Failure, *} import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* -import zio.json.* -import zio.json.ast.Json import zio.metrics.* import zio.prelude.Validation import zio.prelude.ZValidation.{Failure as ZFailure, *} -import java.time.{Clock, Instant, ZoneId} +import java.time.{Instant, ZoneId} +import java.util.UUID object PresentBackgroundJobs extends BackgroundJobsHelper { @@ -48,54 +49,57 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { /*DIDSecretStorageError | PresentationError | CredentialServiceError | BackgroundJobError | TransportError | */ CastorDIDResolutionError | GetManagedDIDError | Failure - private type RESOURCES = COMMON_RESOURCES & CredentialService & JwtDidResolver & DIDService & AppConfig & - MESSAGING_RESOURCES + private type RESOURCES = COMMON_RESOURCES & CredentialService & JwtDidResolver & UriResolver & DIDService & + AppConfig & MESSAGING_RESOURCES private type COMMON_RESOURCES = PresentationService & DIDNonSecretStorage & ManagedDIDService private type MESSAGING_RESOURCES = DidOps & DIDResolver & HttpClient - val presentProofExchanges: ZIO[RESOURCES, Throwable, Unit] = { - for { + private val TOPIC_NAME = "present" + + val presentFlowsHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + PresentBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.presentFlow) + ) + } yield () + + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ + RESOURCES, + Unit + ] = { + (for { + _ <- ZIO.logDebug(s"!!! Present Proof Handling recordId: ${message.value} via Kafka queue") presentationService <- ZIO.service[PresentationService] - config <- ZIO.service[AppConfig] - records <- presentationService - .getPresentationRecordsByStatesForAllWallets( - ignoreWithZeroRetries = true, - limit = config.pollux.presentationBgJobRecordsLimit, - PresentationRecord.ProtocolState.RequestPending, - PresentationRecord.ProtocolState.PresentationPending, - PresentationRecord.ProtocolState.PresentationGenerated, - PresentationRecord.ProtocolState.PresentationReceived - ) - .mapError(err => Throwable(s"Error occurred while getting Presentation records: $err")) - _ <- ZIO.logInfo(s"Processing ${records.size} Presentation records") - _ <- ZIO - .foreachPar(records)(performPresentProofExchange) - .withParallelism(config.pollux.presentationBgJobProcessingParallelism) - } yield () + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + record <- presentationService + .findPresentationRecord(DidCommID(message.value.recordId.toString)) + .provideSome(ZLayer.succeed(walletAccessContext)) + .someOrElseZIO(ZIO.dieMessage("Record Not Found")) + _ <- performPresentProofExchange(record) + .tapSomeError { case f: Failure => + for { + presentationService <- ZIO.service[PresentationService] + _ <- presentationService + .reportProcessingFailure(record.id, Some(f)) + } yield () + } + .catchAll { e => ZIO.fail(RuntimeException(s"Attempt failed with: ${e}")) } + } yield ()) @@ Metric + .gauge("present_proof_flow_did_com_exchange_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) } private def counterMetric(key: String) = Metric .counterInt(key) .fromConst(1) - private def performPresentProofExchange(record: PresentationRecord): URIO[RESOURCES, Unit] = - aux(record) - .catchAll { - case ex: Failure => - ZIO - .service[PresentationService] - .flatMap(_.reportProcessingFailure(record.id, Some(ex))) - case ex => ZIO.logErrorCause(s"PresentBackgroundJobs - Error processing record: ${record.id}", Cause.fail(ex)) - } - .catchAllDefect(d => - ZIO.logErrorCause(s"PresentBackgroundJobs - Defect processing record: ${record.id}", Cause.fail(d)) - ) - - private def aux(record: PresentationRecord): ZIO[RESOURCES, ERROR, Unit] = { + private def performPresentProofExchange(record: PresentationRecord): ZIO[RESOURCES, ERROR, Unit] = { import org.hyperledger.identus.pollux.core.model.PresentationRecord.ProtocolState.* - for { + val exchange = for { _ <- ZIO.logDebug(s"Running action with records => $record") _ <- record match { case PresentationRecord( @@ -604,6 +608,8 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { ZIO.logWarning(s"Unhandled PresentationRecord state: ${record.protocolState}") } } yield () + + exchange } object Prover { @@ -1035,7 +1041,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { credentialFormat: CredentialFormat, invitation: Option[Invitation] ): ZIO[ - AppConfig & JwtDidResolver & COMMON_RESOURCES & MESSAGING_RESOURCES, + AppConfig & JwtDidResolver & UriResolver & COMMON_RESOURCES & MESSAGING_RESOURCES, Failure, Unit ] = { @@ -1069,7 +1075,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { presentation: Presentation, invitation: Option[Invitation] ): ZIO[ - AppConfig & JwtDidResolver & COMMON_RESOURCES & MESSAGING_RESOURCES, + AppConfig & JwtDidResolver & UriResolver & COMMON_RESOURCES & MESSAGING_RESOURCES, Failure, Unit ] = { @@ -1114,28 +1120,12 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { // https://www.w3.org/TR/vc-data-model/#proofs-signatures-0 // A proof is typically attached to a verifiable presentation for authentication purposes // and to a verifiable credential as a method of assertion. - httpLayer <- ZIO.service[HttpClient] - httpUrlResolver = new UriResolver { - override def resolve(uri: String): IO[GenericUriResolverError, String] = { - val res = HttpClient - .get(uri) - .map(x => x.bodyAsString) - .provideSomeLayer(ZLayer.succeed(httpLayer)) - res.mapError(err => SchemaSpecificResolutionError("http", err)) - } - } - genericUriResolver = GenericUriResolver( - Map( - "data" -> DataUrlResolver(), - "http" -> httpUrlResolver, - "https" -> httpUrlResolver - ) - ) + uriResolver <- ZIO.service[UriResolver] result <- JwtPresentation .verify( JWT(base64Decoded), verificationConfig.toPresentationVerificationOptions() - )(didResolverService, genericUriResolver)(clock) + )(didResolverService, uriResolver)(clock) .mapError(error => PresentationError.PresentationVerificationError(error.mkString)) } yield result diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala index 71d02db3e2..1fe5d77551 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala @@ -1,131 +1,181 @@ package org.hyperledger.identus.agent.server.jobs import org.hyperledger.identus.agent.server.config.AppConfig +import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.castor.core.model.did.VerificationRelationship +import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.mercury.* import org.hyperledger.identus.mercury.protocol.revocationnotificaiton.RevocationNotification +import org.hyperledger.identus.pollux.core.model.{CredInStatusList, CredentialStatusListWithCreds} import org.hyperledger.identus.pollux.core.service.{CredentialService, CredentialStatusListService} -import org.hyperledger.identus.pollux.vc.jwt.revocation.{VCStatusList2021, VCStatusList2021Error} -import org.hyperledger.identus.shared.models.* +import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021, VCStatusList2021Error} +import org.hyperledger.identus.resolvers.DIDResolver +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, Producer, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* import zio.metrics.Metric +import java.util.UUID + object StatusListJobs extends BackgroundJobsHelper { - val syncRevocationStatuses = - for { - credentialStatusListService <- ZIO.service[CredentialStatusListService] - credentialService <- ZIO.service[CredentialService] - credentialStatusListsWithCreds <- credentialStatusListService.getCredentialsAndItsStatuses - @@ Metric - .gauge("revocation_status_list_sync_get_status_lists_w_creds_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) + private val TOPIC_NAME = "sync-status-list" - updatedVcStatusListsCredsEffects = credentialStatusListsWithCreds.map { statusListWithCreds => - val vcStatusListCredString = statusListWithCreds.statusListCredential - val walletAccessContext = WalletAccessContext(statusListWithCreds.walletId) + val statusListsSyncTrigger = { + (for { + config <- ZIO.service[AppConfig] + producer <- ZIO.service[Producer[UUID, WalletIdAndRecordId]] + trigger = for { + credentialStatusListService <- ZIO.service[CredentialStatusListService] + walletAndStatusListIds <- credentialStatusListService.getCredentialStatusListIds + _ <- ZIO.logInfo(s"Triggering status list revocation sync for '${walletAndStatusListIds.size}' status lists") + _ <- ZIO.foreach(walletAndStatusListIds) { (walletId, statusListId) => + producer.produce(TOPIC_NAME, walletId.toUUID, WalletIdAndRecordId(walletId.toUUID, statusListId)) + } + } yield () + _ <- trigger.repeat(Schedule.spaced(config.pollux.statusListSyncTriggerRecurrenceDelay)) + } yield ()).debug.fork + } - val effect = for { - vcStatusListCredJson <- ZIO - .fromEither(io.circe.parser.parse(vcStatusListCredString)) - .mapError(_.underlying) - issuer <- createJwtVcIssuer( - statusListWithCreds.issuer, - VerificationRelationship.AssertionMethod, - None - ) - vcStatusListCred <- VCStatusList2021 - .decodeFromJson(vcStatusListCredJson, issuer) - .mapError(x => new Throwable(x.msg)) - bitString <- vcStatusListCred.getBitString.mapError(x => new Throwable(x.msg)) - updateBitStringEffects = statusListWithCreds.credentials.map { cred => - if cred.isCanceled then { - val sendMessageEffect = for { - maybeIssueCredentialRecord <- credentialService.findById(cred.issueCredentialRecordId) - issueCredentialRecord <- ZIO - .fromOption(maybeIssueCredentialRecord) - .mapError(_ => - new Throwable(s"Issue credential record not found by id: ${cred.issueCredentialRecordId}") - ) - issueCredentialData <- ZIO - .fromOption(issueCredentialRecord.issueCredentialData) - .mapError(_ => - new Throwable( - s"Issue credential data not found in issue credential record by id: ${cred.issueCredentialRecordId}" - ) - ) - issueCredentialProtocolThreadId <- ZIO - .fromOption(issueCredentialData.thid) - .mapError(_ => new Throwable("thid not found in issue credential data")) - revocationNotification = RevocationNotification.build( - issueCredentialData.from, - issueCredentialData.to, - issueCredentialProtocolThreadId = issueCredentialProtocolThreadId - ) - didCommAgent <- buildDIDCommAgent(issueCredentialData.from) - response <- MessagingService - .send(revocationNotification.makeMessage) - .provideSomeLayer(didCommAgent) @@ Metric - .gauge("revocation_status_list_sync_revocation_notification_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) - } yield response + val statusListSyncHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + StatusListJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.statusListSync) + ) + } yield () - val updateBitStringEffect = bitString.setRevokedInPlace(cred.statusListIndex, true) + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ + DIDService & ManagedDIDService & CredentialService & DidOps & DIDResolver & HttpClient & + CredentialStatusListService, + Unit + ] = { + (for { + _ <- ZIO.logDebug(s"!!! Handling recordId: ${message.value} via Kafka queue") + credentialStatusListService <- ZIO.service[CredentialStatusListService] + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + statusListWithCreds <- credentialStatusListService + .getCredentialStatusListWithCreds(message.value.recordId) + .provideSome(ZLayer.succeed(walletAccessContext)) + _ <- updateStatusList(statusListWithCreds) + } yield ()) @@ Metric + .gauge("revocation_status_list_sync_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } - val updateAndNotify = for { - updated <- updateBitStringEffect.mapError(x => new Throwable(x.message)) - _ <- - if !cred.isProcessed then - sendMessageEffect.flatMap { resp => - if (resp.status >= 200 && resp.status < 300) - ZIO.logInfo("successfully sent revocation notification message") - else ZIO.logError(s"failed to send revocation notification message") - } - else ZIO.unit - } yield updated - updateAndNotify.provideSomeLayer(ZLayer.succeed(walletAccessContext)) @@ Metric - .gauge("revocation_status_list_sync_process_single_credential_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) - } else ZIO.unit - } - _ <- ZIO - .collectAll(updateBitStringEffects) + private def updateStatusList(statusListWithCreds: CredentialStatusListWithCreds) = { + for { + credentialStatusListService <- ZIO.service[CredentialStatusListService] + vcStatusListCredString = statusListWithCreds.statusListCredential + walletAccessContext = WalletAccessContext(statusListWithCreds.walletId) + effect = for { + vcStatusListCredJson <- ZIO + .fromEither(io.circe.parser.parse(vcStatusListCredString)) + .mapError(_.underlying) + issuer <- createJwtVcIssuer(statusListWithCreds.issuer, VerificationRelationship.AssertionMethod, None) + vcStatusListCred <- VCStatusList2021 + .decodeFromJson(vcStatusListCredJson, issuer) + .mapError(x => new Throwable(x.msg)) + bitString <- vcStatusListCred.getBitString.mapError(x => new Throwable(x.msg)) + _ <- ZIO.collectAll( + statusListWithCreds.credentials.map(c => + updateBitStringForCredentialAndNotify(bitString, c, walletAccessContext) + ) + ) + unprocessedEntityIds = statusListWithCreds.credentials.collect { + case x if !x.isProcessed && x.isCanceled => x.id + } + _ <- credentialStatusListService + .markAsProcessedMany(unprocessedEntityIds) + @@ Metric + .gauge("revocation_status_list_sync_mark_as_processed_many_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) - unprocessedEntityIds = statusListWithCreds.credentials.collect { - case x if !x.isProcessed && x.isCanceled => x.id - } - _ <- credentialStatusListService - .markAsProcessedMany(unprocessedEntityIds) - @@ Metric - .gauge("revocation_status_list_sync_mark_as_processed_many_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) + updatedVcStatusListCred <- vcStatusListCred.updateBitString(bitString).mapError { + case VCStatusList2021Error.EncodingError(msg: String) => new Throwable(msg) + case VCStatusList2021Error.DecodingError(msg: String) => new Throwable(msg) + } + vcStatusListCredJsonString <- updatedVcStatusListCred.toJsonWithEmbeddedProof.map(_.spaces2) + _ <- credentialStatusListService.updateStatusListCredential( + statusListWithCreds.id, + vcStatusListCredJsonString + ) + } yield () + _ <- effect + .catchAll(e => + ZIO.logErrorCause(s"Error processing status list record: ${statusListWithCreds.id} ", Cause.fail(e)) + ) + .catchAllDefect(d => + ZIO.logErrorCause(s"Defect processing status list record: ${statusListWithCreds.id}", Cause.fail(d)) + ) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + } yield () + } - updatedVcStatusListCred <- vcStatusListCred.updateBitString(bitString).mapError { - case VCStatusList2021Error.EncodingError(msg: String) => new Throwable(msg) - case VCStatusList2021Error.DecodingError(msg: String) => new Throwable(msg) - } - vcStatusListCredJsonString <- updatedVcStatusListCred.toJsonWithEmbeddedProof - .map(_.spaces2) - _ <- credentialStatusListService - .updateStatusListCredential(statusListWithCreds.id, vcStatusListCredJsonString) - } yield () + private def updateBitStringForCredentialAndNotify( + bitString: BitString, + credInStatusList: CredInStatusList, + walletAccessContext: WalletAccessContext + ) = { + for { + credentialService <- ZIO.service[CredentialService] + _ <- + if credInStatusList.isCanceled then { + val updateBitStringEffect = bitString.setRevokedInPlace(credInStatusList.statusListIndex, true) + val notifyEffect = sendRevocationNotificationMessage(credInStatusList) + val updateAndNotify = for { + updated <- updateBitStringEffect.mapError(x => new Throwable(x.message)) + _ <- + if !credInStatusList.isProcessed then + notifyEffect.flatMap { resp => + if (resp.status >= 200 && resp.status < 300) + ZIO.logInfo("successfully sent revocation notification message") + else ZIO.logError(s"failed to send revocation notification message") + } + else ZIO.unit + } yield updated + updateAndNotify.provideSomeLayer(ZLayer.succeed(walletAccessContext)) @@ Metric + .gauge("revocation_status_list_sync_process_single_credential_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } else ZIO.unit + } yield () + } - effect - .catchAll(e => - ZIO.logErrorCause(s"Error processing status list record: ${statusListWithCreds.id} ", Cause.fail(e)) - ) - .catchAllDefect(d => - ZIO.logErrorCause(s"Defect processing status list record: ${statusListWithCreds.id}", Cause.fail(d)) + private def sendRevocationNotificationMessage( + credInStatusList: CredInStatusList + ) = { + for { + credentialService <- ZIO.service[CredentialService] + maybeIssueCredentialRecord <- credentialService.findById(credInStatusList.issueCredentialRecordId) + issueCredentialRecord <- ZIO + .fromOption(maybeIssueCredentialRecord) + .mapError(_ => + new Throwable(s"Issue credential record not found by id: ${credInStatusList.issueCredentialRecordId}") + ) + issueCredentialData <- ZIO + .fromOption(issueCredentialRecord.issueCredentialData) + .mapError(_ => + new Throwable( + s"Issue credential data not found in issue credential record by id: ${credInStatusList.issueCredentialRecordId}" ) - .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - - } - config <- ZIO.service[AppConfig] - _ <- (ZIO - .collectAll(updatedVcStatusListsCredsEffects) @@ Metric - .gauge("revocation_status_list_sync_process_status_lists_w_creds_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .withParallelism(config.pollux.syncRevocationStatusesBgJobProcessingParallelism) - } yield () + ) + issueCredentialProtocolThreadId <- ZIO + .fromOption(issueCredentialData.thid) + .mapError(_ => new Throwable("thid not found in issue credential data")) + revocationNotification = RevocationNotification.build( + issueCredentialData.from, + issueCredentialData.to, + issueCredentialProtocolThreadId = issueCredentialProtocolThreadId + ) + didCommAgent <- buildDIDCommAgent(issueCredentialData.from) + response <- MessagingService + .send(revocationNotification.makeMessage) + .provideSomeLayer(didCommAgent) @@ Metric + .gauge("revocation_status_list_sync_revocation_notification_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } yield response + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala deleted file mode 100644 index 64269a575d..0000000000 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.hyperledger.identus.api.http.codec - -import io.circe.Json as CirceJson -import org.hyperledger.identus.shared.json.JsonInterop -import sttp.tapir.json.zio.* -import sttp.tapir.Schema -import zio.json.* -import zio.json.ast.Json as ZioJson - -object CirceJsonInterop { - given encodeJson: JsonEncoder[CirceJson] = JsonEncoder[ZioJson].contramap(JsonInterop.toZioJsonAst) - given decodeJson: JsonDecoder[CirceJson] = JsonDecoder[ZioJson].map(JsonInterop.toCirceJsonAst) - given schemaJson: Schema[CirceJson] = - Schema.derived[ZioJson].map[CirceJson](js => Some(JsonInterop.toCirceJsonAst(js)))(JsonInterop.toZioJsonAst) -} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala index 7229e911fc..52b566f436 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala @@ -1,15 +1,15 @@ package org.hyperledger.identus.castor.controller.http -import io.circe.Json -import org.hyperledger.identus.api.http.codec.CirceJsonInterop import org.hyperledger.identus.api.http.Annotation import org.hyperledger.identus.castor.controller.http.Service.annotations import org.hyperledger.identus.castor.core.model.{did as castorDomain, ProtoModelHelper} import org.hyperledger.identus.castor.core.model.did.w3c +import org.hyperledger.identus.shared.json.JsonInterop import org.hyperledger.identus.shared.utils.Traverse.* import sttp.tapir.Schema import sttp.tapir.Schema.annotations.{description, encodedExample} -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} +import zio.json.ast.Json import scala.language.implicitConversions @@ -48,7 +48,7 @@ object Service { extends Annotation[Json]( description = "The service endpoint. Can contain multiple possible values as described in the [Create DID operation](https://github.com/input-output-hk/prism-did-method-spec/blob/main/w3c-spec/PRISM-method.md#create-did)", - example = Json.fromString("https://example.com") + example = Json.Str("https://example.com") ) } @@ -62,7 +62,7 @@ object Service { Service( id = service.id, `type` = service.`type`, - serviceEndpoint = ServiceEndpoint.fromJson(service.serviceEndpoint) + serviceEndpoint = ServiceEndpoint.fromJson(JsonInterop.toZioJsonAst(service.serviceEndpoint)) ) extension (service: Service) { @@ -93,12 +93,13 @@ object ServiceType { case Single(value) => Left(value) case Multiple(values) => Right(values.toArray) } - given decoder: JsonDecoder[ServiceType] = JsonDecoder.string - .orElseEither(JsonDecoder.array[String]) - .map[ServiceType] { - case Left(value) => Single(value) - case Right(values) => Multiple(values.toSeq) - } + + given decoder: JsonDecoder[ServiceType] = JsonDecoder[String] + .map(Single.apply) + .orElse( + JsonDecoder[Seq[String]].map(Multiple.apply) + ) + given schema: Schema[ServiceType] = Schema .schemaForEither(Schema.schemaForString, Schema.schemaForArray[String]) .map[ServiceType] { @@ -139,9 +140,9 @@ object ServiceType { opaque type ServiceEndpoint = Json object ServiceEndpoint { - given encoder: JsonEncoder[ServiceEndpoint] = CirceJsonInterop.encodeJson - given decoder: JsonDecoder[ServiceEndpoint] = CirceJsonInterop.decodeJson - given schema: Schema[ServiceEndpoint] = CirceJsonInterop.schemaJson + given encoder: JsonEncoder[ServiceEndpoint] = Json.encoder + given decoder: JsonDecoder[ServiceEndpoint] = Json.decoder + given schema: Schema[ServiceEndpoint] = Schema.any[ServiceEndpoint] def fromJson(json: Json): ServiceEndpoint = json @@ -149,7 +150,7 @@ object ServiceEndpoint { def toDomain: Either[String, castorDomain.ServiceEndpoint] = { val stringEncoded = serviceEndpoint.asString match { case Some(s) => s - case None => serviceEndpoint.noSpaces + case None => serviceEndpoint.toJson } ProtoModelHelper.parseServiceEndpoint(stringEncoded) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala index 8d59f50530..0206b60827 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala @@ -22,7 +22,7 @@ case class StatusListCredential( `type`: Set[String], @description(annotations.issuer.description) @encodedExample(annotations.issuer.example) - issuer: Either[String, CredentialIssuer], + issuer: String | CredentialIssuer, @description(annotations.id.description) @encodedExample(annotations.id.example) id: String, @@ -136,11 +136,9 @@ object StatusListCredential { |""".stripMargin given StatusPurposeCodec: JsonCodec[StatusPurpose] = JsonCodec[StatusPurpose]( - JsonEncoder[String].contramap[StatusPurpose](_.str), - JsonDecoder[String].mapOrFail { - case StatusPurpose.Revocation.str => Right(StatusPurpose.Revocation) - case StatusPurpose.Suspension.str => Right(StatusPurpose.Suspension) - case str => Left(s"no enum value matched for \"$str\"") + JsonEncoder[String].contramap[StatusPurpose](_.toString), + JsonDecoder[String].mapOrFail { input => + StatusPurpose.values.find(_.toString.compareToIgnoreCase(input) == 0).toRight("Unknown StatusPurpose") }, ) @@ -156,11 +154,18 @@ object StatusListCredential { given credentialIssuerDecoder: JsonDecoder[CredentialIssuer] = DeriveJsonDecoder.gen[CredentialIssuer] - given eitherStringOrCredentialIssuerEncoder: JsonEncoder[Either[String, CredentialIssuer]] = - JsonEncoder[String].orElseEither(JsonEncoder[CredentialIssuer]) + given stringOrCredentialIssuerEncoder: JsonEncoder[String | CredentialIssuer] = + JsonEncoder[String] + .orElseEither(JsonEncoder[CredentialIssuer]) + .contramap[String | CredentialIssuer] { + case string: String => Left(string) + case credentialIssuer: CredentialIssuer => Right(credentialIssuer) + } - given eitherStringOrCredentialIssuerDecoder: JsonDecoder[Either[String, CredentialIssuer]] = - JsonDecoder[CredentialIssuer].map(Right(_)).orElse(JsonDecoder[String].map(Left(_))) + given stringOrCredentialIssuerDecoder: JsonDecoder[String | CredentialIssuer] = + JsonDecoder[CredentialIssuer] + .map(issuer => issuer: String | CredentialIssuer) + .orElse(JsonDecoder[String].map(schemaId => schemaId: String | CredentialIssuer)) given statusListCredentialEncoder: JsonEncoder[StatusListCredential] = DeriveJsonEncoder.gen[StatusListCredential] @@ -176,10 +181,20 @@ object StatusListCredential { given credentialSubjectSchema: Schema[CredentialSubject] = Schema.derived - given statusPurposeSchema: Schema[StatusPurpose] = Schema.derived + given statusPurposeSchema: Schema[StatusPurpose] = Schema.derivedEnumeration.defaultStringBased given credentialIssuerSchema: Schema[CredentialIssuer] = Schema.derived + given schemaIssuer: Schema[String | CredentialIssuer] = Schema + .schemaForEither(Schema.schemaForString, Schema.derived[CredentialIssuer]) + .map[String | CredentialIssuer] { + case Left(string) => Some(string) + case Right(credentialIssuer) => Some(credentialIssuer) + } { + case string: String => Left(string) + case credentialIssuer: CredentialIssuer => Right(credentialIssuer) + } + given statusListCredentialSchema: Schema[StatusListCredential] = Schema.derived } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala index 587b3376da..69cfc710bf 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala @@ -8,27 +8,28 @@ import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext} import org.hyperledger.identus.api.http.model.{CollectionStats, PaginationInput} import org.hyperledger.identus.api.util.PaginationUtils -import org.hyperledger.identus.castor.core.model.did.{PrismDID, VerificationRelationship} +import org.hyperledger.identus.castor.core.model.did.{DIDUrl, PrismDID, VerificationRelationship} import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.connect.core.service.ConnectionService -import org.hyperledger.identus.issue.controller.http.{ - AcceptCredentialOfferInvitation, - AcceptCredentialOfferRequest, - CreateIssueCredentialRecordRequest, - IssueCredentialRecord, - IssueCredentialRecordPage -} +import org.hyperledger.identus.issue.controller.http.* import org.hyperledger.identus.mercury.model.DidId -import org.hyperledger.identus.pollux.core.model.{CredentialFormat, DidCommID} +import org.hyperledger.identus.pollux.core.model.{CredentialFormat, DidCommID, ResourceResolutionMethod} import org.hyperledger.identus.pollux.core.model.CredentialFormat.{AnonCreds, JWT, SDJWT} import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.Role -import org.hyperledger.identus.pollux.core.service.CredentialService +import org.hyperledger.identus.pollux.core.service.{CredentialDefinitionService, CredentialService} +import org.hyperledger.identus.shared.crypto.Sha256Hash +import org.hyperledger.identus.shared.json.Json as JsonUtils import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext} -import zio.{Duration, URLayer, ZIO, ZLayer} +import org.hyperledger.identus.shared.utils.Base64Utils +import zio.* +import zio.json.given +import scala.collection.immutable.ListMap import scala.language.implicitConversions + class IssueControllerImpl( credentialService: CredentialService, + credentialDefinitionService: CredentialDefinitionService, connectionService: ConnectionService, didService: DIDService, managedDIDService: ManagedDIDService, @@ -48,100 +49,134 @@ class IssueControllerImpl( request: CreateIssueCredentialRecordRequest, offerContext: OfferContext ): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecord] = { + + def getIssuingDidFromRequest(request: CreateIssueCredentialRecordRequest) = extractPrismDIDFromString( + request.issuingDID + ) + for { - jsonClaims <- ZIO + jsonClaims <- ZIO // TODO: Get read of Circe and use zio-json all the way down .fromEither(io.circe.parser.parse(request.claims.toString())) - .mapError(e => ErrorResponse.badRequest(detail = Some(s"Invalid claims JSON: ${e.getMessage}"))) - + .mapError(e => ErrorResponse.badRequest(detail = Some(e.getMessage))) credentialFormat = request.credentialFormat.map(CredentialFormat.valueOf).getOrElse(CredentialFormat.JWT) + outcome <- + credentialFormat match + case JWT => + for { + issuingDID <- getIssuingDidFromRequest(request) + _ <- validatePrismDID(issuingDID, allowUnpublished = true, Role.Issuer) + record <- credentialService + .createJWTIssueCredentialRecord( + pairwiseIssuerDID = offerContext.pairwiseIssuerDID, + pairwiseHolderDID = offerContext.pairwiseHolderDID, + kidIssuer = request.issuingKid, + thid = DidCommID(), + maybeSchemaIds = request.schemaId.map { + case schemaId: String => List(schemaId) + case schemaIds: List[String] => schemaIds + }, + claims = jsonClaims, + validityPeriod = request.validityPeriod, + automaticIssuance = request.automaticIssuance.orElse(Some(true)), + issuingDID = issuingDID.asCanonical, + goalCode = offerContext.goalCode, + goal = offerContext.goal, + expirationDuration = offerContext.expirationDuration, + connectionId = request.connectionId + ) + } yield record + case SDJWT => + for { + issuingDID <- getIssuingDidFromRequest(request) + _ <- validatePrismDID(issuingDID, allowUnpublished = true, Role.Issuer) + record <- credentialService + .createSDJWTIssueCredentialRecord( + pairwiseIssuerDID = offerContext.pairwiseIssuerDID, + pairwiseHolderDID = offerContext.pairwiseHolderDID, + kidIssuer = request.issuingKid, + thid = DidCommID(), + maybeSchemaIds = request.schemaId.map { + case schemaId: String => List(schemaId) + case schemaIds: List[String] => schemaIds + }, + claims = jsonClaims, + validityPeriod = request.validityPeriod, + automaticIssuance = request.automaticIssuance.orElse(Some(true)), + issuingDID = issuingDID.asCanonical, + goalCode = offerContext.goalCode, + goal = offerContext.goal, + expirationDuration = offerContext.expirationDuration, + connectionId = request.connectionId + ) + } yield record + case AnonCreds => + for { + issuingDID <- getIssuingDidFromRequest(request) + credentialDefinitionGUID <- ZIO + .fromOption(request.credentialDefinitionId) + .mapError(_ => + ErrorResponse.badRequest(detail = Some("Missing request parameter: credentialDefinitionId")) + ) + credentialDefinition <- credentialDefinitionService.getByGUID(credentialDefinitionGUID) + credentialDefinitionId <- { - outcome <- credentialFormat match { - case JWT => - for { - issuingDID <- ZIO - .fromOption(request.issuingDID) - .mapError(_ => ErrorResponse.badRequest(detail = Some("Missing request parameter: issuingDID"))) - .flatMap(extractPrismDIDFromString) - _ <- validatePrismDID(issuingDID, allowUnpublished = true, Role.Issuer) - record <- credentialService - .createJWTIssueCredentialRecord( - pairwiseIssuerDID = offerContext.pairwiseIssuerDID, - pairwiseHolderDID = offerContext.pairwiseHolderDID, - kidIssuer = request.issuingKid, - thid = DidCommID(), - maybeSchemaId = request.schemaId, - claims = jsonClaims, - validityPeriod = request.validityPeriod, - automaticIssuance = request.automaticIssuance.orElse(Some(true)), - issuingDID = issuingDID.asCanonical, - goalCode = offerContext.goalCode, - goal = offerContext.goal, - expirationDuration = offerContext.expirationDuration, - connectionId = request.connectionId - ) - } yield record + credentialDefinition.resolutionMethod match + case ResourceResolutionMethod.did => + val publicEndpointServiceName = appConfig.agent.httpEndpoint.serviceName + val didUrlResourcePath = + s"credential-definition-registry/definitions/did-url/${credentialDefinitionGUID.toString}/definition" + val didUrl = for { + canonicalized <- JsonUtils.canonicalizeToJcs(credentialDefinition.definition.toJson) + encoded = Base64Utils.encodeURL(canonicalized.getBytes) + hash = Sha256Hash.compute(encoded.getBytes).hexEncoded + didUrl = DIDUrl( + issuingDID.did, + Seq(), + ListMap( + "resourceService" -> Seq(publicEndpointServiceName), + "resourcePath" -> Seq( + s"$didUrlResourcePath?resourceHash=$hash" + ), + ), + None + ).toString + } yield didUrl - case SDJWT => - for { - issuingDID <- ZIO - .fromOption(request.issuingDID) - .mapError(_ => ErrorResponse.badRequest(detail = Some("Missing request parameter: issuingDID"))) - .flatMap(extractPrismDIDFromString) - _ <- validatePrismDID(issuingDID, allowUnpublished = true, Role.Issuer) - record <- credentialService - .createSDJWTIssueCredentialRecord( - pairwiseIssuerDID = offerContext.pairwiseIssuerDID, - pairwiseHolderDID = offerContext.pairwiseHolderDID, - kidIssuer = request.issuingKid, - thid = DidCommID(), - maybeSchemaId = request.schemaId, - claims = jsonClaims, - validityPeriod = request.validityPeriod, - automaticIssuance = request.automaticIssuance.orElse(Some(true)), - issuingDID = issuingDID.asCanonical, - goalCode = offerContext.goalCode, - goal = offerContext.goal, - expirationDuration = offerContext.expirationDuration, - connectionId = request.connectionId - ) - } yield record + ZIO + .fromEither(didUrl) + .mapError(_ => ErrorResponse.badRequest(detail = Some("Could not parse credential definition"))) + + case ResourceResolutionMethod.http => + val publicEndpointUrl = appConfig.agent.httpEndpoint.publicEndpointUrl.toExternalForm + val httpUrlSuffix = + s"credential-definition-registry/definitions/${credentialDefinitionGUID.toString}/definition" + val urlPrefix = if (publicEndpointUrl.endsWith("/")) publicEndpointUrl else publicEndpointUrl + "/" + ZIO.succeed(s"$urlPrefix$httpUrlSuffix") + } + record <- credentialService + .createAnonCredsIssueCredentialRecord( + pairwiseIssuerDID = offerContext.pairwiseIssuerDID, + pairwiseHolderDID = offerContext.pairwiseHolderDID, + thid = DidCommID(), + credentialDefinitionGUID = credentialDefinitionGUID, + credentialDefinitionId = credentialDefinitionId, + claims = jsonClaims, + validityPeriod = request.validityPeriod, + automaticIssuance = request.automaticIssuance.orElse(Some(true)), + goalCode = offerContext.goalCode, + goal = offerContext.goal, + expirationDuration = offerContext.expirationDuration, + connectionId = request.connectionId + ) + } yield record - case AnonCreds => - for { - credentialDefinitionGUID <- ZIO - .fromOption(request.credentialDefinitionId) - .mapError(_ => - ErrorResponse.badRequest(detail = Some("Missing request parameter: credentialDefinitionId")) - ) - credentialDefinitionId = { - val publicEndpointUrl = appConfig.agent.httpEndpoint.publicEndpointUrl.toExternalForm - val urlSuffix = - s"credential-definition-registry/definitions/${credentialDefinitionGUID.toString}/definition" - val urlPrefix = if (publicEndpointUrl.endsWith("/")) publicEndpointUrl else publicEndpointUrl + "/" - s"$urlPrefix$urlSuffix" - } - record <- credentialService - .createAnonCredsIssueCredentialRecord( - pairwiseIssuerDID = offerContext.pairwiseIssuerDID, - pairwiseHolderDID = offerContext.pairwiseHolderDID, - thid = DidCommID(), - credentialDefinitionGUID = credentialDefinitionGUID, - credentialDefinitionId = credentialDefinitionId, - claims = jsonClaims, - validityPeriod = request.validityPeriod, - automaticIssuance = request.automaticIssuance.orElse(Some(true)), - goalCode = offerContext.goalCode, - goal = offerContext.goal, - expirationDuration = offerContext.expirationDuration, - connectionId = request.connectionId - ) - } yield record - } } yield IssueCredentialRecord.fromDomain(outcome) } + override def createCredentialOffer( request: CreateIssueCredentialRecordRequest )(implicit rc: RequestContext): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecord] = { + for { connectionId <- ZIO .fromOption(request.connectionId) @@ -173,6 +208,7 @@ class IssueControllerImpl( result <- createCredentialOfferRecord(request, offerContext) } yield result } + def acceptCredentialOfferInvitation( request: AcceptCredentialOfferInvitation )(implicit @@ -314,7 +350,9 @@ class IssueControllerImpl( } object IssueControllerImpl { - val layer - : URLayer[CredentialService & ConnectionService & DIDService & ManagedDIDService & AppConfig, IssueController] = - ZLayer.fromFunction(IssueControllerImpl(_, _, _, _, _)) + val layer: URLayer[ + CredentialService & CredentialDefinitionService & ConnectionService & DIDService & ManagedDIDService & AppConfig, + IssueController + ] = + ZLayer.fromFunction(IssueControllerImpl(_, _, _, _, _, _)) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala index 405b302409..5c530b194d 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala @@ -9,6 +9,7 @@ import sttp.tapir.Schema.annotations.{description, encodedExample} import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} import java.util.UUID +import scala.language.implicitConversions /** A class to represent an incoming request to create a new credential offer. * @@ -33,7 +34,7 @@ final case class CreateIssueCredentialRecordRequest( validityPeriod: Option[Double] = None, @description(annotations.schemaId.description) @encodedExample(annotations.schemaId.example) - schemaId: Option[String], + schemaId: Option[String | List[String]] = None, @description(annotations.credentialDefinitionId.description) @encodedExample(annotations.credentialDefinitionId.example) credentialDefinitionId: Option[UUID], @@ -48,7 +49,7 @@ final case class CreateIssueCredentialRecordRequest( automaticIssuance: Option[Boolean] = None, @description(annotations.issuingDID.description) @encodedExample(annotations.issuingDID.example) - issuingDID: Option[String], + issuingDID: String, @description(annotations.issuingKid.description) @encodedExample(annotations.issuingKid.example) issuingKid: Option[KeyId], @@ -129,12 +130,11 @@ object CreateIssueCredentialRecordRequest { ) object issuingDID - extends Annotation[Option[String]]( + extends Annotation[String]( description = """ - |The short-form issuer Prism DID by which the JWT verifiable credential will be issued. - |Note that this parameter only applies when the offer is type 'JWT'. + |The issuer Prism DID by which the verifiable credential will be issued. DID can be short for or long form. |""".stripMargin, - example = Some("did:prism:3bb0505d13fcb04d28a48234edb27b0d4e6d7e18a81e2c1abab58f3bbc21ce6f") + example = "did:prism:3bb0505d13fcb04d28a48234edb27b0d4e6d7e18a81e2c1abab58f3bbc21ce6f" ) object issuingKid @@ -178,6 +178,19 @@ object CreateIssueCredentialRecordRequest { ) } + given schemaIdEncoder: JsonEncoder[String | List[String]] = + JsonEncoder[String] + .orElseEither(JsonEncoder[List[String]]) + .contramap[String | List[String]] { + case schemaId: String => Left(schemaId) + case schemaIds: List[String] => Right(schemaIds) + } + + given schemaIdDecoder: JsonDecoder[String | List[String]] = + JsonDecoder[List[String]] + .map(schemaId => schemaId: String | List[String]) + .orElse(JsonDecoder[String].map(schemaId => schemaId: String | List[String])) + given encoder: JsonEncoder[CreateIssueCredentialRecordRequest] = DeriveJsonEncoder.gen[CreateIssueCredentialRecordRequest] @@ -185,6 +198,17 @@ object CreateIssueCredentialRecordRequest { DeriveJsonDecoder.gen[CreateIssueCredentialRecordRequest] given schemaJson: Schema[KeyId] = Schema.schemaForString.map[KeyId](v => Some(KeyId(v)))(KeyId.value) + + given schemaId: Schema[String | List[String]] = Schema + .schemaForEither(Schema.schemaForString, Schema.schemaForArray[String]) + .map[String | List[String]] { + case Left(value) => Some(value) + case Right(values) => Some(values.toList) + } { + case value: String => Left(value) + case values: List[String] => Right(values.toArray) + } + given schema: Schema[CreateIssueCredentialRecordRequest] = Schema.derived } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala index b49c11068a..1920718961 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.oid4vci import org.hyperledger.identus.api.http.{EndpointOutputs, ErrorResponse, RequestContext} +import org.hyperledger.identus.api.http.EndpointOutputs.FailureVariant import org.hyperledger.identus.iam.authentication.apikey.ApiKeyCredentials import org.hyperledger.identus.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader import org.hyperledger.identus.iam.authentication.oidc.JwtCredentials @@ -197,7 +198,14 @@ object CredentialIssuerEndpoints { statusCode(StatusCode.Created).description("Credential configuration created successfully") ) .out(jsonBody[CredentialConfiguration]) - .errorOut(EndpointOutputs.basicFailureAndNotFoundAndForbidden) + .errorOut( + EndpointOutputs.basicFailuresWith( + FailureVariant.notFound, + FailureVariant.unauthorized, + FailureVariant.forbidden, + FailureVariant.conflict + ) + ) .name("createCredentialConfiguration") .summary("Create a new credential configuration") .description( diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala index 69300821ec..c60a371342 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala @@ -125,10 +125,12 @@ case class CredentialIssuerControllerImpl( import CredentialIssuerController.Errors.* import OIDCCredentialIssuerService.Errors.* - private def parseURL(url: String): IO[ErrorResponse, URL] = + private def parseAbsoluteURL(url: String): IO[ErrorResponse, URL] = ZIO - .attempt(URI.create(url).toURL()) + .attempt(URI.create(url)) .mapError(ue => badRequest(detail = Some(s"Invalid URL: $url"))) + .filterOrFail(_.isAbsolute())(badRequest(detail = Some(s"Relative URL '$url' is not allowed"))) + .map(_.toURL()) private def baseCredentialIssuerUrl(issuerId: UUID): URL = URI(s"$agentBaseUrl/oid4vci/issuers/$issuerId").toURL() @@ -255,7 +257,7 @@ case class CredentialIssuerControllerImpl( request: CreateCredentialIssuerRequest ): ZIO[WalletAccessContext, ErrorResponse, CredentialIssuer] = for { - authServerUrl <- parseURL(request.authorizationServer.url) + authServerUrl <- parseAbsoluteURL(request.authorizationServer.url) id = request.id.getOrElse(UUID.randomUUID()) issuerToCreate = PolluxCredentialIssuer( id, @@ -287,7 +289,7 @@ case class CredentialIssuerControllerImpl( maybeAuthServerUrl <- ZIO .succeed(request.authorizationServer.flatMap(_.url)) .flatMap { - case Some(url) => parseURL(url).asSome + case Some(url) => parseAbsoluteURL(url).asSome case None => ZIO.none } issuer <- issuerMetadataService.updateCredentialIssuer( diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala index 839f1a4d8e..340dda1622 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala @@ -11,8 +11,7 @@ import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.service.{ CredentialService, OID4VCIIssuerMetadataService, - OID4VCIIssuerMetadataServiceError, - URIDereferencer + OID4VCIIssuerMetadataServiceError } import org.hyperledger.identus.pollux.vc.jwt.{ DidResolver, @@ -23,6 +22,7 @@ import org.hyperledger.identus.pollux.vc.jwt.{ W3cCredentialPayload, * } +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.* import zio.* @@ -111,7 +111,7 @@ case class OIDCCredentialIssuerServiceImpl( issuerMetadataService: OID4VCIIssuerMetadataService, issuanceSessionStorage: IssuanceSessionStorage, didResolver: DidResolver, - uriDereferencer: URIDereferencer, + uriResolver: UriResolver, ) extends OIDCCredentialIssuerService with Openid4VCIProofJwtOps { @@ -199,7 +199,7 @@ case class OIDCCredentialIssuerServiceImpl( `type` = Set( "VerifiableCredential" ) ++ credentialDefinition.`type`, // TODO: This information should come from Schema registry by record.schemaId - issuer = Left(issuerDid.toString), + issuer = issuerDid.toString, issuanceDate = Instant.now(), maybeExpirationDate = None, // TODO: Add expiration date maybeCredentialSchema = None, // TODO: Add schema from schema registry @@ -256,7 +256,7 @@ case class OIDCCredentialIssuerServiceImpl( } .map(_.schemaId) _ <- CredentialSchema - .validateJWTCredentialSubject(schemaId.toString(), simpleZioToCirce(claims).noSpaces, uriDereferencer) + .validateJWTCredentialSubject(schemaId.toString(), simpleZioToCirce(claims).noSpaces, uriResolver) .mapError(e => CredentialSchemaError(e)) session <- buildNewIssuanceSession(issuerId, issuingDID, claims, schemaId) _ <- issuanceSessionStorage @@ -320,7 +320,7 @@ case class OIDCCredentialIssuerServiceImpl( object OIDCCredentialIssuerServiceImpl { val layer: URLayer[ - DIDNonSecretStorage & CredentialService & IssuanceSessionStorage & DidResolver & URIDereferencer & + DIDNonSecretStorage & CredentialService & IssuanceSessionStorage & DidResolver & UriResolver & OID4VCIIssuerMetadataService, OIDCCredentialIssuerService ] = diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/PrismEnvelopeResponse.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/PrismEnvelopeResponse.scala new file mode 100644 index 0000000000..d32219ef52 --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/PrismEnvelopeResponse.scala @@ -0,0 +1,42 @@ +package org.hyperledger.identus.pollux + +import org.hyperledger.identus.api.http.* +import org.hyperledger.identus.pollux.PrismEnvelopeResponse.annotations +import org.hyperledger.identus.shared.models.PrismEnvelope +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{default, description, encodedExample, encodedName} +import zio.json.* + +case class PrismEnvelopeResponse( + @description(annotations.resource.description) + @encodedExample(annotations.resource.example) + resource: String, + @description(annotations.resource.description) + @encodedExample(annotations.url.example) + url: String +) extends PrismEnvelope + +object PrismEnvelopeResponse { + given encoder: JsonEncoder[PrismEnvelopeResponse] = + DeriveJsonEncoder.gen[PrismEnvelopeResponse] + + given decoder: JsonDecoder[PrismEnvelopeResponse] = + DeriveJsonDecoder.gen[PrismEnvelopeResponse] + + given schema: Schema[PrismEnvelopeResponse] = Schema.derived + + object annotations { + object resource + extends Annotation[String]( + description = "JCS normalized and base64url encoded json of the resource", + example = "" // TODO Add example + ) + + object url + extends Annotation[String]( + description = "DID url that can be used to resolve this resource", + example = + "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a?resourceService=agent-base-url&resourcePath=credential-definition-registry/definitions/did-url/ef3e4135-8fcf-3ce7-b5bb-df37defc13f6?resourceHash=4074bb1a8e0ea45437ad86763cd7e12de3fe8349ef19113df773b0d65c8a9c46" + ) + } +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala index 96592dc7d7..664981ba6f 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala @@ -9,11 +9,13 @@ import org.hyperledger.identus.iam.authentication.apikey.ApiKeyEndpointSecurityL import org.hyperledger.identus.iam.authentication.oidc.JwtCredentials import org.hyperledger.identus.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import org.hyperledger.identus.pollux.credentialdefinition.http.{ + CredentialDefinitionDidUrlResponsePage, CredentialDefinitionInput, CredentialDefinitionResponse, CredentialDefinitionResponsePage, FilterInput } +import org.hyperledger.identus.pollux.PrismEnvelopeResponse import sttp.apispec.{ExternalDocumentation, Tag} import sttp.model.StatusCode import sttp.tapir.{ @@ -52,7 +54,7 @@ object CredentialDefinitionRegistryEndpoints { val tag = Tag(name = tagName, description = Option(tagDescription), externalDocs = Option(tagExternalDocumentation)) - val createCredentialDefinitionEndpoint: Endpoint[ + val createCredentialDefinitionHttpUrlEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), (RequestContext, CredentialDefinitionInput), ErrorResponse, @@ -79,15 +81,52 @@ object CredentialDefinitionRegistryEndpoints { .out(jsonBody[http.CredentialDefinitionResponse]) .description("Credential definition record") .errorOut(basicFailureAndNotFoundAndForbidden) - .name("createCredentialDefinition") - .summary("Publish new definition to the definition registry") + .name("createCredentialDefinitionHttpUrl") + .summary("Publish new definition to the definition registry, resolvable by HTTP url") .description( "Create the new credential definition record with metadata and internal JSON Schema on behalf of Cloud Agent. " + "The credential definition will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it." ) .tag(tagName) - val getCredentialDefinitionByIdEndpoint: PublicEndpoint[ + val createCredentialDefinitionDidUrlEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, CredentialDefinitionInput), + ErrorResponse, + CredentialDefinitionResponse, + Any + ] = + endpoint.post + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in("credential-definition-registry" / "definitions" / "did-url") + .in( + jsonBody[CredentialDefinitionInput] + .description( + "JSON object required for the credential definition creation" + ) + ) + .out( + statusCode(StatusCode.Created) + .description( + "The new credential definition record is successfully created" + ) + ) + .out( + jsonBody[http.CredentialDefinitionResponse] + ) // We use same response as for HTTP url on DID url for definitions + .description("Credential definition record") + .errorOut(basicFailureAndNotFoundAndForbidden) + .name("createCredentialDefinitionDidUrl") + .summary("Publish new definition to the definition registry, resolvable by DID url") + .description( + "Create the new credential definition record with metadata and internal JSON Schema on behalf of the Cloud Agent. " + + "The credential definition will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it." + ) + .tag(tagName) + + val getCredentialDefinitionByIdHttpUrlEndpoint: PublicEndpoint[ (RequestContext, UUID), ErrorResponse, CredentialDefinitionResponse, @@ -102,14 +141,40 @@ object CredentialDefinitionRegistryEndpoints { ) .out(jsonBody[CredentialDefinitionResponse].description("CredentialDefinition found by `guid`")) .errorOut(basicFailuresAndNotFound) - .name("getCredentialDefinitionById") + .name("getCredentialDefinitionByIdHttpUrl") .summary("Fetch the credential definition from the registry by `guid`") .description( "Fetch the credential definition by the unique identifier" ) .tag(tagName) - val getCredentialDefinitionInnerDefinitionByIdEndpoint: PublicEndpoint[ + val getCredentialDefinitionByIdDidUrlEndpoint: PublicEndpoint[ + (RequestContext, UUID), + ErrorResponse, + PrismEnvelopeResponse, + Any + ] = + endpoint.get + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + "credential-definition-registry" / "definitions" / "did-url" / path[UUID]("guid").description( + "Globally unique identifier of the credential definition record" + ) + ) + .out( + jsonBody[PrismEnvelopeResponse].description( + "CredentialDefinition found by `guid`, wrapped in an envelope" + ) + ) + .errorOut(basicFailuresAndNotFound) + .name("getCredentialDefinitionByIdDidUrl") + .summary("Fetch the credential definition from the registry by `guid`, wrapped in an envelope") + .description( + "Fetch the credential definition by the unique identifier, it should have been crated via DID url, otherwise not found error is returned." + ) + .tag(tagName) + + val getCredentialDefinitionInnerDefinitionByIdHttpUrlEndpoint: PublicEndpoint[ (RequestContext, UUID), ErrorResponse, zio.json.ast.Json, @@ -124,16 +189,42 @@ object CredentialDefinitionRegistryEndpoints { ) .out(jsonBody[zio.json.ast.Json].description("CredentialDefinition found by `guid`")) .errorOut(basicFailuresAndNotFound) - .name("getCredentialDefinitionInnerDefinitionById") + .name("getCredentialDefinitionInnerDefinitionByIdHttpUrl") .summary("Fetch the inner definition field of the credential definition from the registry by `guid`") .description( "Fetch the inner definition fields of the credential definition by the unique identifier" ) .tag(tagName) + val getCredentialDefinitionInnerDefinitionByIdDidUrlEndpoint: PublicEndpoint[ + (RequestContext, UUID), + ErrorResponse, + PrismEnvelopeResponse, + Any + ] = + endpoint.get + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + "credential-definition-registry" / "definitions" / "did-url" / path[UUID]("guid") / "definition".description( + "Globally unique identifier of the credential definition record" + ) + ) + .out( + jsonBody[PrismEnvelopeResponse].description("CredentialDefinition found by `guid`") + ) + .errorOut(basicFailuresAndNotFound) + .name("getCredentialDefinitionInnerDefinitionByIdDidUrl") + .summary( + "Fetch the inner definition field of the credential definition from the registry by `guid`, wrapped in an envelope" + ) + .description( + "Fetch the inner definition fields of the credential definition by the unique identifier, it should have been crated via DID url, otherwise not found error is returned." + ) + .tag(tagName) + private val credentialDefinitionFilterInput: EndpointInput[http.FilterInput] = EndpointInput.derived[http.FilterInput] private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] - val lookupCredentialDefinitionsByQueryEndpoint: Endpoint[ + val lookupCredentialDefinitionsByQueryHttpUrlEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), ( RequestContext, @@ -159,10 +250,43 @@ object CredentialDefinitionRegistryEndpoints { .in(query[Option[Order]]("order")) .out(jsonBody[CredentialDefinitionResponsePage].description("Collection of CredentialDefinitions records.")) .errorOut(basicFailures) - .name("lookupCredentialDefinitionsByQuery") + .name("lookupCredentialDefinitionsByQueryHttpUrl") .summary("Lookup credential definitions by indexed fields") .description( "Lookup credential definitions by `author`, `name`, `tag` parameters and control the pagination by `offset` and `limit` parameters " ) .tag(tagName) + + val lookupCredentialDefinitionsByQueryDidUrlEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + ( + RequestContext, + FilterInput, + PaginationInput, + Option[Order] + ), + ErrorResponse, + CredentialDefinitionDidUrlResponsePage, + Any + ] = + endpoint.get + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + "credential-definition-registry" / "definitions" / "did-url".description( + "Lookup credential definitions by query" + ) + ) + .in(credentialDefinitionFilterInput) + .in(paginationInput) + .in(query[Option[Order]]("order")) + .out(jsonBody[CredentialDefinitionDidUrlResponsePage].description("Collection of CredentialDefinitions records.")) + .errorOut(basicFailures) + .name("lookupCredentialDefinitionsByQueryDidUrl") + .summary("Lookup credential definitions by indexed fields") + .description( + "Lookup DID url resolvable credential definitions by `author`, `name`, `tag` parameters and control the pagination by `offset` and `limit` parameters " + ) + .tag(tagName) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala index 6d3c053ecb..5d4e60615a 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.credentialdefinition +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext} import org.hyperledger.identus.api.http.model.{Order, PaginationInput} @@ -15,39 +16,57 @@ import zio.* import java.util.UUID class CredentialDefinitionRegistryServerEndpoints( + config: AppConfig, credentialDefinitionController: CredentialDefinitionController, authenticator: Authenticator[BaseEntity], authorizer: Authorizer[BaseEntity] ) { - val createCredentialDefinitionServerEndpoint: ZServerEndpoint[Any, Any] = - createCredentialDefinitionEndpoint + object create { + val http: ZServerEndpoint[Any, Any] = createCredentialDefinitionHttpUrlEndpoint .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) - .serverLogic { - case wac => { case (ctx: RequestContext, credentialDefinitionInput: CredentialDefinitionInput) => + .serverLogic { wac => + { case (ctx: RequestContext, credentialDefinitionInput: CredentialDefinitionInput) => credentialDefinitionController .createCredentialDefinition(credentialDefinitionInput)(ctx) .provideSomeLayer(ZLayer.succeed(wac)) .logTrace(ctx) } } + val did: ZServerEndpoint[Any, Any] = createCredentialDefinitionDidUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, credentialDefinitionInput: CredentialDefinitionInput) => + credentialDefinitionController + .createCredentialDefinitionDidUrl(credentialDefinitionInput)(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } - val getCredentialDefinitionByIdServerEndpoint: ZServerEndpoint[Any, Any] = - getCredentialDefinitionByIdEndpoint.zServerLogic { case (ctx: RequestContext, guid: UUID) => - credentialDefinitionController - .getCredentialDefinitionByGuid(guid)(ctx) - .logTrace(ctx) - } + val all = List(http, did) + } - val getCredentialDefinitionInnerDefinitionByIdServerEndpoint: ZServerEndpoint[Any, Any] = - getCredentialDefinitionInnerDefinitionByIdEndpoint.zServerLogic { case (ctx: RequestContext, guid: UUID) => - credentialDefinitionController - .getCredentialDefinitionInnerDefinitionByGuid(guid)(ctx) - .logTrace(ctx) + object get { + val http: ZServerEndpoint[Any, Any] = getCredentialDefinitionByIdHttpUrlEndpoint.zServerLogic { + case (ctx: RequestContext, guid: UUID) => + credentialDefinitionController + .getCredentialDefinitionByGuid(guid)(ctx) + .logTrace(ctx) } + val did: ZServerEndpoint[Any, Any] = getCredentialDefinitionByIdDidUrlEndpoint.zServerLogic { + case (ctx: RequestContext, guid: UUID) => + credentialDefinitionController + .getCredentialDefinitionByGuidDidUrl(config.agent.httpEndpoint.serviceName, guid)(ctx) + .logTrace(ctx) + } + + val all = List(http, did) - val lookupCredentialDefinitionsByQueryServerEndpoint: ZServerEndpoint[Any, Any] = - lookupCredentialDefinitionsByQueryEndpoint + } + + object getMany { + val http: ZServerEndpoint[Any, Any] = lookupCredentialDefinitionsByQueryHttpUrlEndpoint .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) .serverLogic { case wac => { @@ -62,22 +81,57 @@ class CredentialDefinitionRegistryServerEndpoints( .logTrace(ctx) } } + val did: ZServerEndpoint[Any, Any] = lookupCredentialDefinitionsByQueryDidUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { + case wac => { + case (ctx: RequestContext, filter: FilterInput, paginationInput: PaginationInput, order: Option[Order]) => + credentialDefinitionController + .lookupCredentialDefinitionsDidUrl( + config.agent.httpEndpoint.serviceName, + filter, + paginationInput.toPagination, + order + )(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } + + val all = List(http, did) + + } + + object getRaw { + val http: ZServerEndpoint[Any, Any] = getCredentialDefinitionInnerDefinitionByIdHttpUrlEndpoint.zServerLogic { + case (ctx: RequestContext, guid: UUID) => + credentialDefinitionController + .getCredentialDefinitionInnerDefinitionByGuid(guid)(ctx) + .logTrace(ctx) + } + val did: ZServerEndpoint[Any, Any] = getCredentialDefinitionInnerDefinitionByIdDidUrlEndpoint.zServerLogic { + case (ctx: RequestContext, guid: UUID) => + credentialDefinitionController + .getCredentialDefinitionInnerDefinitionByGuidDidUrl(config.agent.httpEndpoint.serviceName, guid)(ctx) + .logTrace(ctx) + } + + val all = List(http, did) + + } val all: List[ZServerEndpoint[Any, Any]] = - List( - createCredentialDefinitionServerEndpoint, - getCredentialDefinitionByIdServerEndpoint, - getCredentialDefinitionInnerDefinitionByIdServerEndpoint, - lookupCredentialDefinitionsByQueryServerEndpoint - ) + create.all ++ getMany.all ++ getRaw.all ++ get.all } object CredentialDefinitionRegistryServerEndpoints { - def all: URIO[CredentialDefinitionController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[CredentialDefinitionController & DefaultAuthenticator & AppConfig, List[ZServerEndpoint[Any, Any]]] = { for { credentialDefinitionRegistryService <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[DefaultAuthenticator] + config <- ZIO.service[AppConfig] credentialDefinitionRegistryEndpoints = new CredentialDefinitionRegistryServerEndpoints( + config, credentialDefinitionRegistryService, authenticator, authenticator diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionController.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionController.scala index 3f3b8ffe31..cffaa70a0e 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionController.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionController.scala @@ -3,11 +3,13 @@ package org.hyperledger.identus.pollux.credentialdefinition.controller import org.hyperledger.identus.api.http.* import org.hyperledger.identus.api.http.model.{Order, Pagination} import org.hyperledger.identus.pollux.credentialdefinition.http.{ + CredentialDefinitionDidUrlResponsePage, CredentialDefinitionInput, CredentialDefinitionResponse, CredentialDefinitionResponsePage, FilterInput } +import org.hyperledger.identus.pollux.PrismEnvelopeResponse import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* @@ -19,17 +21,25 @@ trait CredentialDefinitionController { rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponse] - def getCredentialDefinitionByGuid(id: UUID)(implicit + def createCredentialDefinitionDidUrl(in: CredentialDefinitionInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponse] + + def getCredentialDefinitionByGuid(guid: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, CredentialDefinitionResponse] + def getCredentialDefinitionByGuidDidUrl(baseUrlServiceName: String, guid: UUID)(implicit + rc: RequestContext + ): IO[ErrorResponse, PrismEnvelopeResponse] + def getCredentialDefinitionInnerDefinitionByGuid(id: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, zio.json.ast.Json] - def delete(guid: UUID)(implicit + def getCredentialDefinitionInnerDefinitionByGuidDidUrl(baseUrlServiceName: String, guid: UUID)(implicit rc: RequestContext - ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponse] + ): IO[ErrorResponse, PrismEnvelopeResponse] def lookupCredentialDefinitions( filter: FilterInput, @@ -39,4 +49,13 @@ trait CredentialDefinitionController { rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponsePage] + def lookupCredentialDefinitionsDidUrl( + baseUrlServiceName: String, + filter: FilterInput, + pagination: Pagination, + order: Option[Order] + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionDidUrlResponsePage] + } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerImpl.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerImpl.scala index c4039f4d0f..3051efe7b9 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerImpl.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerImpl.scala @@ -1,21 +1,25 @@ package org.hyperledger.identus.pollux.credentialdefinition.controller +import cats.implicits.* import org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, PublicationState} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.api.http.* import org.hyperledger.identus.api.http.model.{CollectionStats, Order, Pagination} import org.hyperledger.identus.castor.core.model.did.{LongFormPrismDID, PrismDID} +import org.hyperledger.identus.pollux.{credentialdefinition, PrismEnvelopeResponse} import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition.FilteredEntries +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.service.CredentialDefinitionService -import org.hyperledger.identus.pollux.credentialdefinition import org.hyperledger.identus.pollux.credentialdefinition.http.{ + CredentialDefinitionDidUrlResponse, + CredentialDefinitionDidUrlResponsePage, + CredentialDefinitionInnerDefinitionDidUrlResponse, CredentialDefinitionInput, CredentialDefinitionResponse, CredentialDefinitionResponsePage, FilterInput } import org.hyperledger.identus.pollux.credentialdefinition.http.CredentialDefinitionInput.toDomain -import org.hyperledger.identus.pollux.credentialdefinition.http.CredentialDefinitionResponse.fromDomain import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* import zio.json.ast.Json @@ -34,7 +38,23 @@ class CredentialDefinitionControllerImpl(service: CredentialDefinitionService, m _ <- validatePrismDID(in.author) result <- service .create(toDomain(in)) - .map(cs => fromDomain(cs).withBaseUri(rc.request.uri)) + .map(cs => CredentialDefinitionResponse.fromDomain(cs).withBaseUri(rc.request.uri)) + } yield result + } + + private def couldNotParseCredDefResponse(e: String) = ErrorResponse + .internalServerError(detail = Some(s"Error occurred while parsing the credential definition response: $e")) + + override def createCredentialDefinitionDidUrl( + in: CredentialDefinitionInput + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponse] = { + for { + _ <- validatePrismDID(in.author) + result <- service + .create(toDomain(in), ResourceResolutionMethod.did) + .map(cs => CredentialDefinitionResponse.fromDomain(cs).withBaseUri(rc.request.uri)) } yield result } @@ -44,28 +64,54 @@ class CredentialDefinitionControllerImpl(service: CredentialDefinitionService, m service .getByGUID(guid) .map( - fromDomain(_) + CredentialDefinitionResponse + .fromDomain(_) .withSelf(rc.request.uri.toString) ) } + override def getCredentialDefinitionByGuidDidUrl( + baseUrlServiceName: String, + guid: UUID + )(implicit rc: RequestContext): IO[ErrorResponse, PrismEnvelopeResponse] = { + + val res = for { + cd <- service.getByGUID(guid, ResourceResolutionMethod.did) + response <- ZIO + .fromEither(CredentialDefinitionDidUrlResponse.asPrismEnvelopeResponse(cd, baseUrlServiceName)) + .mapError(couldNotParseCredDefResponse) + + } yield response + + res + } + override def getCredentialDefinitionInnerDefinitionByGuid(id: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, Json] = { service .getByGUID(id) - .map(fromDomain(_).definition) + .map(CredentialDefinitionResponse.fromDomain(_).definition) } - override def delete(guid: UUID)(implicit + override def getCredentialDefinitionInnerDefinitionByGuidDidUrl(baseUrlServiceName: String, guid: UUID)(implicit rc: RequestContext - ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponse] = { - service - .delete(guid) - .map( - fromDomain(_) - .withBaseUri(rc.request.uri) - ) + ): IO[ErrorResponse, PrismEnvelopeResponse] = { + val res = for { + cd <- service.getByGUID(guid, ResourceResolutionMethod.did) + authorDid <- ZIO + .fromEither(PrismDID.fromString(cd.author)) + .mapError(_ => ErrorResponse.internalServerError(detail = Some("Invalid credential definition author DID"))) + response <- ZIO + .fromEither( + CredentialDefinitionInnerDefinitionDidUrlResponse + .asPrismEnvelopeResponse(cd.definition, authorDid, cd.guid, baseUrlServiceName) + ) + .mapError(couldNotParseCredDefResponse) + + } yield response + + res } override def lookupCredentialDefinitions( @@ -77,16 +123,43 @@ class CredentialDefinitionControllerImpl(service: CredentialDefinitionService, m ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponsePage] = { for { filteredEntries: FilteredEntries <- service.lookup( - filter.toDomain, + filter.toDomain(), pagination.offset, pagination.limit ) entries = filteredEntries.entries - .map(fromDomain(_).withBaseUri(rc.request.uri)) + .map(CredentialDefinitionResponse.fromDomain(_).withBaseUri(rc.request.uri)) .toList page = CredentialDefinitionResponsePage(entries) stats = CollectionStats(filteredEntries.totalCount, filteredEntries.count) - } yield CredentialDefinitionControllerLogic(rc, pagination, page, stats).result + } yield CredentialDefinitionControllerLogic(rc, pagination, stats).result(page) + } + + override def lookupCredentialDefinitionsDidUrl( + baseUrlServiceName: String, + filter: FilterInput, + pagination: Pagination, + order: Option[Order] + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionDidUrlResponsePage] = { + for { + filteredEntries: FilteredEntries <- service.lookup( + filter.toDomain(ResourceResolutionMethod.did), + pagination.offset, + pagination.limit + ) + + entriesZio = filteredEntries.entries + .traverse(cd => CredentialDefinitionDidUrlResponse.asPrismEnvelopeResponse(cd, baseUrlServiceName)) + + entries <- ZIO + .fromEither(entriesZio) + .mapError(couldNotParseCredDefResponse) + + page = CredentialDefinitionDidUrlResponsePage(entries) + stats = CollectionStats(filteredEntries.totalCount, filteredEntries.count) + } yield CredentialDefinitionControllerLogic(rc, pagination, stats).resultDidUrl(page) } private def validatePrismDID(author: String) = diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerLogic.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerLogic.scala index 9d90e3b5bd..ee446116f8 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerLogic.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerLogic.scala @@ -3,27 +3,24 @@ package org.hyperledger.identus.pollux.credentialdefinition.controller import org.hyperledger.identus.api.http.model.{CollectionStats, Pagination} import org.hyperledger.identus.api.http.RequestContext import org.hyperledger.identus.api.util.PaginationUtils -import org.hyperledger.identus.pollux.credentialdefinition.http.CredentialDefinitionResponsePage +import org.hyperledger.identus.pollux.credentialdefinition.http.{ + CredentialDefinitionDidUrlResponsePage, + CredentialDefinitionResponsePage +} import sttp.model.Uri case class CredentialDefinitionControllerLogic( ctx: RequestContext, pagination: Pagination, - page: CredentialDefinitionResponsePage, stats: CollectionStats ) { - private def composeNextUri(uri: Uri): Option[Uri] = - PaginationUtils.composeNextUri(uri, page.contents, pagination, stats) - - private def composePreviousUri(uri: Uri): Option[Uri] = - PaginationUtils.composePreviousUri(uri, page.contents, pagination, stats) + val self = ctx.request.uri.toString + val pageOf = ctx.request.uri.copy(querySegments = Seq.empty).toString - def result: CredentialDefinitionResponsePage = { - val self = ctx.request.uri.toString - val pageOf = ctx.request.uri.copy(querySegments = Seq.empty).toString - val next = composeNextUri(ctx.request.uri).map(_.toString) - val previous = composePreviousUri(ctx.request.uri).map(_.toString) + def result(page: CredentialDefinitionResponsePage): CredentialDefinitionResponsePage = { + val next = PaginationUtils.composeNextUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + val previous = PaginationUtils.composePreviousUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) val pageResult = page.copy( self = self, @@ -39,4 +36,20 @@ case class CredentialDefinitionControllerLogic( pageResult } + + def resultDidUrl(page: CredentialDefinitionDidUrlResponsePage): CredentialDefinitionDidUrlResponsePage = { + val next = PaginationUtils.composeNextUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + val previous = PaginationUtils.composePreviousUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + + val pageResult = page.copy( + self = self, + pageOf = pageOf, + next = next, + previous = previous, + contents = page.contents + ) + + pageResult + } + } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponse.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponse.scala new file mode 100644 index 0000000000..9b57575dd2 --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponse.scala @@ -0,0 +1,69 @@ +package org.hyperledger.identus.pollux.credentialdefinition.http + +import org.hyperledger.identus.castor.core.model.did.{DIDUrl, PrismDID} +import org.hyperledger.identus.pollux.core.model +import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition +import org.hyperledger.identus.pollux.PrismEnvelopeResponse +import org.hyperledger.identus.shared.crypto.Sha256Hash +import org.hyperledger.identus.shared.json.Json as JsonUtils +import org.hyperledger.identus.shared.utils.Base64Utils +import zio.json.* +import zio.json.ast.Json + +import java.util.UUID +import scala.collection.immutable.ListMap + +object CredentialDefinitionDidUrlResponse { + + def asPrismEnvelopeResponse(cd: CredentialDefinition, serviceName: String): Either[String, PrismEnvelopeResponse] = { + for { + authorDid <- PrismDID.fromString(cd.author) + canonicalized <- JsonUtils.canonicalizeToJcs(cd.toJson).left.map(_.toString) + encoded = Base64Utils.encodeURL(canonicalized.getBytes) + hash = Sha256Hash.compute(encoded.getBytes).hexEncoded + didUrl = DIDUrl( + authorDid.did, + Seq(), + ListMap( + "resourceService" -> Seq(serviceName), + "resourcePath" -> Seq(s"credential-definition-registry/definitions/did-url/${cd.guid}?resourceHash=$hash"), + ), + None + ).toString + } yield PrismEnvelopeResponse( + resource = encoded, + url = didUrl + ) + } +} + +object CredentialDefinitionInnerDefinitionDidUrlResponse { + + def asPrismEnvelopeResponse( + innerDefinition: Json, + authorDid: PrismDID, + definitionGuid: UUID, + serviceName: String + ): Either[String, PrismEnvelopeResponse] = { + for { + canonicalized <- JsonUtils.canonicalizeToJcs(innerDefinition.toJson).left.map(_.toString) + encoded = Base64Utils.encodeURL(canonicalized.getBytes) + hash = Sha256Hash.compute(encoded.getBytes).hexEncoded + didUrl = DIDUrl( + authorDid.did, + Seq(), + ListMap( + "resourceService" -> Seq(serviceName), + "resourcePath" -> Seq( + s"credential-definition-registry/definitions/did-url/$definitionGuid/definition?resourceHash=$hash" + ), + ), + None + ).toString + } yield PrismEnvelopeResponse( + resource = encoded, + url = didUrl + ) + } + +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponsePage.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponsePage.scala new file mode 100644 index 0000000000..bfcb77011e --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponsePage.scala @@ -0,0 +1,93 @@ +package org.hyperledger.identus.pollux.credentialdefinition.http + +import org.hyperledger.identus.api.http.Annotation +import org.hyperledger.identus.pollux.credentialdefinition.http.CredentialDefinitionResponsePage.annotations +import org.hyperledger.identus.pollux.PrismEnvelopeResponse +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{description, encodedExample} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +case class CredentialDefinitionDidUrlResponsePage( + @description(annotations.contents.description) + @encodedExample(annotations.contents.example) + contents: Seq[PrismEnvelopeResponse], + @description(annotations.kind.description) + @encodedExample(annotations.kind.example) + kind: String = "CredentialDefinitionDidUrlPage", + @description(annotations.self.description) + @encodedExample(annotations.self.example) + self: String = "", + @description(annotations.pageOf.description) + @encodedExample(annotations.pageOf.example) + pageOf: String = "", + @description(annotations.next.description) + @encodedExample(annotations.next.example) + next: Option[String] = None, + @description(annotations.previous.description) + @encodedExample(annotations.previous.example) + previous: Option[String] = None +) { + def withSelf(self: String) = copy(self = self) +} + +object CredentialDefinitionDidUrlResponsePage { + given encoder: JsonEncoder[CredentialDefinitionDidUrlResponsePage] = + DeriveJsonEncoder.gen[CredentialDefinitionDidUrlResponsePage] + + given decoder: JsonDecoder[CredentialDefinitionDidUrlResponsePage] = + DeriveJsonDecoder.gen[CredentialDefinitionDidUrlResponsePage] + + given schema: Schema[CredentialDefinitionDidUrlResponsePage] = Schema.derived + + val Example = CredentialDefinitionDidUrlResponsePage( + contents = annotations.contents.example, + kind = annotations.kind.example, + self = annotations.self.example, + pageOf = annotations.pageOf.example, + next = Some(annotations.next.example), + previous = Some(annotations.previous.example) + ) + + object annotations { + + object contents + extends Annotation[Seq[PrismEnvelopeResponse]]( + description = + "A sequence of PrismEnvelopeResponse objects representing the list of credential definitions that the API response contains", + example = Seq.empty + ) + + object kind + extends Annotation[String]( + description = + "A string field indicating the type of the API response. In this case, it will always be set to `CredentialDefinitionDidUrlPage`", + example = "CredentialDefinitionDidUrlPage" + ) // TODO Tech Debt ticket - the kind in a collection should be collection, not the underlying record type + + object self + extends Annotation[String]( + description = "A string field containing the URL of the current API endpoint", + example = "/cloud-agent/credential-definition-registry/definitions/did-url?skip=10&limit=10" + ) + + object pageOf + extends Annotation[String]( + description = "A string field indicating the type of resource that the contents field contains", + example = "/cloud-agent/credential-definition-registry/definitions/did-url" + ) + + object next + extends Annotation[String]( + description = "An optional string field containing the URL of the next page of results. " + + "If the API response does not contain any more pages, this field should be set to None.", + example = "/cloud-agent/credential-definition-registry/definitions/did-url?skip=20&limit=10" + ) + + object previous + extends Annotation[String]( + description = "An optional string field containing the URL of the previous page of results. " + + "If the API response is the first page of results, this field should be set to None.", + example = "/cloud-agent/credential-definition-registry/definitions/did-url?skip=0&limit=10" + ) + } +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/FilterInput.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/FilterInput.scala index cd39afb2b1..fcda095187 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/FilterInput.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/FilterInput.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.credentialdefinition.http import org.hyperledger.identus.api.http.* import org.hyperledger.identus.pollux.core.model import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.credentialdefinition.http.FilterInput.annotations import sttp.tapir.EndpointIO.annotations.{example, query} import sttp.tapir.Validator.* @@ -21,7 +22,8 @@ case class FilterInput( @example(Option(annotations.tag.example)) tag: Option[String] = Option.empty[String] ) { - def toDomain = CredentialDefinition.Filter(author, name, version, tag) + def toDomain(resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http) = + CredentialDefinition.Filter(author, name, version, tag, resolutionMethod) } object FilterInput { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryEndpoints.scala index 26e21ea5c9..1fae593271 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryEndpoints.scala @@ -9,24 +9,16 @@ import org.hyperledger.identus.iam.authentication.apikey.ApiKeyEndpointSecurityL import org.hyperledger.identus.iam.authentication.oidc.JwtCredentials import org.hyperledger.identus.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import org.hyperledger.identus.pollux.credentialschema.http.{ + CredentialSchemaDidUrlResponsePage, CredentialSchemaInput, CredentialSchemaResponse, CredentialSchemaResponsePage, FilterInput } +import org.hyperledger.identus.pollux.PrismEnvelopeResponse import sttp.apispec.{ExternalDocumentation, Tag} import sttp.model.StatusCode -import sttp.tapir.{ - endpoint, - extractFromRequest, - path, - query, - statusCode, - stringToPath, - Endpoint, - EndpointInput, - PublicEndpoint -} +import sttp.tapir.* import sttp.tapir.json.zio.{jsonBody, schemaForZioJsonValue} import zio.json.ast.Json @@ -65,8 +57,10 @@ object SchemaRegistryEndpoints { ) val tag = Tag(name = tagName, description = Option(tagDescription), externalDocs = Option(tagExternalDocumentation)) + val httpUrlPathPrefix = "schema-registry" / "schemas" + val didUrlPathPrefix = "schema-registry" / "schemas" / "did-url" - val createSchemaEndpoint: Endpoint[ + val createSchemaHttpUrlEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), (RequestContext, CredentialSchemaInput), ErrorResponse, @@ -77,7 +71,7 @@ object SchemaRegistryEndpoints { .securityIn(apiKeyHeader) .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) - .in("schema-registry" / "schemas") + .in(httpUrlPathPrefix) .in( jsonBody[CredentialSchemaInput] .description( @@ -94,16 +88,51 @@ object SchemaRegistryEndpoints { .description("Credential schema record") .errorOut(basicFailureAndNotFoundAndForbidden) .name("createSchema") - .summary("Publish new schema to the schema registry") + .summary("Publish new schema to the schema registry, http url resolvable") + .description( + "Create the new credential schema record with metadata and internal JSON Schema on behalf of Cloud Agent. " + + "The credential schema will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it." + ) + .tag(tagName) + + val createSchemaDidUrlEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, CredentialSchemaInput), + ErrorResponse, + PrismEnvelopeResponse, + Any + ] = + endpoint.post + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in(didUrlPathPrefix) + .in( + jsonBody[CredentialSchemaInput] + .description( + "JSON object required for the credential schema creation" + ) + ) + .out( + statusCode(StatusCode.Created) + .description( + "The new credential schema record is successfully created" + ) + ) + .out(jsonBody[PrismEnvelopeResponse]) + .description("Credential schema record") + .errorOut(basicFailureAndNotFoundAndForbidden) + .name("createSchemaDidUrl") + .summary("Publish new schema to the schema registry, did url resolvable") .description( "Create the new credential schema record with metadata and internal JSON Schema on behalf of Cloud Agent. " + "The credential schema will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it." ) .tag(tagName) - val updateSchemaEndpoint: Endpoint[ + val updateSchemaHttpUrlEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), - (RequestContext, String, UUID, CredentialSchemaInput), + (RequestContext, UUID, CredentialSchemaInput), ErrorResponse, CredentialSchemaResponse, Any @@ -113,9 +142,9 @@ object SchemaRegistryEndpoints { .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( - "schema-registry" / - path[String]("author").description(CredentialSchemaResponse.annotations.author.description) / - path[UUID]("id").description(CredentialSchemaResponse.annotations.id.description) + httpUrlPathPrefix / path[UUID]("id").description( + CredentialSchemaResponse.annotations.id.description + ) ) .in( jsonBody[CredentialSchemaInput] @@ -140,7 +169,46 @@ object SchemaRegistryEndpoints { ) .tag(tagName) - val getSchemaByIdEndpoint: PublicEndpoint[ + val updateSchemaDidUrlEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, UUID, CredentialSchemaInput), + ErrorResponse, + PrismEnvelopeResponse, + Any + ] = + endpoint.put + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + didUrlPathPrefix / path[UUID]("id").description( + CredentialSchemaResponse.annotations.id.description + ) + ) + .in( + jsonBody[CredentialSchemaInput] + .description( + "JSON object required for the credential schema update" + ) + ) + .out( + statusCode(StatusCode.Ok) + .description( + "The credential schema record is successfully updated" + ) + ) + .out(jsonBody[PrismEnvelopeResponse]) + .description("Credential schema record wrapped in an envelope") + .errorOut(basicFailureAndNotFoundAndForbidden) + .name("updateSchemaDidUrl") + .summary("Publish the new version of the credential schema to the schema registry") + .description( + "Publish the new version of the credential schema record with metadata and internal JSON Schema on behalf of Cloud Agent. " + + "The credential schema will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it." + ) + .tag(tagName) + + val getSchemaByIdHttpUrlEndpoint: PublicEndpoint[ (RequestContext, UUID), ErrorResponse, CredentialSchemaResponse, @@ -149,7 +217,7 @@ object SchemaRegistryEndpoints { endpoint.get .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( - "schema-registry" / "schemas" / path[UUID]("guid").description( + httpUrlPathPrefix / path[UUID]("guid").description( "Globally unique identifier of the credential schema record" ) ) @@ -162,16 +230,42 @@ object SchemaRegistryEndpoints { ) .tag(tagName) - val getRawSchemaByIdEndpoint: PublicEndpoint[ + val getSchemaByIdDidUrlEndpoint: PublicEndpoint[ (RequestContext, UUID), ErrorResponse, - Json, // changed to generic Json type + PrismEnvelopeResponse, Any ] = endpoint.get .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( - "schema-registry" / "schemas" / path[UUID]("guid") / "schema".description( + didUrlPathPrefix / path[UUID]("guid").description( + "Globally unique identifier of the credential schema record" + ) + ) + .out( + jsonBody[PrismEnvelopeResponse].description( + "CredentialSchema found by `guid`, wrapped in an envelope" + ) + ) + .errorOut(basicFailuresAndNotFound) + .name("getSchemaByIdDidUrl") + .summary("Fetch the schema from the registry by `guid`") + .description( + "Fetch the credential schema by the unique identifier" + ) + .tag(tagName) + + val getRawSchemaByIdHttpUrlEndpoint: PublicEndpoint[ + (RequestContext, UUID), + ErrorResponse, + Json, // returns json of raw schema + Any + ] = + endpoint.get + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + httpUrlPathPrefix / path[UUID]("guid") / "schema".description( "Globally unique identifier of the credential schema record" ) ) @@ -182,9 +276,32 @@ object SchemaRegistryEndpoints { .description("Fetch the credential schema by the unique identifier") .tag("Schema Registry") + val getRawSchemaByIdDidUrlEndpoint: PublicEndpoint[ + (RequestContext, UUID), + ErrorResponse, + PrismEnvelopeResponse, // returns an envelope, where resource is a json of wrapped schema + Any + ] = + endpoint.get + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + didUrlPathPrefix / path[UUID]("guid") / "schema".description( + "Globally unique identifier of the credential schema record" + ) + ) + .out( + jsonBody[PrismEnvelopeResponse].description("Raw JSON response of the CredentialSchema") + ) + .errorOut(basicFailuresAndNotFound) + .name("getRawSchemaByIdDidUrl") + .summary("Fetch the schema from the registry by `guid`") + .description("Fetch the credential schema by the unique identifier") + .tag("Schema Registry") + private val credentialSchemaFilterInput: EndpointInput[FilterInput] = EndpointInput.derived[FilterInput] private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] - val lookupSchemasByQueryEndpoint: Endpoint[ + + val lookupSchemasByQueryHttpUrlEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), ( RequestContext, @@ -212,4 +329,37 @@ object SchemaRegistryEndpoints { "Lookup schemas by `author`, `name`, `tags` parameters and control the pagination by `offset` and `limit` parameters " ) .tag(tagName) + + val lookupSchemasByQueryDidUrlEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + ( + RequestContext, + FilterInput, + PaginationInput, + Option[Order] + ), + ErrorResponse, + CredentialSchemaDidUrlResponsePage, + Any + ] = + endpoint.get + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in("schema-registry" / "schemas" / "did-url".description("Lookup schemas by query")) + .in(credentialSchemaFilterInput) + .in(paginationInput) + .in(query[Option[Order]]("order")) + .out( + jsonBody[CredentialSchemaDidUrlResponsePage].description( + "Collection of CredentialSchema records each wrapped in an envelope." + ) + ) + .errorOut(basicFailuresAndForbidden) + .name("lookupSchemasByQueryDidUrl") + .summary("Lookup schemas by indexed fields") + .description( + "Lookup schemas by `author`, `name`, `tags` parameters and control the pagination by `offset` and `limit` parameters " + ) + .tag(tagName) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryServerEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryServerEndpoints.scala index 75c8f4b1ec..7d8c57e7ca 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryServerEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryServerEndpoints.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.credentialschema +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.api.http.model.{Order, PaginationInput} import org.hyperledger.identus.api.http.RequestContext @@ -15,12 +16,14 @@ import zio.* import java.util.UUID class SchemaRegistryServerEndpoints( + config: AppConfig, credentialSchemaController: CredentialSchemaController, authenticator: Authenticator[BaseEntity], authorizer: Authorizer[BaseEntity] ) { - val createSchemaServerEndpoint: ZServerEndpoint[Any, Any] = - createSchemaEndpoint + + object create { + val http: ZServerEndpoint[Any, Any] = createSchemaHttpUrlEndpoint .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, schemaInput: CredentialSchemaInput) => @@ -30,65 +33,125 @@ class SchemaRegistryServerEndpoints( .logTrace(ctx) } } + val did: ZServerEndpoint[Any, Any] = createSchemaDidUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, schemaInput: CredentialSchemaInput) => + credentialSchemaController + .createSchemaDidUrl(config.agent.httpEndpoint.serviceName, schemaInput)(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } - val updateSchemaServerEndpoint: ZServerEndpoint[Any, Any] = - updateSchemaEndpoint + val all = List(http, did) + + } + + object update { + val http: ZServerEndpoint[Any, Any] = updateSchemaHttpUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, id: UUID, schemaInput: CredentialSchemaInput) => + credentialSchemaController + .updateSchema(id, schemaInput)(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } + val did: ZServerEndpoint[Any, Any] = updateSchemaDidUrlEndpoint .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) .serverLogic { wac => - { case (ctx: RequestContext, author: String, id: UUID, schemaInput: CredentialSchemaInput) => + { case (ctx: RequestContext, id: UUID, schemaInput: CredentialSchemaInput) => credentialSchemaController - .updateSchema(author, id, schemaInput)(ctx) + .updateSchemaDidUrl(config.agent.httpEndpoint.serviceName, id, schemaInput)(ctx) .provideSomeLayer(ZLayer.succeed(wac)) .logTrace(ctx) } } + val all = List(http, did) + + } - val getSchemaByIdServerEndpoint: ZServerEndpoint[Any, Any] = - getSchemaByIdEndpoint + object get { + val http: ZServerEndpoint[Any, Any] = getSchemaByIdHttpUrlEndpoint .zServerLogic { case (ctx: RequestContext, guid: UUID) => credentialSchemaController .getSchemaByGuid(guid)(ctx) .logTrace(ctx) } + val did: ZServerEndpoint[Any, Any] = getSchemaByIdDidUrlEndpoint + .zServerLogic { case (ctx: RequestContext, guid: UUID) => + credentialSchemaController + .getSchemaByGuidDidUrl(config.agent.httpEndpoint.serviceName, guid)(ctx) + .logTrace(ctx) + } + val all = List(http, did) + + } - val getRawSchemaByIdServerEndpoint: ZServerEndpoint[Any, Any] = - getRawSchemaByIdEndpoint + object getRaw { + val http: ZServerEndpoint[Any, Any] = getRawSchemaByIdHttpUrlEndpoint .zServerLogic { case (ctx: RequestContext, guid: UUID) => credentialSchemaController.getSchemaJsonByGuid(guid)(ctx) } + val did: ZServerEndpoint[Any, Any] = getRawSchemaByIdDidUrlEndpoint + .zServerLogic { case (ctx: RequestContext, guid: UUID) => + credentialSchemaController.getSchemaJsonByGuidDidUrl(config.agent.httpEndpoint.serviceName, guid)(ctx) + } + val all = List(http, did) - val lookupSchemasByQueryServerEndpoint: ZServerEndpoint[Any, Any] = - lookupSchemasByQueryEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) - .serverLogic { wac => - { case (ctx: RequestContext, filter: FilterInput, paginationInput: PaginationInput, order: Option[Order]) => - credentialSchemaController - .lookupSchemas( - filter, - paginationInput.toPagination, - order - )(ctx) - .provideSomeLayer(ZLayer.succeed(wac)) - .logTrace(ctx) + } + + object getMany { + val http: ZServerEndpoint[Any, Any] = + lookupSchemasByQueryHttpUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, filter: FilterInput, paginationInput: PaginationInput, order: Option[Order]) => + credentialSchemaController + .lookupSchemas( + filter, + paginationInput.toPagination, + order, + )(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } } - } + + val did: ZServerEndpoint[Any, Any] = + lookupSchemasByQueryDidUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, filter: FilterInput, paginationInput: PaginationInput, order: Option[Order]) => + credentialSchemaController + .lookupSchemasDidUrl( + config.agent.httpEndpoint.serviceName, + filter, + paginationInput.toPagination, + order, + )(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } + + val all = List(http, did) + } val all: List[ZServerEndpoint[Any, Any]] = - List( - createSchemaServerEndpoint, - updateSchemaServerEndpoint, - getSchemaByIdServerEndpoint, - getRawSchemaByIdServerEndpoint, - lookupSchemasByQueryServerEndpoint - ) + create.all ++ update.all ++ getMany.all ++ getRaw.all ++ get.all } object SchemaRegistryServerEndpoints { - def all: URIO[CredentialSchemaController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[CredentialSchemaController & DefaultAuthenticator & AppConfig, List[ZServerEndpoint[Any, Any]]] = { for { authenticator <- ZIO.service[DefaultAuthenticator] + config <- ZIO.service[AppConfig] schemaRegistryService <- ZIO.service[CredentialSchemaController] schemaRegistryEndpoints = new SchemaRegistryServerEndpoints( + config, schemaRegistryService, authenticator, authenticator diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaController.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaController.scala index 938ddfaed1..d5576d5a27 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaController.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaController.scala @@ -3,11 +3,13 @@ package org.hyperledger.identus.pollux.credentialschema.controller import org.hyperledger.identus.api.http.* import org.hyperledger.identus.api.http.model.{Order, Pagination} import org.hyperledger.identus.pollux.credentialschema.http.{ + CredentialSchemaDidUrlResponsePage, CredentialSchemaInput, CredentialSchemaResponse, CredentialSchemaResponsePage, FilterInput } +import org.hyperledger.identus.pollux.PrismEnvelopeResponse import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* import zio.json.ast.Json @@ -20,27 +22,48 @@ trait CredentialSchemaController { rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] - def updateSchema(author: String, id: UUID, in: CredentialSchemaInput)(implicit + def createSchemaDidUrl(baseUrlServiceName: String, in: CredentialSchemaInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PrismEnvelopeResponse] + + def updateSchema(id: UUID, in: CredentialSchemaInput)(implicit rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] + def updateSchemaDidUrl(baseUrlServiceName: String, id: UUID, in: CredentialSchemaInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PrismEnvelopeResponse] + def getSchemaByGuid(id: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, CredentialSchemaResponse] + def getSchemaByGuidDidUrl(baseUrlServiceName: String, id: UUID)(implicit + rc: RequestContext + ): IO[ErrorResponse, PrismEnvelopeResponse] + def getSchemaJsonByGuid(id: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, Json] - def delete(guid: UUID)(implicit + def getSchemaJsonByGuidDidUrl(baseUrlServiceName: String, id: UUID)(implicit rc: RequestContext - ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] + ): IO[ErrorResponse, PrismEnvelopeResponse] def lookupSchemas( filter: FilterInput, pagination: Pagination, - order: Option[Order] + order: Option[Order], )(implicit rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponsePage] + + def lookupSchemasDidUrl( + baseUrlServiceName: String, + filter: FilterInput, + pagination: Pagination, + order: Option[Order], + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaDidUrlResponsePage] } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerImpl.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerImpl.scala index 3804d24a4e..91e66f97d8 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerImpl.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerImpl.scala @@ -1,22 +1,29 @@ package org.hyperledger.identus.pollux.credentialschema.controller +import cats.implicits.* import org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, PublicationState} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.api.http.* import org.hyperledger.identus.api.http.model.{CollectionStats, Order, Pagination} import org.hyperledger.identus.castor.core.model.did.{LongFormPrismDID, PrismDID} +import org.hyperledger.identus.pollux.core.model import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema.FilteredEntries +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.service.CredentialSchemaService import org.hyperledger.identus.pollux.credentialschema.http.{ + CredentialSchemaDidUrlResponse, + CredentialSchemaDidUrlResponsePage, + CredentialSchemaInnerDidUrlResponse, CredentialSchemaInput, CredentialSchemaResponse, CredentialSchemaResponsePage, FilterInput } import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaInput.toDomain -import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaResponse.fromDomain +import org.hyperledger.identus.pollux.PrismEnvelopeResponse import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* +import zio.json.* import zio.json.ast.Json import java.util.UUID @@ -24,41 +31,89 @@ import scala.language.implicitConversions class CredentialSchemaControllerImpl(service: CredentialSchemaService, managedDIDService: ManagedDIDService) extends CredentialSchemaController { + + private def parsingCredentialSchemaError(e: String) = ErrorResponse + .internalServerError(detail = Some(s"Error occurred while parsing the credential schema response: $e")) + override def createSchema( in: CredentialSchemaInput )(implicit rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] = { for { - validated <- validatePrismDID(in.author) + _ <- validatePrismDID(in.author) result <- service .create(toDomain(in)) - .map(cs => fromDomain(cs).withBaseUri(rc.request.uri)) + .map(cs => CredentialSchemaResponse.fromDomain(cs).withBaseUri(rc.request.uri)) } yield result } - override def updateSchema(author: String, id: UUID, in: CredentialSchemaInput)(implicit + def createSchemaDidUrl(baseUrlServiceName: String, in: CredentialSchemaInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PrismEnvelopeResponse] = { + val res = for { + validated <- validatePrismDID(in.author) + result <- service.create(toDomain(in), ResourceResolutionMethod.did) + response <- ZIO + .fromEither(CredentialSchemaDidUrlResponse.asPrismEnvelopeResponse(result, baseUrlServiceName)) + .mapError(parsingCredentialSchemaError) + + } yield response + + res + } + + override def updateSchema(id: UUID, in: CredentialSchemaInput)(implicit rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] = { for { _ <- validatePrismDID(in.author) result <- service - .update(id, toDomain(in).copy(author = author)) - .map(cs => fromDomain(cs).withBaseUri(rc.request.uri)) + .update(id, toDomain(in)) + .map(cs => CredentialSchemaResponse.fromDomain(cs).withBaseUri(rc.request.uri)) } yield result } + override def updateSchemaDidUrl(baseUrlServiceName: String, id: UUID, in: CredentialSchemaInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PrismEnvelopeResponse] = { + val res = for { + _ <- validatePrismDID(in.author) + cs <- service + .update(id, toDomain(in), ResourceResolutionMethod.did) + result <- ZIO + .fromEither(CredentialSchemaDidUrlResponse.asPrismEnvelopeResponse(cs, baseUrlServiceName)) + .mapError(parsingCredentialSchemaError) + } yield result + + res + } + override def getSchemaByGuid(guid: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, CredentialSchemaResponse] = { service .getByGUID(guid) .map( - fromDomain(_) + CredentialSchemaResponse + .fromDomain(_) .withSelf(rc.request.uri.toString) ) } + override def getSchemaByGuidDidUrl(baseUrlServiceName: String, guid: UUID)(implicit + rc: RequestContext + ): IO[ErrorResponse, PrismEnvelopeResponse] = { + val res: IO[ErrorResponse, PrismEnvelopeResponse] = for { + cs <- service.getByGUID(guid, ResourceResolutionMethod.did) + response <- ZIO + .fromEither(CredentialSchemaDidUrlResponse.asPrismEnvelopeResponse(cs, baseUrlServiceName)) + .mapError(parsingCredentialSchemaError) + } yield response + + res + } + override def getSchemaJsonByGuid(guid: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, Json] = { @@ -69,15 +124,22 @@ class CredentialSchemaControllerImpl(service: CredentialSchemaService, managedDI ) } - override def delete(guid: UUID)(implicit + override def getSchemaJsonByGuidDidUrl(baseUrlServiceName: String, id: UUID)(implicit rc: RequestContext - ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] = { - service - .delete(guid) - .map( - fromDomain(_) - .withBaseUri(rc.request.uri) - ) + ): IO[ErrorResponse, PrismEnvelopeResponse] = { + val res = for { + cs <- service.getByGUID(id, ResourceResolutionMethod.did) + authorDid <- ZIO + .fromEither(PrismDID.fromString(cs.author)) + .mapError(_ => ErrorResponse.internalServerError(detail = Some("Invalid schema author DID"))) + response <- ZIO + .fromEither( + CredentialSchemaInnerDidUrlResponse.asPrismEnvelopeResponse(cs.schema, authorDid, cs.id, baseUrlServiceName) + ) + .mapError(parsingCredentialSchemaError) + } yield response + + res } override def lookupSchemas( @@ -89,16 +151,44 @@ class CredentialSchemaControllerImpl(service: CredentialSchemaService, managedDI ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponsePage] = { for { filteredEntries: FilteredEntries <- service.lookup( - filter.toDomain, + filter.toDomain(), pagination.offset, pagination.limit ) entries = filteredEntries.entries - .map(fromDomain(_).withBaseUri(rc.request.uri)) + .map(CredentialSchemaResponse.fromDomain(_).withBaseUri(rc.request.uri)) .toList page = CredentialSchemaResponsePage(entries) stats = CollectionStats(filteredEntries.totalCount, filteredEntries.count) - } yield CredentialSchemaControllerLogic(rc, pagination, page, stats).result + } yield CredentialSchemaControllerLogic(rc, pagination, stats).result(page) + } + + override def lookupSchemasDidUrl( + baseUrlServiceName: String, + filter: FilterInput, + pagination: Pagination, + order: Option[Order], + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaDidUrlResponsePage] = { + for { + filteredEntries: FilteredEntries <- service.lookup( + filter.toDomain(ResourceResolutionMethod.did), + pagination.offset, + pagination.limit + ) + entriesZio = filteredEntries.entries + .traverse(cs => CredentialSchemaDidUrlResponse.asPrismEnvelopeResponse(cs, baseUrlServiceName)) + + entries <- ZIO + .fromEither(entriesZio) + .mapError(e => + ErrorResponse.internalServerError(detail = Some(s"Error occurred while parsing a schema response: $e")) + ) + + page = CredentialSchemaDidUrlResponsePage(entries) + stats = CollectionStats(filteredEntries.totalCount, filteredEntries.count) + } yield CredentialSchemaControllerLogic(rc, pagination, stats).resultDidUrl(page) } private def validatePrismDID(author: String) = diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerLogic.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerLogic.scala index b4afaf255e..f80e256ff9 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerLogic.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerLogic.scala @@ -3,27 +3,24 @@ package org.hyperledger.identus.pollux.credentialschema.controller import org.hyperledger.identus.api.http.model.{CollectionStats, Pagination} import org.hyperledger.identus.api.http.RequestContext import org.hyperledger.identus.api.util.PaginationUtils -import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaResponsePage +import org.hyperledger.identus.pollux.credentialschema.http.{ + CredentialSchemaDidUrlResponsePage, + CredentialSchemaResponsePage +} import sttp.model.Uri case class CredentialSchemaControllerLogic( ctx: RequestContext, pagination: Pagination, - page: CredentialSchemaResponsePage, stats: CollectionStats ) { - private def composeNextUri(uri: Uri): Option[Uri] = - PaginationUtils.composeNextUri(uri, page.contents, pagination, stats) - - private def composePreviousUri(uri: Uri): Option[Uri] = - PaginationUtils.composePreviousUri(uri, page.contents, pagination, stats) + val self = ctx.request.uri.toString + val pageOf = ctx.request.uri.copy(querySegments = Seq.empty).toString - def result: CredentialSchemaResponsePage = { - val self = ctx.request.uri.toString - val pageOf = ctx.request.uri.copy(querySegments = Seq.empty).toString - val next = composeNextUri(ctx.request.uri).map(_.toString) - val previous = composePreviousUri(ctx.request.uri).map(_.toString) + def result(page: CredentialSchemaResponsePage): CredentialSchemaResponsePage = { + val next = PaginationUtils.composeNextUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + val previous = PaginationUtils.composePreviousUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) val pageResult = page.copy( self = self, @@ -39,4 +36,19 @@ case class CredentialSchemaControllerLogic( pageResult } + + def resultDidUrl(page: CredentialSchemaDidUrlResponsePage): CredentialSchemaDidUrlResponsePage = { + val next = PaginationUtils.composeNextUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + val previous = PaginationUtils.composePreviousUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + + val pageResult = page.copy( + self = self, + pageOf = pageOf, + next = next, + previous = previous, + contents = page.contents + ) + + pageResult + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponse.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponse.scala new file mode 100644 index 0000000000..a269e85872 --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponse.scala @@ -0,0 +1,68 @@ +package org.hyperledger.identus.pollux.credentialschema.http + +import org.hyperledger.identus.castor.core.model.did.{DIDUrl, PrismDID} +import org.hyperledger.identus.pollux.core.model +import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema +import org.hyperledger.identus.pollux.PrismEnvelopeResponse +import org.hyperledger.identus.shared.crypto.Sha256Hash +import org.hyperledger.identus.shared.json.Json as JsonUtils +import org.hyperledger.identus.shared.utils.Base64Utils +import zio.json.* +import zio.json.ast.Json + +import java.util.UUID +import scala.collection.immutable.ListMap + +object CredentialSchemaDidUrlResponse { + + def asPrismEnvelopeResponse(cs: CredentialSchema, serviceName: String): Either[String, PrismEnvelopeResponse] = { + + for { + authorDid <- PrismDID.fromString(cs.author) + canonicalized <- JsonUtils.canonicalizeToJcs(cs.toJson).left.map(_.toString) + encoded = Base64Utils.encodeURL(canonicalized.getBytes) + hash = Sha256Hash.compute(encoded.getBytes).hexEncoded + didUrl = DIDUrl( + authorDid.did, + Seq(), + ListMap( + "resourceService" -> Seq(serviceName), + "resourcePath" -> Seq(s"schema-registry/schemas/did-url/${cs.guid}?resourceHash=$hash"), + ), + None + ).toString + } yield PrismEnvelopeResponse( + resource = encoded, + url = didUrl + ) + } + +} + +object CredentialSchemaInnerDidUrlResponse { + + def asPrismEnvelopeResponse( + innerSchema: Json, + authorDid: PrismDID, + schemaGuid: UUID, + serviceName: String + ): Either[String, PrismEnvelopeResponse] = { + for { + canonicalized <- JsonUtils.canonicalizeToJcs(innerSchema.toJson).left.map(_.toString) + encoded = Base64Utils.encodeURL(canonicalized.getBytes) + hash = Sha256Hash.compute(encoded.getBytes).hexEncoded + didUrl = DIDUrl( + authorDid.did, + Seq(), + ListMap( + "resourceService" -> Seq(serviceName), + "resourcePath" -> Seq(s"schema-registry/schemas/did-url/$schemaGuid/schema?resourceHash=$hash"), + ), + None + ).toString + } yield PrismEnvelopeResponse( + resource = encoded, + url = didUrl + ) + } +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponsePage.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponsePage.scala new file mode 100644 index 0000000000..e18de3c60c --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponsePage.scala @@ -0,0 +1,91 @@ +package org.hyperledger.identus.pollux.credentialschema.http + +import org.hyperledger.identus.api.http.Annotation +import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaDidUrlResponsePage.annotations +import org.hyperledger.identus.pollux.PrismEnvelopeResponse +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{description, encodedExample} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +case class CredentialSchemaDidUrlResponsePage( + @description(annotations.contents.description) + @encodedExample(annotations.contents.example) + contents: Seq[PrismEnvelopeResponse], + @description(annotations.kind.description) + @encodedExample(annotations.kind.example) + kind: String = "CredentialSchemaDidUrlPage", + @description(annotations.self.description) + @encodedExample(annotations.self.example) + self: String = "", + @description(annotations.pageOf.description) + @encodedExample(annotations.pageOf.example) + pageOf: String = "", + @description(annotations.next.description) + @encodedExample(annotations.next.example) + next: Option[String] = None, + @description(annotations.previous.description) + @encodedExample(annotations.previous.example) + previous: Option[String] = None +) { + def withSelf(self: String) = copy(self = self) +} + +object CredentialSchemaDidUrlResponsePage { + given encoder: JsonEncoder[CredentialSchemaDidUrlResponsePage] = + DeriveJsonEncoder.gen[CredentialSchemaDidUrlResponsePage] + given decoder: JsonDecoder[CredentialSchemaDidUrlResponsePage] = + DeriveJsonDecoder.gen[CredentialSchemaDidUrlResponsePage] + given schema: Schema[CredentialSchemaDidUrlResponsePage] = Schema.derived + + val Example = CredentialSchemaDidUrlResponsePage( + contents = annotations.contents.example, + kind = annotations.kind.example, + self = annotations.self.example, + pageOf = annotations.pageOf.example, + next = Some(annotations.next.example), + previous = Some(annotations.previous.example) + ) + + object annotations { + + object contents + extends Annotation[Seq[PrismEnvelopeResponse]]( + description = + "A sequence of PrismEnvelopeResponse objects representing the list of credential schemas wrapped in an envelope", + example = Seq.empty + ) + + object kind + extends Annotation[String]( + description = + "A string field indicating the type of the API response. In this case, it will always be set to `CredentialSchemaPage`", + example = "CredentialSchemaPage" + ) // TODO Tech Debt ticket - the kind in a collection should be collection, not the underlying record type + + object self + extends Annotation[String]( + description = "A string field containing the URL of the current API endpoint", + example = "/cloud-agent/schema-registry/schemas/did-url?skip=10&limit=10" + ) + + object pageOf + extends Annotation[String]( + description = "A string field indicating the type of resource that the contents field contains", + example = "/cloud-agent/schema-registry/schemas/did-url" + ) + + object next + extends Annotation[String]( + description = "An optional string field containing the URL of the next page of results. " + + "If the API response does not contain any more pages, this field should be set to None.", + example = "/cloud-agent/schema-registry/schemas/did-url?skip=20&limit=10" + ) + + object previous + extends Annotation[String]( + description = "An optional string field containing the URL of the previous page of results. " + + "If the API response is the first page of results, this field should be set to None.", + example = "/cloud-agent/schema-registry/schemas/did-url?skip=0&limit=10" + ) + } +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponse.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponse.scala index ccb7e5f3cd..0d21684ddd 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponse.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponse.scala @@ -3,9 +3,10 @@ package org.hyperledger.identus.pollux.credentialschema.http import org.hyperledger.identus.api.http.* import org.hyperledger.identus.pollux.core.model import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod.* import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaResponse.annotations import sttp.model.Uri -import sttp.model.Uri.* import sttp.tapir.json.zio.schemaForZioJsonValue import sttp.tapir.Schema import sttp.tapir.Schema.annotations.{default, description, encodedExample, encodedName} @@ -52,6 +53,9 @@ case class CredentialSchemaResponse( @description(annotations.proof.description) @encodedExample(annotations.proof.example.toJson) proof: Option[Proof], + @description(annotations.resolutionMethod.description) + @encodedExample(annotations.resolutionMethod.example) + resolutionMethod: ResourceResolutionMethod, @description(annotations.kind.description) @encodedExample(annotations.kind.example) kind: String = "CredentialSchema", @@ -78,15 +82,15 @@ object CredentialSchemaResponse { schema = cs.schema, author = cs.author, authored = cs.authored, + resolutionMethod = cs.resolutionMethod, proof = None ) - given scala.Conversion[CredentialSchema, CredentialSchemaResponse] = fromDomain - given encoder: zio.json.JsonEncoder[CredentialSchemaResponse] = DeriveJsonEncoder.gen[CredentialSchemaResponse] given decoder: zio.json.JsonDecoder[CredentialSchemaResponse] = DeriveJsonDecoder.gen[CredentialSchemaResponse] + given schema: Schema[CredentialSchemaResponse] = Schema.derived object annotations { @@ -134,6 +138,13 @@ object CredentialSchemaResponse { description = "A string that identifies the type of resource being returned in the response.", example = "CredentialSchema" ) + + object resolutionMethod + extends Annotation[String]( + description = s"The method used to resolve the schema. It can be either HTTP or DID.", + example = ResourceResolutionMethod.http.toString + ) + object proof extends Annotation[Proof]( description = "A digital signature over the Credential Schema for the sake of asserting authorship. " + diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponsePage.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponsePage.scala index ac2f0850ea..b3f8174c69 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponsePage.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponsePage.scala @@ -2,9 +2,10 @@ package org.hyperledger.identus.pollux.credentialschema.http import org.hyperledger.identus.api.http.Annotation import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaResponsePage.annotations +import sttp.tapir.generic.auto.* import sttp.tapir.Schema import sttp.tapir.Schema.annotations.{description, encodedExample} -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} +import zio.json.* case class CredentialSchemaResponsePage( @description(annotations.contents.description) diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/FilterInput.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/FilterInput.scala index 48ad4f542b..5f7500ec20 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/FilterInput.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/FilterInput.scala @@ -3,9 +3,10 @@ package org.hyperledger.identus.pollux.credentialschema.http import org.hyperledger.identus.api.http.* import org.hyperledger.identus.pollux.core.model import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod.* import org.hyperledger.identus.pollux.credentialschema.http.FilterInput.annotations import sttp.tapir.EndpointIO.annotations.{example, query} -import sttp.tapir.Validator.* case class FilterInput( @query @@ -21,7 +22,8 @@ case class FilterInput( @example(annotations.tags.example.headOption) tags: Option[String] = Option.empty[String] ) { - def toDomain = CredentialSchema.Filter(author, name, version, tags) + def toDomain(resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http) = + CredentialSchema.Filter(author, name, version, tags, resolutionMethod) } object FilterInput { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/prex/http/PresentationExchangeTapirSchemas.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/prex/http/PresentationExchangeTapirSchemas.scala index 91b129f5f2..e42fe2dcf9 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/prex/http/PresentationExchangeTapirSchemas.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/prex/http/PresentationExchangeTapirSchemas.scala @@ -1,9 +1,7 @@ package org.hyperledger.identus.pollux.prex.http import org.hyperledger.identus.pollux.prex.* -import sttp.tapir.json.zio.* import sttp.tapir.Schema -import zio.json.ast.Json import scala.language.implicitConversions @@ -20,5 +18,5 @@ object PresentationExchangeTapirSchemas { given Schema[Ldp] = Schema.derived given Schema[Field] = Schema.derived given Schema[JsonPathValue] = Schema.schemaForString.map[JsonPathValue](Some(_))(_.value) - given Schema[FieldFilter] = Schema.derived[Json].map[FieldFilter](Some(_))(_.asJsonZio) + given Schema[FieldFilter] = Schema.any[FieldFilter] } diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala index 05b217c71e..2ef8229f7b 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala @@ -1,5 +1,7 @@ package org.hyperledger.identus.api.util +import com.typesafe.config.ConfigFactory +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.http.DocModels import org.hyperledger.identus.agent.server.AgentHttpServer import org.hyperledger.identus.castor.controller.{DIDController, DIDRegistrarController} @@ -23,6 +25,7 @@ import org.hyperledger.identus.verification.controller.VcVerificationController import org.scalatestplus.mockito.MockitoSugar.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter import zio.{Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer} +import zio.config.typesafe.TypesafeConfigProvider import java.nio.charset.StandardCharsets import java.nio.file.{Files, Path} @@ -42,6 +45,11 @@ object Tapir2StaticOAS extends ZIOAppDefault { val path = Path.of(args.head) Using(Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { writer => writer.write(yaml) } } + val configLayer = ZLayer.fromZIO( + TypesafeConfigProvider + .fromTypesafeConfig(ConfigFactory.load()) + .load(AppConfig.config) + ) effect.provideSomeLayer( ZLayer.succeed(mock[ConnectionController]) ++ ZLayer.succeed(mock[CredentialDefinitionController]) ++ @@ -60,7 +68,8 @@ object Tapir2StaticOAS extends ZIOAppDefault { ZLayer.succeed(mock[EventController]) ++ ZLayer.succeed(mock[CredentialIssuerController]) ++ ZLayer.succeed(mock[PresentationExchangeController]) ++ - ZLayer.succeed(mock[Oid4vciAuthenticatorFactory]) + ZLayer.succeed(mock[Oid4vciAuthenticatorFactory]) ++ + configLayer ) } diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerImplSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerImplSpec.scala index 422928756c..7d37bbb9c3 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerImplSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerImplSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.issue.controller +import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, ManagedDIDState, PublicationState} import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} import org.hyperledger.identus.api.http.ErrorResponse @@ -21,7 +22,9 @@ import org.hyperledger.identus.mercury.protocol.connection.ConnectionResponse import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation import org.hyperledger.identus.pollux.core.model.{CredentialFormat, DidCommID, IssueCredentialRecord} import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.{ProtocolState, Role} -import org.hyperledger.identus.pollux.core.service.MockCredentialService +import org.hyperledger.identus.pollux.core.repository.CredentialDefinitionRepositoryInMemory +import org.hyperledger.identus.pollux.core.service.{CredentialDefinitionServiceImpl, MockCredentialService} +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.shared.models.{KeyId, WalletId} import sttp.client3.{basicRequest, DeserializationException, UriContext} import sttp.client3.ziojson.* @@ -53,9 +56,8 @@ object IssueControllerImplSpec extends ZIOSpecDefault with IssueControllerTestTo credentialFormat = Some("JWT"), claims = json.toJsonAST.toOption.get, automaticIssuance = Some(true), - issuingDID = Some( - "did:prism:332518729a7b7805f73a788e0944802527911901d9b7c16152281be9bc62d944:CosBCogBEkkKFW15LWtleS1hdXRoZW50aWNhdGlvbhAESi4KCXNlY3AyNTZrMRIhAuYoRIefsLhkvYwHz8gDtkG2b0kaZTDOLj_SExWX1fOXEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQLOzab8f0ibt1P0zdMfoWDQTSlPc8_tkV9Jk5BBsXB8fA" - ), + issuingDID = + "did:prism:332518729a7b7805f73a788e0944802527911901d9b7c16152281be9bc62d944:CosBCogBEkkKFW15LWtleS1hdXRoZW50aWNhdGlvbhAESi4KCXNlY3AyNTZrMRIhAuYoRIefsLhkvYwHz8gDtkG2b0kaZTDOLj_SExWX1fOXEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQLOzab8f0ibt1P0zdMfoWDQTSlPc8_tkV9Jk5BBsXB8fA", issuingKid = Some(KeyId("some_kid_id")), connectionId = Some(UUID.fromString("123e4567-e89b-12d3-a456-426614174000")) ) @@ -177,11 +179,18 @@ object IssueControllerImplSpec extends ZIOSpecDefault with IssueControllerTestTo ) ) ) + + private val credentialDefinitionServiceLayer = + CredentialDefinitionRepositoryInMemory.layer + >+> GenericSecretStorageInMemory.layer >+> + ResourceUrlResolver.layer >>> CredentialDefinitionServiceImpl.layer + val baseLayer = MockManagedDIDService.empty >+> MockDIDService.empty >+> MockCredentialService.empty >+> MockConnectionService.empty + >+> credentialDefinitionServiceLayer def spec = (httpErrorResponses @@ migrate( schema = "public", diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala index bcc8e989ab..4cb227e8ff 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala @@ -61,7 +61,7 @@ trait IssueControllerTestTools extends PostgresTestContainerSupport { lazy val testEnvironmentLayer = ZLayer.makeSome[ - ManagedDIDService & DIDService & CredentialService & ConnectionService, + ManagedDIDService & DIDService & CredentialService & CredentialDefinitionService & ConnectionService, IssueController & AppConfig & PostgreSQLContainer & AuthenticatorWithAuthZ[BaseEntity] ](IssueControllerImpl.layer, configLayer, pgContainerLayer, DefaultEntityAuthenticator.layer) @@ -69,9 +69,9 @@ trait IssueControllerTestTools extends PostgresTestContainerSupport { def bootstrapOptions[F[_]](monadError: MonadError[F]): CustomiseInterceptors[F, Any] = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend(controller: IssueController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala index ea80db437c..fb4ce6f7ab 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala @@ -12,15 +12,15 @@ import org.hyperledger.identus.oid4vci.storage.InMemoryIssuanceSessionService import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialConfiguration import org.hyperledger.identus.pollux.core.model.CredentialFormat import org.hyperledger.identus.pollux.core.repository.{ - CredentialRepository, CredentialRepositoryInMemory, CredentialStatusListRepositoryInMemory } import org.hyperledger.identus.pollux.core.service.* +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.PrismDidResolver +import org.hyperledger.identus.shared.messaging.{MessagingService, MessagingServiceConfig, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.{Clock, Random, URLayer, ZIO, ZLayer} -import zio.json.* import zio.json.ast.Json import zio.mock.MockSpecDefault import zio.test.* @@ -48,11 +48,13 @@ object OIDCCredentialIssuerServiceSpec CredentialRepositoryInMemory.layer, CredentialStatusListRepositoryInMemory.layer, PrismDidResolver.layer, - ResourceURIDereferencerImpl.layer, + ResourceUrlResolver.layer, credentialDefinitionServiceLayer, GenericSecretStorageInMemory.layer, LinkSecretServiceImpl.layer, CredentialServiceImpl.layer, + (MessagingServiceConfig.inMemoryLayer >>> MessagingService.serviceLayer >>> + MessagingService.producerLayer[UUID, WalletIdAndRecordId]).orDie, OIDCCredentialIssuerServiceImpl.layer ) diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala index 3ffdc9d43a..ae15a7b190 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.credentialdefinition +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, Entity} import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage import org.hyperledger.identus.api.http.ErrorResponse @@ -69,7 +70,8 @@ object CredentialDefinitionBasicSpec extends ZIOSpecDefault with CredentialDefin for { controller <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - } yield httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + } yield httpBackend(config, controller, authenticator) def createCredentialDefinitionResponseZIO = for { backend <- backendZIO diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala index 7eac221f70..735d49012d 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.credentialdefinition +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.agent.walletapi.service.MockManagedDIDService import org.hyperledger.identus.api.http.ErrorResponse @@ -28,7 +29,8 @@ object CredentialDefinitionFailureSpec extends ZIOSpecDefault with CredentialDef for { credentialDefinitionRegistryService <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(credentialDefinitionRegistryService, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, credentialDefinitionRegistryService, authenticator) response: CredentialDefinitionBadRequestResponse <- basicRequest .post(credentialDefinitionUriBase) .body("""{"foo":"bar"}""") diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala index 6da8f500fb..169df11436 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.credentialdefinition +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.container.util.MigrationAspects.migrate import org.hyperledger.identus.iam.authentication.AuthenticatorWithAuthZ @@ -25,13 +26,14 @@ object CredentialDefinitionLookupAndPaginationSpec def fetchAllPages( uri: Uri - ): ZIO[CredentialDefinitionController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + ): ZIO[CredentialDefinitionController & AuthenticatorWithAuthZ[BaseEntity] & AppConfig, Throwable, List[ CredentialDefinitionResponsePage ]] = { for { controller <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) response: CredentialDefinitionResponsePageType <- for { response <- basicRequest @@ -81,8 +83,8 @@ object CredentialDefinitionLookupAndPaginationSpec _ <- deleteAllCredentialDefinitions controller <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) - + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) inputs <- Generator.credentialDefinitionInput.runCollectN(10) _ <- inputs .map(in => diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala index 541a95a3af..55b93f3007 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala @@ -1,6 +1,8 @@ package org.hyperledger.identus.pollux.credentialdefinition import com.dimafeng.testcontainers.PostgreSQLContainer +import com.typesafe.config.ConfigFactory +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.http.CustomServerInterceptors import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, ManagedDIDState, PublicationState} @@ -10,11 +12,8 @@ import org.hyperledger.identus.api.http.ErrorResponse import org.hyperledger.identus.castor.core.model.did.PrismDIDOperation import org.hyperledger.identus.iam.authentication.{AuthenticatorWithAuthZ, DefaultEntityAuthenticator} import org.hyperledger.identus.pollux.core.repository.CredentialDefinitionRepository -import org.hyperledger.identus.pollux.core.service.{ - CredentialDefinitionService, - CredentialDefinitionServiceImpl, - ResourceURIDereferencerImpl -} +import org.hyperledger.identus.pollux.core.service.{CredentialDefinitionService, CredentialDefinitionServiceImpl} +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.credentialdefinition.controller.{ CredentialDefinitionController, CredentialDefinitionControllerImpl @@ -35,6 +34,7 @@ import sttp.tapir.server.interceptor.CustomiseInterceptors import sttp.tapir.server.stub.TapirStubInterpreter import sttp.tapir.ztapir.RIOMonadError import zio.* +import zio.config.typesafe.TypesafeConfigProvider import zio.json.EncoderOps import zio.mock.Expectation import zio.test.{Assertion, Gen, ZIOSpecDefault} @@ -56,7 +56,7 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { private val controllerLayer = GenericSecretStorageInMemory.layer >+> systemTransactorLayer >+> contextAwareTransactorLayer >+> JdbcCredentialDefinitionRepository.layer >+> - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> CredentialDefinitionServiceImpl.layer >+> CredentialDefinitionControllerImpl.layer @@ -76,43 +76,52 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { val authenticatorLayer: TaskLayer[AuthenticatorWithAuthZ[BaseEntity]] = DefaultEntityAuthenticator.layer + val configLayer = ZLayer.fromZIO( + TypesafeConfigProvider + .fromTypesafeConfig(ConfigFactory.load()) + .load(AppConfig.config) + ) + lazy val testEnvironmentLayer = ZLayer.makeSome[ ManagedDIDService, CredentialDefinitionController & CredentialDefinitionRepository & CredentialDefinitionService & - PostgreSQLContainer & AuthenticatorWithAuthZ[BaseEntity] & GenericSecretStorage + PostgreSQLContainer & AuthenticatorWithAuthZ[BaseEntity] & GenericSecretStorage & AppConfig ]( controllerLayer, pgContainerLayer, - authenticatorLayer + authenticatorLayer, + configLayer ) val credentialDefinitionUriBase = uri"http://test.com/credential-definition-registry/definitions" def bootstrapOptions[F[_]](monadError: MonadError[F]) = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend( + config: AppConfig, controller: CredentialDefinitionController, authenticator: AuthenticatorWithAuthZ[BaseEntity] ) = { + val credentialDefinitionRegistryEndpoints = - CredentialDefinitionRegistryServerEndpoints(controller, authenticator, authenticator) + CredentialDefinitionRegistryServerEndpoints(config, controller, authenticator, authenticator) val backend = TapirStubInterpreter( bootstrapOptions(new RIOMonadError[Any]), SttpBackendStub(new RIOMonadError[Any]) ) - .whenServerEndpoint(credentialDefinitionRegistryEndpoints.createCredentialDefinitionServerEndpoint) + .whenServerEndpoint(credentialDefinitionRegistryEndpoints.create.http) .thenRunLogic() - .whenServerEndpoint(credentialDefinitionRegistryEndpoints.getCredentialDefinitionByIdServerEndpoint) + .whenServerEndpoint(credentialDefinitionRegistryEndpoints.get.http) .thenRunLogic() .whenServerEndpoint( - credentialDefinitionRegistryEndpoints.lookupCredentialDefinitionsByQueryServerEndpoint + credentialDefinitionRegistryEndpoints.getMany.http ) .thenRunLogic() .backend() @@ -188,13 +197,14 @@ trait CredentialDefinitionGen { def generateCredentialDefinitionsN( count: Int - ): ZIO[CredentialDefinitionController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + ): ZIO[CredentialDefinitionController & AppConfig & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ CredentialDefinitionInput ]] = for { controller <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) inputs <- Generator.credentialDefinitionInput.runCollectN(count) _ <- inputs .map(in => diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaAnoncredSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaAnoncredSpec.scala index 29064e587f..a9c5fff060 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaAnoncredSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaAnoncredSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.schema +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.api.http.ErrorResponse import org.hyperledger.identus.container.util.MigrationAspects.* @@ -51,7 +52,9 @@ object CredentialSchemaAnoncredSpec extends ZIOSpecDefault with CredentialSchema + wrapSpec(unsupportedSchemaSpec) + wrapSpec(wrongSchemaSpec) - private def wrapSpec(spec: Spec[CredentialSchemaController & AuthenticatorWithAuthZ[BaseEntity], Throwable]) = { + private def wrapSpec( + spec: Spec[CredentialSchemaController & AppConfig & AuthenticatorWithAuthZ[BaseEntity], Throwable] + ) = { (spec @@ nondeterministic @@ sequential @@ timed @@ migrateEach( schema = "public", @@ -65,7 +68,8 @@ object CredentialSchemaAnoncredSpec extends ZIOSpecDefault with CredentialSchema def getSchemaZIO(uuid: UUID) = for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) response <- basicRequest .get(credentialSchemaUriBase.addPath(uuid.toString)) .response(asJsonAlways[CredentialSchemaResponse]) @@ -142,7 +146,8 @@ object CredentialSchemaAnoncredSpec extends ZIOSpecDefault with CredentialSchema for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) response <- basicRequest .post(credentialSchemaUriBase) diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaBasicSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaBasicSpec.scala index ea7cda5e27..ce72b3c1f2 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaBasicSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaBasicSpec.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.api.http.ErrorResponse @@ -69,7 +70,8 @@ object CredentialSchemaBasicSpec extends ZIOSpecDefault with CredentialSchemaTes for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - } yield httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + } yield httpBackend(config, controller, authenticator) def createSchemaResponseZIO = for { backend <- backendZIO diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaFailureSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaFailureSpec.scala index 4069ee5715..73882dc2d5 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaFailureSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaFailureSpec.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.agent.walletapi.service.MockManagedDIDService import org.hyperledger.identus.api.http.ErrorResponse @@ -27,7 +28,8 @@ object CredentialSchemaFailureSpec extends ZIOSpecDefault with CredentialSchemaT for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) response: SchemaBadRequestResponse <- basicRequest .post(credentialSchemaUriBase) .body("""{"foo":"bar"}""") diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala index a348ae5254..6806cc918b 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.container.util.MigrationAspects.migrate import org.hyperledger.identus.iam.authentication.AuthenticatorWithAuthZ @@ -28,13 +29,14 @@ object CredentialSchemaLookupAndPaginationSpec def fetchAllPages( uri: Uri - ): ZIO[CredentialSchemaController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + ): ZIO[CredentialSchemaController & AppConfig & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ CredentialSchemaResponsePage ]] = { for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) response: SchemaPageResponse <- basicRequest .get(uri) .response(asJsonAlways[CredentialSchemaResponsePage]) @@ -77,7 +79,8 @@ object CredentialSchemaLookupAndPaginationSpec _ <- deleteAllCredentialSchemas controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) inputs <- Generator.schemaInput.runCollectN(101) _ <- inputs diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaMultiTenancySpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaMultiTenancySpec.scala index 8737c4a1a5..c797e8a74a 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaMultiTenancySpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaMultiTenancySpec.scala @@ -3,7 +3,7 @@ package org.hyperledger.identus.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer import org.hyperledger.identus.agent.walletapi.model.Entity import org.hyperledger.identus.container.util.MigrationAspects.* -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaGuidNotFoundError +import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaUpdateError import org.hyperledger.identus.pollux.core.model.schema.`type`.CredentialJsonSchemaType import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.service.{CredentialSchemaService, CredentialSchemaServiceImpl} @@ -106,10 +106,10 @@ object CredentialSchemaMultiTenancySpec extends ZIOSpecDefault with CredentialSc .exit aliceCannotUpdateBobsVCSchema = assert(notFoundSchemaAError)( - fails(isSubtype[CredentialSchemaGuidNotFoundError](anything)) + fails(isSubtype[CredentialSchemaUpdateError](anything)) ) bobCannotUpdateAlicesVCSchema = assert(notFoundSchemaBError)( - fails(isSubtype[CredentialSchemaGuidNotFoundError](anything)) + fails(isSubtype[CredentialSchemaUpdateError](anything)) ) fetchedSchemaAbyB <- service.getByGUID(updatedSchemaA.guid).provideLayer(Bob.wacLayer) diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala index 2c3f148e54..7f72581960 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala @@ -1,6 +1,8 @@ package org.hyperledger.identus.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import com.typesafe.config.ConfigFactory +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.http.CustomServerInterceptors import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, ManagedDIDState, PublicationState} import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} @@ -31,6 +33,7 @@ import sttp.tapir.server.interceptor.CustomiseInterceptors import sttp.tapir.server.stub.TapirStubInterpreter import sttp.tapir.ztapir.RIOMonadError import zio.* +import zio.config.typesafe.TypesafeConfigProvider import zio.json.{DecoderOps, EncoderOps} import zio.json.ast.Json import zio.json.ast.Json.* @@ -65,13 +68,19 @@ trait CredentialSchemaTestTools extends PostgresTestContainerSupport { ) ) + val configLayer = ZLayer.fromZIO( + TypesafeConfigProvider + .fromTypesafeConfig(ConfigFactory.load()) + .load(AppConfig.config) + ) + val authenticatorLayer: TaskLayer[AuthenticatorWithAuthZ[BaseEntity]] = DefaultEntityAuthenticator.layer lazy val testEnvironmentLayer = ZLayer.makeSome[ ManagedDIDService, CredentialSchemaController & CredentialSchemaRepository & CredentialSchemaService & PostgreSQLContainer & - AuthenticatorWithAuthZ[BaseEntity] + AuthenticatorWithAuthZ[BaseEntity] & AppConfig ]( CredentialSchemaControllerImpl.layer, CredentialSchemaServiceImpl.layer, @@ -79,34 +88,39 @@ trait CredentialSchemaTestTools extends PostgresTestContainerSupport { contextAwareTransactorLayer, systemTransactorLayer, pgContainerLayer, - authenticatorLayer + authenticatorLayer, + configLayer ) val credentialSchemaUriBase = uri"http://test.com/schema-registry/schemas" def bootstrapOptions[F[_]](monadError: MonadError[F]) = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } - def httpBackend(controller: CredentialSchemaController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { - val schemaRegistryEndpoints = SchemaRegistryServerEndpoints(controller, authenticator, authenticator) + def httpBackend( + config: AppConfig, + controller: CredentialSchemaController, + authenticator: AuthenticatorWithAuthZ[BaseEntity] + ) = { + val schemaRegistryEndpoints = SchemaRegistryServerEndpoints(config, controller, authenticator, authenticator) val backend = TapirStubInterpreter( bootstrapOptions(new RIOMonadError[Any]), SttpBackendStub(new RIOMonadError[Any]) ) - .whenServerEndpoint(schemaRegistryEndpoints.createSchemaServerEndpoint) + .whenServerEndpoint(schemaRegistryEndpoints.create.http) .thenRunLogic() - .whenServerEndpoint(schemaRegistryEndpoints.getSchemaByIdServerEndpoint) + .whenServerEndpoint(schemaRegistryEndpoints.get.http) .thenRunLogic() - .whenServerEndpoint(schemaRegistryEndpoints.getRawSchemaByIdServerEndpoint) + .whenServerEndpoint(schemaRegistryEndpoints.getRaw.http) .thenRunLogic() .whenServerEndpoint( - schemaRegistryEndpoints.lookupSchemasByQueryServerEndpoint + schemaRegistryEndpoints.getMany.http ) .thenRunLogic() .backend() @@ -179,11 +193,14 @@ trait CredentialSchemaGen { def generateSchemasN( count: Int - ): ZIO[CredentialSchemaController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[CredentialSchemaInput]] = + ): ZIO[CredentialSchemaController & AppConfig & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + CredentialSchemaInput + ]] = for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) inputs <- Generator.schemaInput.runCollectN(count) _ <- inputs .map(in => diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala index 95ed827fec..80f15ce237 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala @@ -41,9 +41,9 @@ trait SystemControllerTestTools { def bootstrapOptions[F[_]](monadError: MonadError[F]): CustomiseInterceptors[F, Any] = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend(controller: SystemController) = { diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala index b2e0fcf012..2e75ce25a2 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala @@ -36,17 +36,15 @@ object VcVerificationControllerImplSpec extends ZIOSpecDefault with VcVerificati `@context` = Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala index f9a5b68968..e4da96b640 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala @@ -7,6 +7,7 @@ import org.hyperledger.identus.castor.core.model.did.VerificationRelationship import org.hyperledger.identus.castor.core.service.MockDIDService import org.hyperledger.identus.iam.authentication.{AuthenticatorWithAuthZ, DefaultEntityAuthenticator} import org.hyperledger.identus.pollux.core.service.* +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.core.service.verification.{VcVerificationService, VcVerificationServiceImpl} import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} @@ -59,7 +60,7 @@ trait VcVerificationControllerTestTools extends PostgresTestContainerSupport { VcVerificationController & VcVerificationService & AuthenticatorWithAuthZ[BaseEntity] ]( didResolverLayer, - ResourceURIDereferencerImpl.layer, + ResourceUrlResolver.layer, VcVerificationControllerImpl.layer, VcVerificationServiceImpl.layer, DefaultEntityAuthenticator.layer @@ -69,9 +70,9 @@ trait VcVerificationControllerTestTools extends PostgresTestContainerSupport { def bootstrapOptions[F[_]](monadError: MonadError[F]): CustomiseInterceptors[F, Any] = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend(controller: VcVerificationController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { diff --git a/cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql b/cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql new file mode 100644 index 0000000000..5a59c4a121 --- /dev/null +++ b/cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql @@ -0,0 +1,19 @@ +-- Last used DID Index per wallet (solving race condition) +CREATE TABLE public.last_did_index_per_wallet +( + "wallet_id" UUID REFERENCES public.wallet ("wallet_id") NOT NULL PRIMARY KEY, + "last_used_index" INT NOT NULL +); + +ALTER TABLE public.last_did_index_per_wallet + ENABLE ROW LEVEL SECURITY; + +CREATE +POLICY last_did_index_per_wallet_wallet_isolation +ON public.last_did_index_per_wallet +USING (wallet_id = current_setting('app.current_wallet_id')::UUID); + +INSERT INTO public.last_did_index_per_wallet(wallet_id, last_used_index) +SELECT wallet_id, MAX(did_index) +FROM public.prism_did_wallet_state +GROUP BY wallet_id; \ No newline at end of file diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala index 935a38244f..3964378fec 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala @@ -17,6 +17,8 @@ trait ManagedDIDService { private[walletapi] def nonSecretStorage: DIDNonSecretStorage + protected def getDefaultDidDocumentServices: Set[Service] = Set.empty + def syncManagedDIDState: ZIO[WalletAccessContext, GetManagedDIDError, Unit] def syncUnconfirmedUpdateOperations: ZIO[WalletAccessContext, GetManagedDIDError, Unit] diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala index 4e8763bdad..37e8543b94 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala @@ -1,13 +1,13 @@ package org.hyperledger.identus.agent.walletapi.service import org.hyperledger.identus.agent.walletapi.model.* -import org.hyperledger.identus.agent.walletapi.model.error.* -import org.hyperledger.identus.agent.walletapi.model.error.given +import org.hyperledger.identus.agent.walletapi.model.error.{*, given} import org.hyperledger.identus.agent.walletapi.service.handler.{DIDCreateHandler, DIDUpdateHandler, PublicationHandler} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService.DEFAULT_MASTER_KEY_ID import org.hyperledger.identus.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage, WalletSecretStorage} import org.hyperledger.identus.agent.walletapi.util.* import org.hyperledger.identus.castor.core.model.did.* +import org.hyperledger.identus.castor.core.model.did.Service as DidDocumentService import org.hyperledger.identus.castor.core.model.error.DIDOperationError import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.castor.core.util.DIDOperationValidator @@ -24,13 +24,13 @@ import scala.language.implicitConversions * indy-wallet-sdk. */ class ManagedDIDServiceImpl private[walletapi] ( + defaultDidDocumentServices: Set[DidDocumentService], didService: DIDService, didOpValidator: DIDOperationValidator, private[walletapi] val secretStorage: DIDSecretStorage, override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, walletSecretStorage: WalletSecretStorage, apollo: Apollo, - createDIDSem: Semaphore ) extends ManagedDIDService { private val AGREEMENT_KEY_ID = KeyId("agreement") @@ -54,6 +54,8 @@ class ManagedDIDServiceImpl private[walletapi] ( def syncUnconfirmedUpdateOperations: ZIO[WalletAccessContext, GetManagedDIDError, Unit] = syncUnconfirmedUpdateOperationsByDID(did = None) + override protected def getDefaultDidDocumentServices: Set[DidDocumentService] = defaultDidDocumentServices + override def findDIDKeyPair( did: CanonicalPrismDID, keyId: KeyId @@ -123,25 +125,23 @@ class ManagedDIDServiceImpl private[walletapi] ( def createAndStoreDID( didTemplate: ManagedDIDTemplate ): ZIO[WalletAccessContext, CreateManagedDIDError, LongFormPrismDID] = { - val effect = for { + for { _ <- ZIO - .fromEither(ManagedDIDTemplateValidator.validate(didTemplate)) - .mapError(CreateManagedDIDError.InvalidArgument.apply) - material <- didCreateHandler.materialize(didTemplate) + .fromEither(ManagedDIDTemplateValidator.validate(didTemplate, defaultDidDocumentServices)) + .mapError { x => + println("x: " + x) + + CreateManagedDIDError.InvalidArgument(x) + } + _ <- ZIO.logInfo(s"Old did template after validation: $didTemplate") + newDidTemplate = didTemplate.copy(services = didTemplate.services ++ defaultDidDocumentServices) + _ <- ZIO.logInfo(s"Creating managed DID with template2: $newDidTemplate") + material <- didCreateHandler.materialize(newDidTemplate) _ <- ZIO .fromEither(didOpValidator.validate(material.operation)) .mapError(CreateManagedDIDError.InvalidOperation.apply) _ <- material.persist.mapError(CreateManagedDIDError.WalletStorageError.apply) } yield PrismDID.buildLongFormFromOperation(material.operation) - - // This synchronizes createDID effect to only allow 1 execution at a time - // to avoid concurrent didIndex update. Long-term solution should be - // solved at the DB level. - // - // Performance may be improved by not synchronizing the whole operation, - // but only the counter increment part allowing multiple in-flight create operations - // once didIndex is acquired. - createDIDSem.withPermit(effect) } def updateManagedDID( @@ -361,26 +361,27 @@ class ManagedDIDServiceImpl private[walletapi] ( object ManagedDIDServiceImpl { val layer: RLayer[ - DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & WalletSecretStorage & Apollo, + Set[DidDocumentService] & DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & + WalletSecretStorage & Apollo, ManagedDIDService ] = { ZLayer.fromZIO { for { + defaultDidDocumentServices <- ZIO.service[Set[DidDocumentService]] didService <- ZIO.service[DIDService] didOpValidator <- ZIO.service[DIDOperationValidator] secretStorage <- ZIO.service[DIDSecretStorage] nonSecretStorage <- ZIO.service[DIDNonSecretStorage] walletSecretStorage <- ZIO.service[WalletSecretStorage] apollo <- ZIO.service[Apollo] - createDIDSem <- Semaphore.make(1) } yield ManagedDIDServiceImpl( + defaultDidDocumentServices, didService, didOpValidator, secretStorage, nonSecretStorage, walletSecretStorage, - apollo, - createDIDSem + apollo ) } } diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala index 782ee6d9ec..02b5142152 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala @@ -3,7 +3,7 @@ package org.hyperledger.identus.agent.walletapi.service import org.hyperledger.identus.agent.walletapi.model.error.CommonWalletStorageError import org.hyperledger.identus.agent.walletapi.model.ManagedDIDDetail import org.hyperledger.identus.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage, WalletSecretStorage} -import org.hyperledger.identus.castor.core.model.did.CanonicalPrismDID +import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, Service as DidDocumentService} import org.hyperledger.identus.castor.core.model.error.DIDOperationError import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.castor.core.util.DIDOperationValidator @@ -13,22 +13,22 @@ import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* class ManagedDIDServiceWithEventNotificationImpl( + defaultDidDocumentServices: Set[DidDocumentService], didService: DIDService, didOpValidator: DIDOperationValidator, override private[walletapi] val secretStorage: DIDSecretStorage, override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, walletSecretStorage: WalletSecretStorage, apollo: Apollo, - createDIDSem: Semaphore, eventNotificationService: EventNotificationService ) extends ManagedDIDServiceImpl( + defaultDidDocumentServices, didService, didOpValidator, secretStorage, nonSecretStorage, walletSecretStorage, - apollo, - createDIDSem + apollo ) { private val didStatusUpdatedEventName = "DIDStatusUpdated" @@ -57,11 +57,12 @@ class ManagedDIDServiceWithEventNotificationImpl( object ManagedDIDServiceWithEventNotificationImpl { val layer: RLayer[ - DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & WalletSecretStorage & Apollo & - EventNotificationService, + Set[DidDocumentService] & DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & + WalletSecretStorage & Apollo & EventNotificationService, ManagedDIDService ] = ZLayer.fromZIO { for { + defaultDidDocumentServices <- ZIO.service[Set[DidDocumentService]] didService <- ZIO.service[DIDService] didOpValidator <- ZIO.service[DIDOperationValidator] secretStorage <- ZIO.service[DIDSecretStorage] @@ -71,13 +72,13 @@ object ManagedDIDServiceWithEventNotificationImpl { createDIDSem <- Semaphore.make(1) eventNotificationService <- ZIO.service[EventNotificationService] } yield ManagedDIDServiceWithEventNotificationImpl( + defaultDidDocumentServices, didService, didOpValidator, secretStorage, nonSecretStorage, walletSecretStorage, apollo, - createDIDSem, eventNotificationService ) } diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala index 66fec256bb..d87ef1c91d 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala @@ -31,12 +31,7 @@ private[walletapi] class DIDCreateHandler( walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) seed <- walletSecretStorage.findWalletSeed .someOrElseZIO(ZIO.dieMessage(s"Wallet seed for wallet $walletId does not exist")) - didIndex <- nonSecretStorage - .getMaxDIDIndex() - .mapBoth( - CreateManagedDIDError.WalletStorageError.apply, - maybeIdx => maybeIdx.map(_ + 1).getOrElse(0) - ) + didIndex <- nonSecretStorage.incrementAndGetNextDIDIndex generated <- operationFactory.makeCreateOperation(masterKeyId, seed.toByteArray)(didIndex, didTemplate) (createOperation, keys) = generated state = ManagedDIDState(createOperation, didIndex, PublicationState.Created()) diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala index bfdca44f73..7f86258e80 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala @@ -1,27 +1,21 @@ package org.hyperledger.identus.agent.walletapi.sql +import cats.implicits.toFunctorOps import doobie.* import doobie.implicits.* import doobie.postgres.implicits.* import org.hyperledger.identus.agent.walletapi.model.* import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage -import org.hyperledger.identus.castor.core.model.did.{ - EllipticCurve, - InternalKeyPurpose, - PrismDID, - ScheduledDIDOperationStatus, - VerificationRelationship -} +import org.hyperledger.identus.castor.core.model.did.* import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.shared.db.ContextAwareTask -import org.hyperledger.identus.shared.db.Implicits.* -import org.hyperledger.identus.shared.db.Implicits.given +import org.hyperledger.identus.shared.db.Implicits.{*, given} import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext, WalletId} import zio.* import zio.interop.catz.* import java.time.Instant -import scala.collection.immutable.ArraySeq +import java.util.Objects class JdbcDIDNonSecretStorage(xa: Transactor[ContextAwareTask], xb: Transactor[Task]) extends DIDNonSecretStorage { @@ -109,11 +103,11 @@ class JdbcDIDNonSecretStorage(xa: Transactor[ContextAwareTask], xb: Transactor[T _ <- insertHdKeyIO.updateMany(randKeyValues(now)) } yield () - for { + (for { walletCtx <- ZIO.service[WalletAccessContext] now <- Clock.instant _ <- txnIO(now, walletCtx.walletId).transactWallet(xa) - } yield () + } yield ()).orDie } override def updateManagedDID(did: PrismDID, patch: ManagedDIDStatePatch): RIO[WalletAccessContext, Unit] = { @@ -151,6 +145,41 @@ class JdbcDIDNonSecretStorage(xa: Transactor[ContextAwareTask], xb: Transactor[T cxnIO.transactWallet(xa).map(_.flatten) } + override def incrementAndGetNextDIDIndex: URIO[WalletAccessContext, Int] = { + def acquireAdvisoryLock(walletId: WalletId): ConnectionIO[Unit] = { + // Should be specific to this process + val PROCESS_UNIQUE_ID = 465263 + val hashCode = Objects.hash(walletId.hashCode(), PROCESS_UNIQUE_ID) + sql"SELECT pg_advisory_xact_lock($hashCode)".query[Unit].unique.void + } + + def insertWalletDIDIndexIfNotExists(walletId: WalletId): ConnectionIO[Int] = { + sql""" + | INSERT INTO public.last_did_index_per_wallet (wallet_id, last_used_index) + | VALUES ($walletId, -1) + | ON CONFLICT (wallet_id) DO NOTHING""".stripMargin.update.run + } + + def incrementWalletDIDIndex(walletId: WalletId): ConnectionIO[Int] = { + sql""" + | UPDATE public.last_did_index_per_wallet + | SET last_used_index = last_used_index + 1 + | WHERE wallet_id = $walletId + | RETURNING last_used_index""".stripMargin.query[Int].unique + } + + for { + walletCtx <- ZIO.service[WalletAccessContext] + walletId = walletCtx.walletId + cnxIO = for { + _ <- acquireAdvisoryLock(walletId) + _ <- insertWalletDIDIndexIfNotExists(walletId) + index <- incrementWalletDIDIndex(walletId) + } yield index + index <- cnxIO.transactWallet(xa).orDie + } yield index + } + override def getHdKeyCounter(did: PrismDID): RIO[WalletAccessContext, Option[HdKeyIndexCounter]] = { val status: ScheduledDIDOperationStatus = ScheduledDIDOperationStatus.Confirmed val cxnIO = diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala index 1830dc1600..612338b1ad 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala @@ -21,6 +21,8 @@ trait DIDNonSecretStorage { def getMaxDIDIndex(): RIO[WalletAccessContext, Option[Int]] + def incrementAndGetNextDIDIndex: URIO[WalletAccessContext, Int] + def getHdKeyCounter(did: PrismDID): RIO[WalletAccessContext, Option[HdKeyIndexCounter]] /** Return a tuple of key metadata and the operation hash */ diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala index 82abee2b7f..dad26a4b10 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala @@ -2,16 +2,35 @@ package org.hyperledger.identus.agent.walletapi.util import org.hyperledger.identus.agent.walletapi.model.ManagedDIDTemplate import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService -import org.hyperledger.identus.castor.core.model.did.{EllipticCurve, VerificationRelationship} +import org.hyperledger.identus.castor.core.model.did.{ + EllipticCurve, + Service as DidDocumentService, + VerificationRelationship +} object ManagedDIDTemplateValidator { - def validate(template: ManagedDIDTemplate): Either[String, Unit] = + def validate( + template: ManagedDIDTemplate, + defaultDidDocumentServices: Set[DidDocumentService] = Set.empty + ): Either[String, Unit] = for { _ <- validateReservedKeyId(template) _ <- validateCurveUsage(template) + _ <- validatePresenceOfDefaultDidServices(template.services, defaultDidDocumentServices) } yield () + private def validatePresenceOfDefaultDidServices( + services: Seq[DidDocumentService], + defaultDidDocumentServices: Set[DidDocumentService] + ): Either[String, Unit] = { + + services.map(_.id).intersect(defaultDidDocumentServices.toSeq.map(_.id)) match { + case Nil => Right(()) + case x => Left(s"Default DID services cannot be overridden: ${x.mkString("[", ", ", "]")}") + } + } + private def validateReservedKeyId(template: ManagedDIDTemplate): Either[String, Unit] = { val keyIds = template.publicKeys.map(_.id) val reservedKeyIds = keyIds.filter(id => ManagedDIDService.reservedKeyIds.contains(id)) diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala index 7950a38265..c32600a08a 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala @@ -11,6 +11,11 @@ import org.hyperledger.identus.agent.walletapi.sql.* import org.hyperledger.identus.agent.walletapi.storage.* import org.hyperledger.identus.agent.walletapi.vault.{VaultDIDSecretStorage, VaultWalletSecretStorage} import org.hyperledger.identus.castor.core.model.did.* +import org.hyperledger.identus.castor.core.model.did.{ + Service as DidDocumentService, + ServiceEndpoint as DidDocumentServiceEndpoint, + ServiceType as DidDocumentServiceType +} import org.hyperledger.identus.castor.core.model.error import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.castor.core.util.DIDOperationValidator @@ -82,21 +87,38 @@ object ManagedDIDServiceSpec ) private def serviceLayer = - ZLayer - .makeSome[ - DIDSecretStorage & WalletSecretStorage, - WalletManagementService & ManagedDIDService & TestDIDService - ]( - ManagedDIDServiceImpl.layer, - WalletManagementServiceImpl.layer, - DIDOperationValidator.layer(), - JdbcDIDNonSecretStorage.layer, - JdbcWalletNonSecretStorage.layer, - systemTransactorLayer, - contextAwareTransactorLayer, - testDIDServiceLayer, - apolloLayer - ) + ZLayer.succeed(Set(defaultDidDocumentServiceFixture)) >>> serviceLayerWithoutDidDocumentServices + + private def serviceLayerWithoutDidDocumentServices = ZLayer + .makeSome[ + Set[DidDocumentService] & DIDSecretStorage & WalletSecretStorage, + WalletManagementService & ManagedDIDService & TestDIDService + ]( + ManagedDIDServiceImpl.layer, + WalletManagementServiceImpl.layer, + DIDOperationValidator.layer(), + JdbcDIDNonSecretStorage.layer, + JdbcWalletNonSecretStorage.layer, + systemTransactorLayer, + contextAwareTransactorLayer, + testDIDServiceLayer, + apolloLayer + ) + + private val defaultDidDocumentServiceFixture = DidDocumentService( + id = "agent-base-url", + serviceEndpoint = DidDocumentServiceEndpoint + .Single( + DidDocumentServiceEndpoint.UriOrJsonEndpoint + .Uri( + DidDocumentServiceEndpoint.UriValue + .fromString("http://localhost:8085/") + .toOption + .get // This will fail if URL is invalid, which will prevent app from starting since public endpoint in config is invalid + ) + ), + `type` = DidDocumentServiceType.Single(DidDocumentServiceType.Name.fromStringUnsafe("LinkedResourceV1")) + ) private def generateDIDTemplate( publicKeys: Seq[DIDPublicKeyTemplate] = Nil, @@ -234,6 +256,15 @@ object ManagedDIDServiceSpec } yield assert(didsBefore)(isEmpty) && assert(didsAfter.map(_._1))(hasSameElements(Seq(did))) }, + test("will not create a DID if one of the provided servies includes default service") { + val template = generateDIDTemplate( + services = Seq( + defaultDidDocumentServiceFixture + ) + ) + val result = ZIO.serviceWithZIO[ManagedDIDService](_.createAndStoreDID(template)) + assertZIO(result.exit)(fails(isSubtype[CreateManagedDIDError.InvalidArgument](anything))) + }, test("create and store DID secret in DIDSecretStorage") { val template = generateDIDTemplate( publicKeys = Seq( diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala index f14df4d00d..6a29b2769d 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala @@ -49,6 +49,9 @@ case class MockDIDNonSecretStorage(proxy: Proxy) extends DIDNonSecretStorage { override def getMaxDIDIndex(): RIO[WalletAccessContext, Option[Int]] = proxy(MockDIDNonSecretStorage.GetMaxDIDIndex) + override def incrementAndGetNextDIDIndex: URIO[WalletAccessContext, RuntimeFlags] = + proxy(MockDIDNonSecretStorage.IncrementAndGetNextDIDIndex) + override def getHdKeyCounter(did: PrismDID): RIO[WalletAccessContext, Option[HdKeyIndexCounter]] = proxy(MockDIDNonSecretStorage.GetHdKeyCounter, did) @@ -89,6 +92,7 @@ object MockDIDNonSecretStorage extends Mock[DIDNonSecretStorage] { ] object UpdateManagedDID extends Effect[(PrismDID, ManagedDIDStatePatch), Throwable, Unit] object GetMaxDIDIndex extends Effect[Unit, Throwable, Option[Int]] + object IncrementAndGetNextDIDIndex extends Effect[Unit, Nothing, Int] object GetHdKeyCounter extends Effect[PrismDID, Throwable, Option[HdKeyIndexCounter]] object GetKeyMeta extends Effect[(PrismDID, KeyId), Throwable, Option[(ManagedDIDKeyMeta, Array[Byte])]] object InsertHdKeyMeta extends Effect[(PrismDID, KeyId, ManagedDIDKeyMeta, Array[Byte]), Throwable, Unit] diff --git a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala new file mode 100644 index 0000000000..687f5e9aa1 --- /dev/null +++ b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala @@ -0,0 +1,19 @@ +//package org.hyperledger.identus.connect.core.model +// +//import org.hyperledger.identus.messaging.Serde +//import zio.json.{DecoderOps, DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} +// +//import java.nio.charset.StandardCharsets +//import java.util.UUID +// +//case class WalletIdAndRecordId(walletId: UUID, recordId: UUID) +// +//object WalletIdAndRecordId { +// given encoder: JsonEncoder[WalletIdAndRecordId] = DeriveJsonEncoder.gen[WalletIdAndRecordId] +// given decoder: JsonDecoder[WalletIdAndRecordId] = DeriveJsonDecoder.gen[WalletIdAndRecordId] +// given ser: Serde[WalletIdAndRecordId] = new Serde[WalletIdAndRecordId] { +// override def serialize(t: WalletIdAndRecordId): Array[Byte] = t.toJson.getBytes(StandardCharsets.UTF_8) +// override def deserialize(ba: Array[Byte]): WalletIdAndRecordId = +// new String(ba, StandardCharsets.UTF_8).fromJson[WalletIdAndRecordId].getOrElse(throw RuntimeException("")) +// } +//} diff --git a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala index a4072aea05..db91377abc 100644 --- a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala +++ b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala @@ -1,14 +1,13 @@ package org.hyperledger.identus.connect.core.service -import org.hyperledger.identus.* import org.hyperledger.identus.connect.core.model.{ConnectionRecord, ConnectionRecordBeforeStored} -import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.* import org.hyperledger.identus.connect.core.model.ConnectionRecord.* import org.hyperledger.identus.connect.core.repository.ConnectionRepository import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.mercury.protocol.connection.* import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation +import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils @@ -21,9 +20,12 @@ import java.util.UUID private class ConnectionServiceImpl( connectionRepository: ConnectionRepository, + messageProducer: Producer[UUID, WalletIdAndRecordId], maxRetries: Int = 5, // TODO move to config ) extends ConnectionService { + private val TOPIC_NAME = "connect" + override def createConnectionInvitation( label: Option[String], goalCode: Option[String], @@ -147,6 +149,11 @@ private class ConnectionServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_invitee_pending_to_req_sent" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + // TODO Should we use a singleton producer or create a new one each time?? (underlying Kafka Producer is thread safe) + _ <- messageProducer + .produce(TOPIC_NAME, record.id, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id)) + .orDie maybeRecord <- connectionRepository .findById(record.id) record <- ZIO.getOrFailWith(RecordIdNotFound(recordId))(maybeRecord) @@ -220,6 +227,10 @@ private class ConnectionServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_inviter_pending_to_res_sent" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id)) + .orDie record <- connectionRepository.getById(record.id) } yield record @@ -306,6 +317,6 @@ private class ConnectionServiceImpl( } object ConnectionServiceImpl { - val layer: URLayer[ConnectionRepository, ConnectionService] = - ZLayer.fromFunction(ConnectionServiceImpl(_)) + val layer: URLayer[ConnectionRepository & Producer[UUID, WalletIdAndRecordId], ConnectionService] = + ZLayer.fromFunction(ConnectionServiceImpl(_, _)) } diff --git a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala index b0fa8d43fd..7067b55bf6 100644 --- a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala +++ b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala @@ -3,17 +3,17 @@ package org.hyperledger.identus.connect.core.service import io.circe.syntax.* import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.InvalidStateForOperation -import org.hyperledger.identus.connect.core.model.ConnectionRecord import org.hyperledger.identus.connect.core.model.ConnectionRecord.* import org.hyperledger.identus.connect.core.repository.ConnectionRepositoryInMemory import org.hyperledger.identus.mercury.model.{DidId, Message} import org.hyperledger.identus.mercury.protocol.connection.ConnectionResponse +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.WalletIdAndRecordId import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import zio.test.* import zio.test.Assertion.* -import java.time.Instant import java.util.UUID object ConnectionServiceImplSpec extends ZIOSpecDefault { @@ -310,7 +310,13 @@ object ConnectionServiceImplSpec extends ZIOSpecDefault { } } } - ).provide(connectionServiceLayer, ZLayer.succeed(WalletAccessContext(WalletId.random))) + ).provide( + connectionServiceLayer, + messaging.MessagingServiceConfig.inMemoryLayer, + messaging.MessagingService.serviceLayer, + messaging.MessagingService.producerLayer[UUID, WalletIdAndRecordId], + ZLayer.succeed(WalletAccessContext(WalletId.random)), + ) } } diff --git a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala index b9e54811b9..185bd95b95 100644 --- a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala +++ b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala @@ -7,11 +7,12 @@ import org.hyperledger.identus.event.notification.* import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse} import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.WalletIdAndRecordId import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import zio.mock.Expectation import zio.test.* -import zio.ZIO.* import java.time.Instant import java.util.UUID @@ -151,7 +152,10 @@ object ConnectionServiceNotifierSpec extends ZIOSpecDefault { ConnectionRepositoryInMemory.layer ++ inviteeExpectations.toLayer ) >>> ConnectionServiceNotifier.layer, - ZLayer.succeed(WalletAccessContext(WalletId.random)) + ZLayer.succeed(WalletAccessContext(WalletId.random)), + messaging.MessagingServiceConfig.inMemoryLayer, + messaging.MessagingService.serviceLayer, + messaging.MessagingService.producerLayer[UUID, WalletIdAndRecordId] ) ) } diff --git a/docs/docusaurus/credentialdefinition/create.md b/docs/docusaurus/credentialdefinition/create.md index 653e7eb6fc..f387e5de33 100644 --- a/docs/docusaurus/credentialdefinition/create.md +++ b/docs/docusaurus/credentialdefinition/create.md @@ -6,6 +6,10 @@ The OpenAPI specification and ReDoc documentation describe the endpoint. In this document, you can find step-by-step instructions for creating the credential definition. +## Prerequisites + +Before creating a credential definition, one must first create and then publish [prism DID](../dids/create.md), and then [create a credential schema](../schemas/create.md) to be used for the credential definition. Credential schema for credential definition **must** have a type of `AnoncredSchemaV1` as shown in [this](../schemas/credential-schema#schema-anoncred-schema) example. + ## Step-by-step guide The following guide demonstrates how to create a birth certificate credential definition. @@ -33,7 +37,9 @@ Here's a sample content of the credential definition: 1. Use your preferred REST API client, such as Postman or Insomnia, or utilize a client stub that's generated based on the OpenAPI specification. -2. In your API client, initiate a new POST request to the `/credential-definition-registry/definitions/` endpoint. +2. In your API client, initiate a new POST request to either `/credential-definition-registry/definitions` or `/credential-definition-registry/definitions/did-url` endpoints. They both take the same payload + 1. `/credential-definition-registry/definitions` creates a credential definition that can later be resolved via HTTP URL + 2. `/credential-definition-registry/definitions/did-url` creates a credential definition that can later be resolved via [DID URL](/docs/concepts/glossary#did-url), the DID includes a service endpoint with the location of the credential definition registry. Please note: The `author` field value should align with the short form of a PRISM DID previously created by the same agent. It's okay if this DID is unpublished. You can refer to the [Create DID](../dids/create.md) documentation for more comprehensive details on crafting a PRISM DID. @@ -55,6 +61,7 @@ Please note: The `author` field value should align with the short form of a PRIS 4. Transmit the POST Request to Create the New Credential Definition Once you've crafted your POST request, send it. Upon success, the server should respond with a GUID that uniquely identifies the new credential definition. +The response bodies will be the same for HTTP URL endpoint and DID URL endpoint, as well as request bodies, the only difference will be the URL, and how this credential definitions will be resolved later, via HTTP URL or DID URL respectivly. For ease of reference, here's a `curl` example: @@ -99,7 +106,7 @@ A potential response could be: ### 3. Retrieve the Created Credential Definition -To obtain details of the newly created credential definition, send a GET request to the `/credential-definition-registry/definitions/{guid}` endpoint. Replace `{guid}` with the unique GUID returned from the previous creation step. +To obtain details of the newly created credential definition, send a GET request to either `/credential-definition-registry/definitions/{guid}` or `/credential-definition-registry/definitions/did-url/{guid}` endpoints. Replace `{guid}` with the unique GUID returned from the previous creation step. Note that if you've created a credential definitoin via HTTP URL endpoint, you can retrieve it via `/credential-definition-registry/definitions/{guid}` and if you've created credential definition via DID URL endpoint, it can only be retrieved via `/credential-definition-registry/definitions/did-url/{guid}` To exemplify this process, use the following `curl` command: @@ -110,6 +117,16 @@ curl -X 'GET' \ -H "apikey: $API_KEY" ``` +or in case of DID URL + + +```shell +curl -X 'GET' \ + 'http://localhost:8080/credential-definition-registry/definitions/did-url/3f86a73f-5b78-39c7-af77-0c16123fa9c2' \ + -H 'accept: application/json' \ + -H "apikey: $API_KEY" +``` + You should receive a response containing the JSON object representing the credential definition you've just established: ```json @@ -131,6 +148,15 @@ You should receive a response containing the JSON object representing the creden } ``` +Or in case of DID URL, the respoinse is [Prism Envelope](/docs/concepts/glossary#prism-envelope) + +```json +{ +"resource": "eyJhdXRob3IiOiJkaWQ6cHJpc206ZDYyYzFlNmJlZDdiYWYzYjgwNzFiZmE5NzUyNDg0Zjg5ODRlNzUzMWZjMmM1MGJiOTQ4OTE4YWYwNWFiMjAxOSIsImF1dGhvcmVkIjoiMjAyNC0wOS0yN1QxMjozNjoxNS42NDcwNTRaIiwiZGVmaW5pdGlvbiI6eyJpc3N1ZXJJZCI6ImRpZDpwcmlzbTpkNjJjMWU2YmVkN2JhZjNiODA3MWJmYTk3NTI0ODRmODk4NGU3NTMxZmMyYzUwYmI5NDg5MThhZjA1YWIyMDE5Iiwic2NoZW1hSWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODUvc2NoZW1hLXJlZ2lzdHJ5L3NjaGVtYXMvZGFjZjNiOGUtODllYi0zYWQ2LWExNDYtMTIyZGRhN2Q2MjY0L3NjaGVtYSIsInRhZyI6IkxpY2VuY2UiLCJ0eXBlIjoiQ0wiLCJ2YWx1ZSI6eyJwcmltYXJ5Ijp7Im4iOiIxMDU2NTQwMzY4NTExMTkxOTg0NDI4MDQ5MDUxOTcxMzg3NzI2NjA5NjEzNjI1NjQ5NDI3MjY0OTIyMjEyMDY1Mzc3ODc0Mjc2NjQ3NjE2MTA1Mjg5MTEyNjg2NTI1ODI5MDI4MzM2MzE4MDQ0NjI5ODg0NDQ3NzI4Mzk5NzY5NDk3Njc3NTg1MDY1NDE2NjA4NDU4ODcxMTEyMDY3NjMwMTU1MjQyNzk5NDcwNzg5MDE3NDUwMjE3NTQ0NzU3NzcyMzc0OTk0MzQ4MTcwODU5MTM3ODQ0NzkwMzY4NzIyOTM2NTY3NzY0Mjk2NTUwMDQ5NzQwODc0NTExMTE4MTQ3ODE0MDIxNzAxNTU2MDY2NDYwMDE2NDQ2NDY5ODY3OTY1MjcyMDMwODEzNDY1ODU3NzQzMjE1NTYwNDUxMjU0NzI0MzU3ODI4NDk0NjAxMzI2MzI2MTczOTgzMzQ3ODk4NTMwODkxMjgzODk2MzY0MjQyNzY0MDY0MzM0MDQ1MTg4Nzk2NzkzMjg3MTgyMjgwMjg0OTI0ODkzMzIwNDg5MjcwMzg0MTg4Nzc3NzQ3MDYwNjg4Mjc1ODg3MzYzMTA1Mzg3MDE3MDAzMzYxMzkxNTcxNjgyMjk4OTY1NDYzNzAxMDYyODcwMTA1MDg5MjkzNTcyMDQ0MzA3ODQ2NzQ3OTQ5NDg3OTMyMTA2MjA2ODIxNzc1MTc1NTA3ODk5MTg2MzMyMTE2NjA5MzU4ODE2OTYzNDUyMzU2NjQwNTMxMTMzNjExMTkyMDY2NDk0MTA1ODEwNzg0NDg2ODAwNjA0Mjc2NDE4MzIxMzciLCJyIjp7ImJpcnRoZGF5IjoiNDk3NzU1NDY2NjkwNTI5MDcxMzE1NTI5NjE1OTY2NTE1MzQ3MzM2NjUyMzExNzg4OTYzODE5MjE4MzU5MTAyOTQ0NzczMjQ1ODgxNzEyMjcxNjAxMTgxNTA2MTQ2MzMwMjYwMTI5MDEyMTUzMzQ1NjU3MzU1NDI5MzM4MDg5ODU5ODc5MDQ5NDczNTAyODUxMjk1NDM4NTIyMzY5NzM3NzQ1NDc3MzkzODI5ODU1NzI0NDk3MzY4NjYyNjI5NTEzNDYzODgwODI3ODI2NjYxNDcyNjc3ODc5NDA2NTE1MDAzMzcwMTA0NjU0MzMyOTYwNzU1NTU0OTY3NDM0NTQwODIzNzcwODg3NTk2MTE4Mjk2NjkyODg1ODY4OTU2MDAwNzc1MjkxNDIyNzQ1ODEzMzUzODMxMzMxNDk4OTk3ODc5MjUzNTIxNTg1Nzg2NDExODk0NTY4MTE5NzI1MjAzNjg3MDg4MDU0NDgzODc3NDEzNDI5MjQ5NDc2MjYwNDU3MjIwMjA1Nzc1MzA1NTA4NzcxMjYyMjM0MTI1ODMyOTA1OTUzMDM4MTg1MDc2NjUzMTQzNDcwNDQzNTQ2Mzc0NDk4MTM3NTc2NzY0Mzk3NjU5NjY4NDc4MTU0Njg5MzY5NzUyMDY0MDg1MTYwMzM0ODQxNTQ2MjYxMzM2MzAyODE5NTI0MDI1MDkwMDcwOTEzNDQwMjA2NzI3NzEyODIzNTIzMjI2MDQ2NTY0Nzk3MTA1MjA2NTU1OTY0ODcxMzMxMzAxNjc2ODQyMTIyODMzMTM5NjEwNjM1ODg5ODkyODg4NTY3OTk5MDUiLCJsb2NhdGlvbiI6Ijk2NTAwODMyMjE1NTE5Mjc1MTAwMDc4MjgzMjkyNTU2NTM2MzYyMjQ3ODM4NzcxNDczMzAzMDMwMTQwMTA0ODM2OTUyODI5ODcxMTYxODUxMTgxODIyMDg1Mzc3Nzg4NTI5NDg4ODg3NTA5OTEyODIzNTE0NzIzOTM0MTg4Mzc1MjAxMjE5NzQ2MDYyMjExNzU0MTgxOTgzNjMxNDIzOTMwNTgwMTM2Njg1ODYwNjc3NzAxNDMwMTIwNjM2NTc5MTUwNzIwMzA5OTg2NDM1NzEzNDIwMzIxMjQyODIwNzI2NzIxNzM4MjE4Mjg1NzM1OTU0NTE0ODI3MTQ2MTk0NjAzMzUyMDExNTg3NzEwNDYxODEwMTcxOTI2NzQ2OTM5ODEzMzYxOTAzODYxNzIwODg0NjcxOTMzMjkwNTk1MDM2NjA2MzYyMTk1NTk4MTQ4NTM3NTkzOTQxNzg1NTI3NTA2Nzg1ODkwMTc4OTU0MTMwMzQ5MTI3NjA0NjQ2NjY3Nzg3Nzc5ODgxMDM0OTYwNjM2ODE4NDU3ODMzMTAxMTM0ODYzNDkxOTA1NDAyMzM1NzIwNjg0MjM4MTM1NjcwNDg4ODgzNzU5Nzk0NTAwMDc4MDkxMzMyNjI3NzEwMzc4NTQ3MTEwMDM5MTk1NTQ2MjE0NDUwMzAzOTc3NTI0NDU4ODgzMTI1NzIyNjY2OTYxMDczODA2MDE2NTg1NjA5Nzg2MTIwODI3MDcwNzY1ODA4MTg0NDAyMzYyMDk1NDkyOTc4MDAxOTU3MDQxMjA2NjU3MTgwMTA0ODA0ODE1NDE5MTg1OTE0MzY1IiwibWFzdGVyX3NlY3JldCI6IjkwMTQyODA1NjM0MTQxNTAwODgxMDI5NzU2NjQ0MDYzMzIyNDAyNTg4OTg5NTMwMzA4NzExNjI1ODk3NDc1MDcyMDUzNDIwMjU0MDQxNTY0MjUzMjA1ODczMjMyNDg0NTc2NDk5OTk0MDUzODg1MjY4NjcxODY0MzU3NTc4NTY5NjU4MTQyMTc3NDEzOTc5MjcyNTU3MDQ0MDAxMzc3MzU0MDQwMTk3ODAxMzgwMzEwMTQ5ODAzMTMxMDc0ODAwOTM2Nzc5NjU5MjU3NTcwNDkxODk3MzI2ODYyNDMxMDUzNDE3NTE0NTEwNzQ3MDI4ODY2OTE3ODA4MDA5NTQyOTM1NDI2NDQ4NzQwMjI0MjAwNDAzMjIwNTY4MDA5ODgxMDI5ODIwNDUzMjE0MTU5MzY2MzU4NTc4NzE3Mzc5NTMwNzQ4NDk1NzM3MDQ5NjMwNDM5NjIwODcxODk2NTMwODQ3ODAzMjE3NDUyODk4OTI2NDYxNjQ1NTAxODU5MzAxMjM5MjAyOTA5NzY5NDUyMTIwNDQ0NDUyNzAzNzgyNDIyOTQ1NjE1MjY3Nzc4MzI0NzkyNTYxNTYzODg5MzQ5NDI3NzU3Mjk5OTc3MTA5MDE5NjMwOTAxMzk5ODg2NTg2NTU3NTE0NTg5MTM0Nzk5MTk3OTY3MzIxMzQ3Nzc1Mjk5OTIxMDU0MjE0NTM5MDQ3MjA5NTAwNjMyNTY5NDM3Njc1MDQ5NzQxMDQxNDE4OTczNDEyMjYwMzUxODI5Nzk0MDk5ODIzMzg5OTU1MDQ1NjU0MjMwMTA5MTU0NzA1NjU3MDk3NjczODc0In0sInJjdHh0IjoiNTQ4NTk3NDYwMTc5NjcxNzIwMjg0MjIxOTMwODU0NjI4MDMyOTQyNDMwMDI2NTM2NDk3NTY3MDcyOTEzMzIyOTc1NzE3NzgwMzY3MjAwOTUyMjEwNzk2NDY3MTUxMjI4OTg1OTU0MTM3MDQzMDIyNTQ4MjA0NzU0NDE4NTc0OTU1NDA0ODgyNjc1MzI2Mzg5NDU0NjAzMzkwNjE3MzAyMzYyMjMwMTM3NDI1ODgyODcxMTA2NjQ1Nzg0OTg3MjgwNDUwNjIxODc0MjQxNDkzOTQzNDkyMzc5NjczMTk3OTYxNzk2Nzc4NzYwNTI4MzI5NDU0MTc4MzM2MDIyMjM4OTYwNjQ1NDgxNzc0NDAyMzY3NDAwMTMwNzk2OTc1MzM2OTU2NjI3MzM3MzMzMjUxMzY4MTY3MjkzMTk1NDU1MTA1MTUyOTM4MTU2MzU1NDg4MDI5ODk4NjUyOTM1Mzc1MTI5NTMyNTI3NjUzNjgyMzE1MzcwNjA2MTkxNzkxMzkyMjUyODI5NzkyMTc3MDc3ODA3MDU1MjAwNzU2NjI5NjI3NDI2NDc5MDg3MTI2NDk3MDQ0NDU5MDY2ODE3MTExODYwMjUzMjQ1NTc5NzMyMDMyNzk3NjQ0NjM0ODY2MTU0NDI2NjIwNzE2ODc3NjcxNDI5NjIyNzAzMzU5NzEzNTM1MjQwNjUzMjcwNzEyOTc4NDk1NDI0NjYyNTkxNDk3MTM0NjkyODM5MDMxMDYwMTMxODkyMjE4ODQ0MDcxNjg2MjIyODE0MzM4NTY2MDIyNjM0MTQ3ODY0MDc5OTg4Mzc4MjQ1OTE4NTkiLCJzIjoiNzkxNDQwOTkzMTkzMjI4NzQ4Nzg3NDA2MTM4MzQ3Nzg1NTI1NjUwODgyNzE3NTg4MzcxODgzMzk4NDg2NTg3OTc1Nzc0ODQ1MDY1NTE3Mjk4NzM2NTAxMjU4MTUyNjg1NDI1MDA1OTU3MTUwNjk1MDE3MDUwMzc2NzE4MDQ2MjUyNTM2NDI4MTQ0MzEyMzMzMDgzMTI5MDMyNDE2NzI3MzI0NzgxNTMwMTI4MTE0MDQ3MTU1ODc3Mzg5Njg1MTE1MTMyODU4Njk3NTYwNzgwMjE2MTAxOTI1MTc3NzYxNzE5OTkxODc3OTE4OTAyMzQ2MDA2MDg5OTU2MTExNTAwNzQ5MDkwMzMxNzY5Njk4NjUzNzIwNTkyNTc5ODEzMzY4Nzk5NzI0MDg3ODAwMjAwNTM3MzYwNTg3NjQwNTc3ODYyMjAwMzE4NTIxODI2NjAyNTA3ODA5Mjc3NzUwMTY0ODUyNTY1MjQwMzgwMDU0MTM1NDY5NzUxNDEwNDk4MTQ3NzMxNDIxNzU0OTE2NDIwMjgzNTM2NzY1NDA3OTE3NDcxOTgyMDQxMDkyOTYwODUxMzA2NjM0NzgzMDY0NzA5MTY2NDc1MDc3MTk4MTAxNDIwODQ5MzA2NzkyMTU3NjMzMTUwNDkwNjgyMDE2NDk1NDMwNDAwMjI5MDEyOTM2MTg2Mjk2MzMyMDcwMzA2OTE2MDM5NDk5ODQ5Njk5MTgxNDQ2ODYxNzgyNTM3OTU5ODc0MDAzNzc3MzE3NjY4MTI3NDg4MjE3MzcxOTIyNDc3ODMzMDAxODMyODAwMjEyNDU5MzkzNTgxNjMiLCJ6IjoiNzI5ODc2NDE0MjA1NzkyNTk2ODc4NTQwMTAzODUxODM5NjY3ODIyMDE5MzQwODMxNjgxMTgxOTI1MTY3ODcwMDU4OTk0ODg4MDE4OTg4Nzc2NTczMDEyMDQ2NTc3ODI4OTY5OTA5NTE4MTIwNzMwMTc4MTQ2ODQ2NDU0NjczNDk3NTQ5MTAyNjAxNzY0NzYzMDUyMjExMTcwNDMyNzQyNTI4Nzg1OTU1NjIyOTE4NzMyMjQwMjk5NDg2NjY5OTc5MjI1ODk5NzMwMzUwMDMyMjM0NTIyMzA5MjU3MjcxNDE5MDc1MDg4MDg5MzkzNjQxOTYxOTA0Njg2MDAwODgwNTQ5ODIxMTEyNTIyMTEyMDA3MzUwODA5NTY2NjYzODM5NjA0NjEwMzQ5NDM4MTgxMzY0MDE5MzU2NTE4NTcyOTA3Mjk5OTY4NzU2NDE4MjIzNDM1ODYxNjE2MzkwNDIzMzYxMzM0NTE1NDk4NDcxMDk4NjA2NjY0NTczOTE1ODQ0NTg2MjYxMTQxNzQ1MzYwMTkzNjc4MDA4MDk3OTE5MTIxNDgxMDgwODM5OTAwMTMyMTY2NTU4NjAxMDAyMzQxMzc5NTczMzc3NjcwNDk2MDc2ODMxMzU0MzE2MjEzNDY3MDA1MDcyMDgxMDgwODcyODIxMzEwMzQ2NTc1MjgzMDYwNDEwMjA2NzUxNzk1NjM1MDY1NDM5MTQ5OTI1NTc5MjIzMjM4NTI0NzA5OTg5NzgzMTA0Njc3NTc0MDE1NjY5MjMxNjMxNDAwMjI5NTc2OTM1NTc3NzE5MTI1MDIwNDczOTcwNjU3MjgifSwicmV2b2NhdGlvbiI6eyJnIjoiMSAxMjI1OTQ3NDcyOTc2QjVBQTgyMjlDMjI4RUFFNUI3NThDMDlGNkIwN0I1NDU4MEJDNzYzNTMwNkJFQjI1ODkyIDEgMDlBMzEzOEIzMzZGMUZCQUJEMEIxM0E5QTNBNzM1RDMyQjRFN0YzREZCNTE1QzkwRkQwNzVGNTY5QTQ5RTUyMSAyIDA5NUU0NURERjQxN0QwNUZCMTA5MzNGRkM2M0Q0NzQ1NDhCN0ZGRkY3ODg4ODAyRjA3RkZGRkZGN0QwN0E4QTgiLCJnX2Rhc2giOiIxIDI0OTA1M0Y0NUJCQTg3MDA3NzhDRjE1ODQxREZERUIwQUY1OTQzRTFFMkYyRUVFMDNFQUI2RDgwOEFDMkYxRUUgMSAwNzQyQ0U3QjM3MjAzNTc1QThDNUQzMjc1NzY3RDkxNUQ2RUVDRkVDMzc5NzkzOTNGNjYxRkY1NDE0QzdENEJBIDEgMThEN0ExMEUzNjJGQTE5MkYxRDJFNTE3OUQ1ODBGMTk0RTM5NDEzMTE1MTE3MDBGQkE3OUE0QTIxMkUxN0Y5RiAxIDA3QzNDQTRFMjBDOTkyQTEwQ0NDNzI0NDlBQUU5NjRFMDFEREZCNTBGQ0VEQkUxRTZDOEQwRUMzOThGOTU3MUMgMiAwOTVFNDVEREY0MTdEMDVGQjEwOTMzRkZDNjNENDc0NTQ4QjdGRkZGNzg4ODgwMkYwN0ZGRkZGRjdEMDdBOEE4IDEgMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsImgiOiIxIDE5RUMwQkExMTRFMTBBN0Y5RkVCMkU3MzMyNTZDQzk4NTU0RkVEQzFGRjgzN0REQUEzNDQxN0U1NzYyNTg0NzkgMSAwMTYzN0VGQTBCQ0E2QjNENjZDMzE1RUU4QUJGNzNCMEU5MUVCNTA1REE1QUZCRDk2MTA5M0QwMjk5RjRGQUVEIDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCIsImgwIjoiMSAxQUMwQUY5NTBEN0UyRkE0MDNDN0UwRkI5RThDNTE4N0Q3QTBBMzFBMzgzQkFFNDVGRUEyMUFGNjZFOEM1MTkxIDEgMTMyNTZGMDBCODQyOTNGMjA3M0VCOUVFRTFCMUY4MDU4MTAyNkMzMkE1RjdGQThDNEExQTEwODMyNkE2Mzc0NSAyIDA5NUU0NURERjQxN0QwNUZCMTA5MzNGRkM2M0Q0NzQ1NDhCN0ZGRkY3ODg4ODAyRjA3RkZGRkZGN0QwN0E4QTgiLCJoMSI6IjEgMUFEMTUwOTJDMTNDNUI0NUY4MDk3OENFQ0ZCM0Y5NUUyMTNBMURBQ0NBNDMxRkEzNkE3MUU4OTg4MUE2RkVDMCAxIDI0NEEwRTMyODkyMjcxNTE5NjYwMEQ2NkYxRTU3MzA4NUE0MURDM0UxRkU4RjZEODU0OTY1RTNFMkFFNkY2RTIgMiAwOTVFNDVEREY0MTdEMDVGQjEwOTMzRkZDNjNENDc0NTQ4QjdGRkZGNzg4ODgwMkYwN0ZGRkZGRjdEMDdBOEE4IiwiaDIiOiIxIDBGMEZDRUJDNTE1MzI3MjEyNDI2QzIwNkY2ODQ4N0U5M0EyNjc1NTdCRjg5QUJENkIxNTBDMzA2MUM1RDNDRjcgMSAwN0I1OUVFMThFREY1RThFQzEzM0ZFMDY1N0RDRjEzMTAxMUYwQzFERUM1RjQ2NDU3MUMxMTIyMDc3RjMwMDVEIDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCIsImhfY2FwIjoiMSAyNDAzOTI2NjBDNjYxMDhCQjgzNDkwMDk1NzBGQTQ0MkFBRDAzQTY1MTU2NkQwMUE4MkVFRDQwMDNEMzY1QzY3IDEgMEMyQTE3Q0Q5MDM4QzE4QzU3Q0QyNDY4RTA2RENFNzU4NEE4Q0I4RTgwMzkzQjNBMjBCRkZERTY5MEQ0QzM2MCAxIDIzQ0ZFREE0NkJDMThEQUMzODdCRDdDMUQ2RjM1MzYyMEY2M0E0REI0OEM5MDBEQ0QxRkYzMEEzNkRDNjQwNEYgMSAwREJCQzQ3MUUyMzUwQ0I1QkU2RkFGN0RDOTBDQkQ3Q0MxRTkyOUUwQ0MzOUJCRDlBOUNBOEJFM0Y3REVCNkIwIDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCAxIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJodGlsZGUiOiIxIDAxQjQ4Mzg3QjlGRDlCQUQwRTg1RTJCMDFCNDI5OEJCNzNDQ0JBMjE5NzdDQTk4RDZFRDQ2MDM2ODNEODFDNjAgMSAxRTQwMzE1RDYyNTY1RDg4MjBFM0RGODRFMEUzRTE4RkI4OTFDNkJDNTJFOTJEMzhCNTIzMDNFOENFNDI4Q0U2IDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCIsInBrIjoiMSAwMjNCRDYzRDM0RkE0NjYwN0REMDgyN0Q2MjlDMzdCOEVBMEZFMjJFOUQwM0JCOTgzNjlBMTI1RjczRDc0OERGIDEgMEZFMEE3Nzg1MEYyRkNBOEFEREFCMTlCQTY0QUYzMEVFM0JBNTNDRTMwRTUxOEY3MzcxRkNFMTQ1NjQ1MTg0MCAyIDA5NUU0NURERjQxN0QwNUZCMTA5MzNGRkM2M0Q0NzQ1NDhCN0ZGRkY3ODg4ODAyRjA3RkZGRkZGN0QwN0E4QTgiLCJ1IjoiMSAwMkY0OUVBNzFGMjEzNkRERjNBNDkyRjMzRjFDRTYzMEFFNzQ1OTc3NDVDQkFEQkZGQzAyQjczNTVEOURBREJGIDEgMEU2OTNENjlFNUUzRDYxRjhGMDlFQ0UwRDJBMDgwOTUzQzkyNzMxOUY2OTQ0NTRGOTJFRjI0RDBBMDIwMjU0RCAxIDIxMTQxRDg0MTU2QkQxMkUzNEFDRDFDMkY3QTA2NzRCQkU0NUMwRjUyRjBFM0M3RUZEMjNDOURCNURDMDc2MjMgMSAxOUNENzY3NUQ5MTk0MzlFMjAxRDQzREFCNDdDREVDQUM1MTE0QkEzQzQyRTE4QkY1REQ5M0Y5N0UxMkNBNkMyIDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCAxIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJ5IjoiMSAwOUU5REJGQkI4QTUxNTZBQjdENjc5QkNCOEZCNkE0RkU3NDJENTVDMURBQzAzNzA2QTQ1QUQyOUFCM0VFNzBGIDEgMTg0OEI3RkYxOTQ4MUFEQjY4RkRFMDI0MEExRkFDOTVBODc0OTg3NUVFOTg4OUZENzlCMjU5NTRERUJDODI4QSAxIDEwODIzRkNDMkI0MTNFRDJFRUEzQkQ4RjJCRjNBMUZGMjc3QjdFQTRGNkI1RjBGMTZDODQ3M0QyRjc1RTc0MTUgMSAxMTJDRjc0NkVDNzVDMEU2MDY3MzJBQTQ1ODZCNDM3MDVDMUY2MEJDREQ5RjBDMTUwQzVGMDYyMkQ3NTNCMTU1IDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCAxIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAifX19LCJkZWZpbml0aW9uSnNvblNjaGVtYUlkIjoiUHVibGljQ3JlZGVudGlhbERlZmluaXRpb25WMSIsImRlc2NyaXB0aW9uIjoiQmlydGggY2VydGlmaWNhdGUgQW5vbmNyZWQgQ3JlZGVudGlhbCBEZWZpbml0aW9uIiwiZ3VpZCI6ImVmNGE1ZGRjLTA2MTItMzRmNi1hZjI4LWM1ZDE0N2FhNDM4MCIsImlkIjoiYjc2YWUwOTMtN2U0ZS00NWE4LThmOTEtMTJhMmMzMGNjYjYzIiwia2V5Q29ycmVjdG5lc3NQcm9vZiI6eyJjIjoiMzA3NDE2NTUyNjQzOTM2NzgxODg3MDMzMzY5MTYxOTk3NTUzNDMyMjQ1OTE3OTA4MDk3Mjg2MTcxMTcwOTYyOTAzNTY5NDQ5MDU3NjYiLCJ4cl9jYXAiOltbImxvY2F0aW9uIiwiMzc2ODI3NzMxMDQ2MzM0MTkzOTYwMzU4OTkwMzgxMDA3NDc5Mzc0MjM2NDQ5OTg5ODA5MDY0MDg3NDg4MDk4MTQ2MDgzMjc5MjI5ODA5NDQ0MjM2OTQzODU2MTk0MjAzMjM0OTE0NTAxMzI4NTc5NDQwMjg3NzM1MTc5Mjk4NjMzNDg5MzIwNTU5NzE1MTk1MzU0MTYwMDg5MzY0MjA3MzMxMzQyNjAwODI2NDAzNDI5MjM0OTUxNjU3MDI4NTI0Nzc3ODc4NzQ2MzAyMDE0OTA3MDg0NTg3MTI1Nzg5NzMyMDY0NDA1NTQ2Mjc5NDczOTIyMTEyNDg2NzU3Nzk2MTQ0MTAwMDcyMjc2NjQ3NTc4NTQ2OTAxMjEwMDAzNzc2NDA4MTg1NTE1NzI5MzM4OTgzODY3NDQyNTEyMzI3NzYxMzYzMDAxODg0Mjc4NjI0MzYyOTQ5ODc3OTQ5MzA0MzAxMjkxODA2NTk5ODkyNzA2OTE3OTQ5OTM0NDI3ODQzODAxMTgxODQxMjcwOTM0ODQ1MTc0NDE2OTIwNzE2MTM3OTU2NjA0NDA4MDc0NTA0OTk4NjIxMjc1NTM0MTIyMjYzMTIyMTUyNDUxMzAyNzAyNzE2ODYwNDc5MDQ3NDk0MTE2NDc3NDAzMjkzOTk4NzUwNTcxNTQ3MjI5NTQ1NTEwNzM1MDYzODUyMDc3Mjg0OTYwMTgwMDU1MDA4NjA5Njg3Mjg0MjE2MzMwMDI4MzY1Njc2OTAzMDIzMzg2NDI3MDk2NTc0MDExNzMzNDAxMTY2NDI5MjA0NzQ2MzI2MDk2MjU2NjQ5MTE1NzUyMjczOTAyMDM5NDM0Mzc2Mjc2MTI3MTMyMzEzOTE3OTI3NTUzOTU4NDUxNDY5ODYwMTk1NjkzNDIwMjIxMzI1NTg1NTg2NzMxIl0sWyJiaXJ0aGRheSIsIjI1NTAwMDM2ODMzNDA5MzQxOTUzOTE4MDAyODI0MTA1MTEwMzA4OTA3NDMzODQzMTQ5OTE3ODc3MTUxNDc5NDY2MzAxOTQ5NjY4MzA1NTgxNTc0NDE2Nzk1OTkyMDYzNzAwMDM3NzY4NDIyMzIxNzk2NDE1OTA3NjY1MzQ4NzI2MTE2OTkzNzgxNDgxODIwNzAzODU4MTA0NDgzOTk0MjI4MDQ1NjUxODMzODY4MjgzMjg1NDkwMzIxMzk3NTQzMjQyNzY5OTU3ODY0ODMzMDAxNjk0OTA2MjUzOTIwOTY1NDU4MzcyNzcxNjczOTExNTk0ODQyNjAzNDM5NTMzMzQwMDc5MDQ2NjM1ODMzNDA1NzcyNDAwMzQyMjQ5MTA4NjA5OTkyMTk3ODY5NjY3MzE5MjA1MzQ5ODg4NTUyNDAzNDc0Mzc0NDgwMzMyMTU2NjQyMjI2OTQ3MDUzNDQwNTEzNzg5NjcwNzY5OTU1MzQ5NjM0NzMyOTkwOTgzMDk5NTkyOTIzNzQ0NTc1OTE5MTE2Mjk3MDY3NjU4OTE0ODgwODAyOTMyODc3OTkwNTQwNDYzMDAxMDE4OTU5MTg0MzA4NzE3OTMwNzQ1OTk2OTE0Mjc2MzA2MzQzNTkwODA3MDk2NTM1MDYxOTI2ODY3MTU0NjQ2NDgzNDU4ODQ1NjY2NjQxNjU4MjA4NDM3MzA1MDM4OTc0NjQ4MDkzMTUyNjg1NTk0NDEwNzk2Mzg2OTI5MDM4MDcwOTQ2ODg1ODYxMzYyMDI2NjE0MTUxMDMxNjQ1OTY1NzMwOTgxNjY5NDc4MTQyOTEyMTMzNzQ1MDc3NTM3NDY4NDQ0MzU0MjA2NTA3NTU4OTE5NTQ5OTUxNTQ0MzI4MTkwMDQwMzA2MzQ3NDc0NzcwNzY1NTU5MDA1MTA4ODgwOSJdLFsibWFzdGVyX3NlY3JldCIsIjQ5MDA3ODIyMDM2OTkwODI2ODQxMTkwMzM3MTU4ODI4Nzc1NzE0NjM3MDc3OTg5Njg0MTA5NTc0NjA4ODQ3MjA2MzIxMTA2ODQxMzcwOTkxNTQ2NTMwMjYwMDIzNDU1MDk4OTcyNjcyNjUyNDQzMDQ1NzI2MTM3MTk2Nzc5NzUxNjQ2MDQyOTk3NjgyMzYyMTQxMTA1MjY5NTQ2MTkxMjMwNzcyODc5MzczNjcxNTY1ODk5NjAxMjQ2MjYwMDMzNjY4OTQyODY1MjgyMTg3NzA3OTc2NDcyNjg3NzU5NTA1NjQ4NjgyNzc0NTI3NzAzNzQyNjQxODc2MTgwOTU4OTI3MzMxNjcwNDAxMzI4ODI4Mzc2MjMxMTk4NzI3ODUyMzU4NTUxNzUzNTEwNTk1MzQwNjI4MTA0MjkxMDU5ODI4NDU2NTUwOTQyNjI0MjA3OTA4ODMyNzQ5NjM5ODQwODIxNzEyNTU1Mjg2MDE5Mjk2MTY4OTQ0NjEzNjg3NDM5MzU5NTMxNjA2NzQ3MDc3Mjc3MDE4MDE5MzU1MjM4NzUyMzUwMTYzODE3MzQ5ODI1ODY4ODQ0NTk0ODU5NDQ3Mzg0NDA0NjMxMDk3NjkzNTQ5NTk0NjkwNDcyMTI3Nzg1MTg4NDI5MTAxNjE5MDI4MTc1NjI3MzIzOTUzNTAyOTUwNzk3NTIwMDQ0OTE2MDY5MDg4MjczODU3NzYwNzU5MDkxNTQyNDQ4ODc4MzU5NTc1NDk5MjM1MzIzODc4NDA1MjIwNDc5MzMxMzE5MDE0ODg2NDk5NjE0MzI2MTA3MTM2MjA1ODg4Njg3ODU4Njg4NTgxNTI4NjQxMzI4ODg1NzkwNTc0MjAwNjUxMzA0NzU4NTE0MTAzNzAzNDM2NDAwMDM0NTQ4MjAxMDc5NDM4MzQ2NjU5OCJdXSwieHpfY2FwIjoiNDk2Njg1Mjk0NTI0Mjc4Njg0NDc0OTQ1MDQ0MzI5OTU5MjI0NTA4NzY1NjMzNTg4MDQzNDUwNDgxNTE4MzQ5ODk4NDAxMjk3MzQ2OTk0MjQ5ODEwMjIzODM3MzE0MDc0NDI1NDQ4NzU1OTYwMjg2MTU3ODY4NTk0NDU2MDE2Mjc3NTcyMjAxMDgwNDQzMTA3NjQxNjM5MzExNDAyMDUyOTM3NjczMzI5MDM4NjU0Nzc2MDI1MTA0OTYyMTEwMTQzNTg5NjA3NjEyOTM2NDI4MDc3MjQ1Nzg4Mjk2OTc2MzU5ODk5MjMwMjQwNTEyMjc5MzU0MDg2MTMwNjIyNjYzMjgzMTA4Njk4MTM0NjU0NTMzMTQ1NjY4NDU1NjExODU2MDY2Mjc2OTgxMjUwMDAyMTQ2ODgwNTk0NzM3MzU2MjA2MDE1NzUyMDAxNjE3ODU2OTI0MDk3NjU3ODU2NjkzNTYwODA1ODQ0MDQ3NTY3ODE2NTczNDUxNTg1NTUzODMzMzQ0ODE0NDMzMzMxNzk2ODc5Mzg5NjgyODM3MjQ3OTU0MDUwOTcwNzE0Mjg2NDQwNDg2NjY5MDE4MjMzODkxNTA0ODE0ODk3NDAyODYxNzMxMDg2NjEzMTU5MDM0NDEzNTIzMzk2NjE0NjE4NjQ3NjczNzgzMDIyMTAzMTQxNTg0MTAwMDY0NTE0NjA4ODYxMTY2Nzg3NjYzODMxMjE5MTM0NTYwNDAyNTEzMjQ5MDg0NzA5NDM2ODM5NTM5NTQ3MzQwNzMyNDYyOTE4MDg3NzU3MTYyOTQ2Mzk5MTMzNzA0MTA0MzMwNzUxNzU3OTMxMTE0OTkwNzA5NjU3NDQ3MjU0OTM1OTc3MDExNjQzMTc5NjU4MTg5MzI3MDg4NDY5NjExMzcxMTg1OTY2MzIwNzU3ODM1In0sImtleUNvcnJlY3RuZXNzUHJvb2ZKc29uU2NoZW1hSWQiOiJQcm9vZktleUNyZWRlbnRpYWxEZWZpbml0aW9uVjEiLCJuYW1lIjoiQmlydGggQ2VydGlmaWNhdGUgbG9jYXRpb24iLCJyZXNvbHV0aW9uTWV0aG9kIjoiZGlkIiwic2NoZW1hSWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODUvc2NoZW1hLXJlZ2lzdHJ5L3NjaGVtYXMvZGFjZjNiOGUtODllYi0zYWQ2LWExNDYtMTIyZGRhN2Q2MjY0L3NjaGVtYSIsInNpZ25hdHVyZVR5cGUiOiJDTCIsInN1cHBvcnRSZXZvY2F0aW9uIjp0cnVlLCJ0YWciOiJMaWNlbmNlIiwidmVyc2lvbiI6IjEuMC4wIn0=", +"url": "did:prism:d62c1e6bed7baf3b8071bfa9752484f8984e7531fc2c50bb948918af05ab2019?resourceService=agent-base-url&resourcePath=credential-definition-registry/definitions/did-url/ef4a5ddc-0612-34f6-af28-c5d147aa4380?resourceHash=ca8ea2c80ff1e07978e2ed59245186fbb9992daedbecb651535ad5996be372c1" + +``` + Remember, in the Cloud Agent, the combination of author, id, and version uniquely identifies each credential definition. Thus, using the same agent DID as the author, you cannot establish another credential definition with identical id and version values. ### 4. Update the Credential Definition @@ -141,5 +167,4 @@ To update or upgrade an existing credential definition, follow the steps outline 2. Update the `version` value to reflect the changes made. This is important to ensure that each version of the credential definition remains distinct. 3. Create a new credential definition entry with the updated version and schema. -Note: When you make changes to an existing credential definition, it's essential to version the new entry accurately. This ensures clarity and avoids potential conflicts or misunderstandings among different versions of the same definition. - +Note: When you make changes to an existing credential definition, it's essential to version the new entry accurately. This ensures clarity and avoids potential conflicts or misunderstandings among different versions of the same definition. \ No newline at end of file diff --git a/docs/docusaurus/credentials/issue.md b/docs/docusaurus/credentials/issue.md index b392d310c2..ff9beeae99 100644 --- a/docs/docusaurus/credentials/issue.md +++ b/docs/docusaurus/credentials/issue.md @@ -134,8 +134,11 @@ curl -X 'POST' \ 1. `claims`: The data stored in a verifiable credential. AnonCreds claims get expressed in a flat, "string -> string", key-value pair format. The claims contain the data that the issuer attests to, such as name, address, date of birth, and so on. 2. `connectionId`: The unique ID of the connection between the holder and the issuer to offer this credential over. 3. `credentialDefinitionId`: The unique ID of the [credential definition](../credentialdefinition/credential-definition.md) that has been created by the issuer as a prerequisite. Please refer to the [Create AnonCreds Credential Definition](../credentialdefinition/credential-definition.md) doc for details on how to create a credential definition. +:::note +📌 Note: If the credential definition was created via HTTP URL endpoint, then this credential definition will be referenced to that credential via HTTP URL, and if this credential definition was created via DID URL endpoint, then it will be referenced via DID URL, How to create credential definition for HTTP URL or DID URL is explained in [credential definition creation guide](../credentialdefinition/create.md) +::: 4. `credentialFormat`: The format of the credential that will be issued - `AnonCreds` in this case. - +5. `issuingDID`: The DID referring to the issuer to issue this credential from :::note The `connectionId` and `credentialDefinitionId` properties come from completing the pre-requisite steps listed above ::: @@ -159,6 +162,7 @@ curl -X 'POST' \ "drivingClass": "3" }, "credentialFormat": "AnonCreds", + "issuingDID": "did:prism:9f847f8bbb66c112f71d08ab39930d468ccbfe1e0e1d002be53d46c431212c26", "connectionId": "9d075518-f97e-4f11-9d10-d7348a7a0fda", "credentialDefinitionId": "5d737816-8fe8-3492-bfe3-1b3e2b67220b" }' diff --git a/docs/docusaurus/schemas/create.md b/docs/docusaurus/schemas/create.md index edfd7454f3..ad34b1f2c4 100644 --- a/docs/docusaurus/schemas/create.md +++ b/docs/docusaurus/schemas/create.md @@ -79,7 +79,9 @@ Specification. 1. Open your preferred REST API client, such as Postman or Insomnia, or use the client stub generated based on the OpenAPI specification. -2. In the client, create a new POST request to the `/cloud-agent/schema-registry/schemas` endpoint. +2. In the client, create a new POST request to either `/cloud-agent/schema-registry/schemas` or `/cloud-agent/schema-registry/schemas/did-url` endpoints. They both take the same payload. + 1. `/cloud-agent/schema-registry/schemas` creates a schema that can later be resolved via HTTP URL + 2. `/cloud-agent/schema-registry/schemas/did-url` creates a schema that can later be resolved via [DID URL](/docs/concepts/glossary#did-url), the DID includes a service endpoint with the location of the schema registry. Note that the value of the `author` field must match the short form of a PRISM DID that has been created using the same agent. An unpublished DID is sufficient. Please refer to the [Create DID](../dids/create.md) documentation page for more details on how to create a PRISM DID. @@ -252,6 +254,15 @@ curl -X 'POST' \ } ``` +or in case of DID url, the response will be created schema wrapped in [Prism Envelope](/docs/concepts/glossary#prism-envelope) + +```json +{ + "resource":"eyJhdXRob3IiOiJkaWQ6cHJpc206ZTAyNjZlZThkODBhMDAxNjNlNWY5MjJkYzI1NjdhYjk2MTE3MjRhMDBkYjkyNDIzMzAxMTU0MjgyMTY5ZGZmOSIsImF1dGhvcmVkIjoiMjAyNC0wOS0yNVQxMDozNzoxNi4wOTM2MDlaIiwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UgU2NoZW1hIiwiZ3VpZCI6IjVjOTNmYTAwLWUwM2UtMzlkZC05NDdmLTI2NWI4YzFlYWQ4YiIsImlkIjoiNjhmMGQ4MDctYTcyYi00OTY2LTg1NWItMmIzNGJjMjYzNzAyIiwibmFtZSI6ImRyaXZpbmctbGljZW5zZSIsInJlc29sdXRpb25NZXRob2QiOiJkaWQiLCJzY2hlbWEiOnsiJGlkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9kcml2aW5nLWxpY2Vuc2UtMS4wLjAiLCIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6dHJ1ZSwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UiLCJwcm9wZXJ0aWVzIjp7ImRhdGVPZklzc3VhbmNlIjp7ImZvcm1hdCI6ImRhdGUtdGltZSIsInR5cGUiOiJzdHJpbmcifSwiZHJpdmluZ0NsYXNzIjp7InR5cGUiOiJpbnRlZ2VyIn0sImRyaXZpbmdMaWNlbnNlSUQiOnsidHlwZSI6InN0cmluZyJ9LCJlbWFpbEFkZHJlc3MiOnsiZm9ybWF0IjoiZW1haWwiLCJ0eXBlIjoic3RyaW5nIn0sImZhbWlseU5hbWUiOnsidHlwZSI6InN0cmluZyJ9LCJnaXZlbk5hbWUiOnsidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsiZW1haWxBZGRyZXNzIiwiZmFtaWx5TmFtZSIsImRhdGVPZklzc3VhbmNlIiwiZHJpdmluZ0xpY2Vuc2VJRCIsImRyaXZpbmdDbGFzcyJdLCJ0eXBlIjoib2JqZWN0In0sInRhZ3MiOlsiZHJpdmluZyIsImxpY2Vuc2UiXSwidHlwZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtanNvbi1zY2hlbWFzL3NjaGVtYS8yLjAvc2NoZW1hLmpzb24iLCJ2ZXJzaW9uIjoiMS4wLjAifQ==", + "url":"did:prism:e0266ee8d80a00163e5f922dc2567ab9611724a00db92423301154282169dff9?resourceService=agent-base-url&resourcePath=schema-registry/schemas/did-url/5c93fa00-e03e-39dd-947f-265b8c1ead8b?resourceHash=d1557ede168f0f91097933aa2080edaf2f14fddd8a7362a22add97e431c4efe2" +} +``` + ### 3. Retrieve the created schema To retrieve the newly created schema, create a new GET request to the `/cloud-agent/schema-registry/schemas/{guid}` @@ -265,7 +276,17 @@ curl -X 'GET' \ -H "apikey: $API_KEY" ``` -The response should contain the JSON object representing the schema you just created. +or if you need to resolve a schema created via DID url, the endpoint will look like this `/cloud-agent/schema-registry/schemas/did-url/{guid}` + +```schell +curl -X 'GET' \ + 'http://localhost:8080/cloud-agent/schema-registry/schemas/did-url/3f86a73f-5b78-39c7-af77-0c16123fa9c2' \ + -H 'accept: application/json' \ + -H "apikey: $API_KEY" + +``` + +The response for HTTP URL request should contain the JSON object representing the schema you just created. ```json { @@ -323,6 +344,18 @@ The response should contain the JSON object representing the schema you just cre } ``` +and for DID URL request, response will include the same schema wrapped in [Prism envelope](/docs/concepts/glossary#prism-envelope) response + +```json +{ + "resource":"eyJhdXRob3IiOiJkaWQ6cHJpc206ZTAyNjZlZThkODBhMDAxNjNlNWY5MjJkYzI1NjdhYjk2MTE3MjRhMDBkYjkyNDIzMzAxMTU0MjgyMTY5ZGZmOSIsImF1dGhvcmVkIjoiMjAyNC0wOS0yNVQxMDozNzoxNi4wOTM2MDlaIiwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UgU2NoZW1hIiwiZ3VpZCI6IjVjOTNmYTAwLWUwM2UtMzlkZC05NDdmLTI2NWI4YzFlYWQ4YiIsImlkIjoiNjhmMGQ4MDctYTcyYi00OTY2LTg1NWItMmIzNGJjMjYzNzAyIiwibmFtZSI6ImRyaXZpbmctbGljZW5zZSIsInJlc29sdXRpb25NZXRob2QiOiJkaWQiLCJzY2hlbWEiOnsiJGlkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9kcml2aW5nLWxpY2Vuc2UtMS4wLjAiLCIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6dHJ1ZSwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UiLCJwcm9wZXJ0aWVzIjp7ImRhdGVPZklzc3VhbmNlIjp7ImZvcm1hdCI6ImRhdGUtdGltZSIsInR5cGUiOiJzdHJpbmcifSwiZHJpdmluZ0NsYXNzIjp7InR5cGUiOiJpbnRlZ2VyIn0sImRyaXZpbmdMaWNlbnNlSUQiOnsidHlwZSI6InN0cmluZyJ9LCJlbWFpbEFkZHJlc3MiOnsiZm9ybWF0IjoiZW1haWwiLCJ0eXBlIjoic3RyaW5nIn0sImZhbWlseU5hbWUiOnsidHlwZSI6InN0cmluZyJ9LCJnaXZlbk5hbWUiOnsidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsiZW1haWxBZGRyZXNzIiwiZmFtaWx5TmFtZSIsImRhdGVPZklzc3VhbmNlIiwiZHJpdmluZ0xpY2Vuc2VJRCIsImRyaXZpbmdDbGFzcyJdLCJ0eXBlIjoib2JqZWN0In0sInRhZ3MiOlsiZHJpdmluZyIsImxpY2Vuc2UiXSwidHlwZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtanNvbi1zY2hlbWFzL3NjaGVtYS8yLjAvc2NoZW1hLmpzb24iLCJ2ZXJzaW9uIjoiMS4wLjAifQ==", + "url":"did:prism:e0266ee8d80a00163e5f922dc2567ab9611724a00db92423301154282169dff9?resourceService=agent-base-url&resourcePath=schema-registry/schemas/did-url/5c93fa00-e03e-39dd-947f-265b8c1ead8b?resourceHash=d1557ede168f0f91097933aa2080edaf2f14fddd8a7362a22add97e431c4efe2" +} +``` + +Schemas created for HTTP URL (`/cloud-agent/schema-registry/schemas`) will not be resolvable by endpoint that returns schemas created for DID URL (`/cloud-agent/schema-registry/schemas/did-url`) and vice verca. + + The Cloud Agent instance's triple `author`, `id`, and `version` are unique. So, having a single [DID](/docs/concepts/glossary#decentralized-identifier) reference that the author uses, creating the credential schema with the same `id` and `version` is impossible. diff --git a/docs/docusaurus/schemas/credential-schema.md b/docs/docusaurus/schemas/credential-schema.md index 0630211be9..641de1011f 100644 --- a/docs/docusaurus/schemas/credential-schema.md +++ b/docs/docusaurus/schemas/credential-schema.md @@ -62,11 +62,13 @@ The locally unique identifier of the schema. ### longId (String) -Resource identifier of the given credential schema composed from the author's [DID]((/docs/concepts/glossary#decentralized-identifier) reference, id, and version fields. +Resource identifier of the given credential schema composed from the author's DID reference, id, and version fields. **Example:** `{author}/{id}?version={version}` > **Note:** According to the [W3C specification](https://w3c-ccg.github.io/vc-json-schemas/#id), this field is locally unique and combines the Issuer `DID`, `uuid`, and `version`. -**For **example:** `did:example:MDP8AsFhHzhwUvGNuYkX7T/06e126d1-fa44-4882-a243-1e326fbe21db?version=1.0` + +**For example:** `did:example:MDP8AsFhHzhwUvGNuYkX7T/06e126d1-fa44-4882-a243-1e326fbe21db?version=1.0` + --- @@ -217,14 +219,56 @@ A valid [ANONCRED-SCHEMA](https://hyperledger.github.io/anoncreds-spec/#term:sch ```json { - "name": "Birth Certificate Schema", - "version": "1.0", - "attrNames": [ - "location", - "birthday" - ], - "issuerId": "did:prism:4a5b5cf0a513e83b598bbea25cd6196746747f361a73ef77068268bc9bd732ff" -} + "name":"anoncred-birthday-cert", + "version":"1.0.0", + "description":"Birthday certificate", + "type":"AnoncredSchemaV1", + "author":"did:prism:e0266ee8d80a00163e5f922dc2567ab9611724a00db92423301154282169dff9", + "tags":[ + "birth", + "certificate" + ], + "schema":{ + "$schema":"https://json-schema.org/draft/2020-12/schema", + "type":"object", + "properties":{ + "name":{ + "type":"string", + "minLength":1 + }, + "version":{ + "type":"string", + "minLength":1 + }, + "attrNames":{ + "type":"array", + "items":{ + "type":"string", + "minLength":1 + }, + "minItems":1, + "maxItems":125, + "uniqueItems":true + }, + "issuerId":{ + "type":"string", + "minLength":1 + } + }, + "name":"Birth Certificate Schema", + "version":"1.0", + "attrNames":[ + "location", + "birthday" + ], + "issuerId":"did:prism:e0266ee8d80a00163e5f922dc2567ab9611724a00db92423301154282169dff9" + }, + "required":[ + "name", + "version" + ], + "additionalProperties":true + } ``` --- diff --git a/docs/docusaurus/schemas/update.md b/docs/docusaurus/schemas/update.md index 17010d13cc..9235b3499d 100644 --- a/docs/docusaurus/schemas/update.md +++ b/docs/docusaurus/schemas/update.md @@ -108,8 +108,8 @@ The JSON Schema changes must be defined as follows: 1. Open your preferred REST API client, such as Postman or Insomnia, or use the client stub generated based on the OpenAPI specification. -2. In the client, create a new PUT request to the `/cloud-agent/schema-registry/schemas/{id}` endpoint, where `id` is a - locally unique credential schema id, formatted as a URL. +2. In the client, create a new PUT request to either `/cloud-agent/schema-registry/schemas/{id}` or `/cloud-agent/schema-registry/schemas/did-url/{id}` endpoint, where `id` is a locally unique credential schema id, formatted as a URL, they both take same payload. + 1. When updating a schema, it is imporant to consider if the schema is `HTTP URL` resolvable or `DID URL` resolvable, the `id` in this endpoint needs to be used accordingly, if schema is `HTTP URL` resolvable, then only `/cloud-agent/schema-registry/schemas/{id}` can be used to update it, and same with regards to `DID URL` Note that the value of the `author` field must match the short form of a PRISM DID that has been created using the same agent. An unpublished DID is sufficient. Please refer to the [Create DID](../dids/create.md) documentation page for more details on how to create a PRISM DID. @@ -323,4 +323,12 @@ curl -X 'PUT' \ "kind": "CredentialSchema", "self": "/schema-registry/schemas/3f86a73f-5b78-39c7-af77-0c16123fa9c2" } -``` \ No newline at end of file +``` + +If you are updating schema that is DID URL resolvable, the response will be in a forom of [Prism Envelope](/docs/concepts/glossary#prism-envelope), like this: + +```json +{ + "resource":"eyJhdXRob3IiOiJkaWQ6cHJpc206ZTAyNjZlZThkODBhMDAxNjNlNWY5MjJkYzI1NjdhYjk2MTE3MjRhMDBkYjkyNDIzMzAxMTU0MjgyMTY5ZGZmOSIsImF1dGhvcmVkIjoiMjAyNC0wOS0yNVQxMDozNzoxNi4wOTM2MDlaIiwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UgU2NoZW1hIiwiZ3VpZCI6IjVjOTNmYTAwLWUwM2UtMzlkZC05NDdmLTI2NWI4YzFlYWQ4YiIsImlkIjoiNjhmMGQ4MDctYTcyYi00OTY2LTg1NWItMmIzNGJjMjYzNzAyIiwibmFtZSI6ImRyaXZpbmctbGljZW5zZSIsInJlc29sdXRpb25NZXRob2QiOiJkaWQiLCJzY2hlbWEiOnsiJGlkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9kcml2aW5nLWxpY2Vuc2UtMS4wLjAiLCIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6dHJ1ZSwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UiLCJwcm9wZXJ0aWVzIjp7ImRhdGVPZklzc3VhbmNlIjp7ImZvcm1hdCI6ImRhdGUtdGltZSIsInR5cGUiOiJzdHJpbmcifSwiZHJpdmluZ0NsYXNzIjp7InR5cGUiOiJpbnRlZ2VyIn0sImRyaXZpbmdMaWNlbnNlSUQiOnsidHlwZSI6InN0cmluZyJ9LCJlbWFpbEFkZHJlc3MiOnsiZm9ybWF0IjoiZW1haWwiLCJ0eXBlIjoic3RyaW5nIn0sImZhbWlseU5hbWUiOnsidHlwZSI6InN0cmluZyJ9LCJnaXZlbk5hbWUiOnsidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsiZW1haWxBZGRyZXNzIiwiZmFtaWx5TmFtZSIsImRhdGVPZklzc3VhbmNlIiwiZHJpdmluZ0xpY2Vuc2VJRCIsImRyaXZpbmdDbGFzcyJdLCJ0eXBlIjoib2JqZWN0In0sInRhZ3MiOlsiZHJpdmluZyIsImxpY2Vuc2UiXSwidHlwZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtanNvbi1zY2hlbWFzL3NjaGVtYS8yLjAvc2NoZW1hLmpzb24iLCJ2ZXJzaW9uIjoiMS4wLjAifQ==", + "url":"did:prism:e0266ee8d80a00163e5f922dc2567ab9611724a00db92423301154282169dff9?resourceService=agent-base-url&resourcePath=schema-registry/schemas/did-url/5c93fa00-e03e-39dd-947f-265b8c1ead8b?resourceHash=d1557ede168f0f91097933aa2080edaf2f14fddd8a7362a22add97e431c4efe2" +} \ No newline at end of file diff --git a/event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala b/event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala new file mode 100644 index 0000000000..54aea505f6 --- /dev/null +++ b/event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala @@ -0,0 +1,49 @@ +package org.hyperledger.identus.messaging + +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, MessagingService, Serde} +import zio.{durationInt, Random, Schedule, Scope, URIO, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer} +import zio.json.{DecoderOps, DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} + +import java.nio.charset.StandardCharsets +import java.util.UUID + +case class Customer(name: String) + +object Customer { + given encoder: JsonEncoder[Customer] = DeriveJsonEncoder.gen[Customer] + given decoder: JsonDecoder[Customer] = DeriveJsonDecoder.gen[Customer] + given serde: Serde[Customer] = new Serde[Customer]: + override def serialize(t: Customer): Array[Byte] = + t.toJson.getBytes(StandardCharsets.UTF_8) + override def deserialize(ba: Array[Byte]): Customer = + new String(ba, StandardCharsets.UTF_8).fromJson[Customer].getOrElse(Customer("Parsing Error")) +} + +object MessagingServiceTest extends ZIOAppDefault { + override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = { + val effect = for { + ms <- ZIO.service[MessagingService] + consumer <- ms.makeConsumer[UUID, Customer]("identus-cloud-agent") + producer <- ms.makeProducer[UUID, Customer]() + f1 <- consumer + .consume("Connect")(handle) + .fork + f2 <- Random.nextUUID + .flatMap(uuid => producer.produce("Connect", uuid, Customer(s"Name $uuid"))) + .repeat(Schedule.spaced(500.millis)) + .fork + _ <- ZIO.never + } yield () + effect.provide( + messaging.MessagingServiceConfig.inMemoryLayer, + messaging.MessagingService.serviceLayer, + ZLayer.succeed("Sample 'R' passed to handler") + ) + } + + def handle[K, V](msg: Message[K, V]): URIO[String, Unit] = for { + tag <- ZIO.service[String] + _ <- ZIO.logInfo(s"Handling new message [$tag]: ${msg.offset} - ${msg.key} - ${msg.value}") + } yield () +} diff --git a/event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala b/event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala new file mode 100644 index 0000000000..c6b068f16b --- /dev/null +++ b/event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala @@ -0,0 +1,66 @@ +package org.hyperledger.identus.messaging.kafka + +import org.hyperledger.identus.shared.messaging.* +import zio.* +import zio.test.* +import zio.test.Assertion.* + +object InMemoryMessagingServiceSpec extends ZIOSpecDefault { + val testLayer = MessagingServiceConfig.inMemoryLayer >+> MessagingService.serviceLayer >+> + MessagingService.producerLayer[String, String] >+> + MessagingService.consumerLayer[String, String]("test-group") + + def spec = suite("InMemoryMessagingServiceSpec")( + test("should produce and consume messages") { + + val key = "key" + val value = "value" + val topic = "test-topic" + for { + producer <- ZIO.service[Producer[String, String]] + consumer <- ZIO.service[Consumer[String, String]] + promise <- Promise.make[Nothing, Message[String, String]] + _ <- producer.produce(topic, key, value) + _ <- consumer + .consume(topic) { msg => + promise.succeed(msg).unit + } + .fork + receivedMessage <- promise.await + } yield assert(receivedMessage)(equalTo(Message(key, value, 1L, 0))) + }.provideLayer(testLayer), + test("should produce and consume 5 messages") { + val topic = "test-topic" + val messages = List( + ("key1", "value1"), + ("key2", "value2"), + ("key3", "value3"), + ("key4", "value4"), + ("key5", "value5") + ) + + for { + producer <- ZIO.service[Producer[String, String]] + consumer <- ZIO.service[Consumer[String, String]] + promise <- Promise.make[Nothing, List[Message[String, String]]] + ref <- Ref.make(List.empty[Message[String, String]]) + + _ <- ZIO.foreach(messages) { case (key, value) => + producer.produce(topic, key, value) *> ZIO.debug(s"Produced message: $key -> $value") + } + _ <- consumer + .consume(topic) { msg => + ZIO.debug(s"Consumed message: ${msg.key} -> ${msg.value}") *> + ref.update(_ :+ msg) *> ref.get.flatMap { msgs => + if (msgs.size == messages.size) promise.succeed(msgs).unit else ZIO.unit + } + } + .fork + receivedMessages <- promise.await + _ <- ZIO.debug(s"Received messages: ${receivedMessages.map(m => (m.key, m.value))}") + } yield assert(receivedMessages.map(m => (m.key, m.value)).sorted)( + equalTo(messages.sorted) + ) + }.provideLayer(testLayer), + ) +} diff --git a/examples/st-oid4vci/demo/requirements.txt b/examples/st-oid4vci/demo/requirements.txt index 791e544886..1fe6463e94 100644 --- a/examples/st-oid4vci/demo/requirements.txt +++ b/examples/st-oid4vci/demo/requirements.txt @@ -1,7 +1,7 @@ certifi==2024.7.4 cffi==1.16.0 charset-normalizer==3.3.2 -cryptography==42.0.8 +cryptography==43.0.1 idna==3.7 pycparser==2.22 PyJWT==2.8.0 diff --git a/infrastructure/shared/docker-compose-with-kafka.yml b/infrastructure/shared/docker-compose-with-kafka.yml new file mode 100644 index 0000000000..3e24ad6128 --- /dev/null +++ b/infrastructure/shared/docker-compose-with-kafka.yml @@ -0,0 +1,256 @@ +--- +services: + ########################## + # Database + ########################## + db: + image: postgres:13 + environment: + POSTGRES_MULTIPLE_DATABASES: "pollux,connect,agent,node_db" + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - pg_data_db:/var/lib/postgresql/data + - ./postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ./postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + ports: + - "127.0.0.1:${PG_PORT:-5432}:5432" + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-d", "agent"] + + interval: 10s + timeout: 5s + retries: 5 + + pgadmin: + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} + PGADMIN_CONFIG_SERVER_MODE: "False" + volumes: + - pgadmin:/var/lib/pgadmin + ports: + - "127.0.0.1:${PGADMIN_PORT:-5050}:80" + depends_on: + db: + condition: service_healthy + profiles: + - debug + + ########################## + # Services + ########################## + + prism-node: + image: ghcr.io/input-output-hk/prism-node:${PRISM_NODE_VERSION} + environment: + NODE_PSQL_HOST: db:5432 + NODE_REFRESH_AND_SUBMIT_PERIOD: + NODE_MOVE_SCHEDULED_TO_PENDING_PERIOD: + NODE_WALLET_MAX_TPS: + depends_on: + db: + condition: service_healthy + + vault-server: + image: hashicorp/vault:latest + # ports: + # - "8200:8200" + environment: + VAULT_ADDR: "http://0.0.0.0:8200" + VAULT_DEV_ROOT_TOKEN_ID: ${VAULT_DEV_ROOT_TOKEN_ID} + command: server -dev -dev-root-token-id=${VAULT_DEV_ROOT_TOKEN_ID} + cap_add: + - IPC_LOCK + healthcheck: + test: ["CMD", "vault", "status"] + interval: 10s + timeout: 5s + retries: 5 + + cloud-agent: + image: ghcr.io/hyperledger/identus-cloud-agent:${AGENT_VERSION} + environment: + POLLUX_DB_HOST: db + POLLUX_DB_PORT: 5432 + POLLUX_DB_NAME: pollux + POLLUX_DB_USER: postgres + POLLUX_DB_PASSWORD: postgres + CONNECT_DB_HOST: db + CONNECT_DB_PORT: 5432 + CONNECT_DB_NAME: connect + CONNECT_DB_USER: postgres + CONNECT_DB_PASSWORD: postgres + AGENT_DB_HOST: db + AGENT_DB_PORT: 5432 + AGENT_DB_NAME: agent + AGENT_DB_USER: postgres + AGENT_DB_PASSWORD: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://${DOCKERHOST}:${PORT}/cloud-agent + DIDCOMM_SERVICE_URL: http://${DOCKERHOST}:${PORT}/didcomm + REST_SERVICE_URL: http://${DOCKERHOST}:${PORT}/cloud-agent + PRISM_NODE_HOST: prism-node + PRISM_NODE_PORT: 50053 + VAULT_ADDR: ${VAULT_ADDR:-http://vault-server:8200} + VAULT_TOKEN: ${VAULT_DEV_ROOT_TOKEN_ID:-root} + SECRET_STORAGE_BACKEND: postgres + DEV_MODE: true + DEFAULT_WALLET_ENABLED: + DEFAULT_WALLET_SEED: + DEFAULT_WALLET_WEBHOOK_URL: + DEFAULT_WALLET_WEBHOOK_API_KEY: + DEFAULT_WALLET_AUTH_API_KEY: + DEFAULT_KAFKA_ENABLED: true + GLOBAL_WEBHOOK_URL: + GLOBAL_WEBHOOK_API_KEY: + WEBHOOK_PARALLELISM: + ADMIN_TOKEN: + API_KEY_SALT: + API_KEY_ENABLED: + API_KEY_AUTHENTICATE_AS_DEFAULT_USER: + API_KEY_AUTO_PROVISIONING: + depends_on: + db: + condition: service_healthy + prism-node: + condition: service_started + vault-server: + condition: service_healthy + init-kafka: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://cloud-agent:8085/_system/health"] + interval: 30s + timeout: 10s + retries: 5 + extra_hosts: + - "host.docker.internal:host-gateway" + + swagger-ui: + image: swaggerapi/swagger-ui:v5.1.0 + environment: + - 'URLS=[ + { name: "Cloud Agent", url: "/docs/cloud-agent/api/docs.yaml" } + ]' + + # apisix: + # image: apache/apisix:2.15.0-alpine + # volumes: + # - ./apisix/conf/apisix.yaml:/usr/local/apisix/conf/apisix.yaml:ro + # - ./apisix/conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro + # ports: + # - "${PORT}:9080/tcp" + # depends_on: + # - cloud-agent + # - swagger-ui + + nginx: + image: nginx:latest + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "${PORT}:80/tcp" + depends_on: + - cloud-agent + - swagger-ui + + zookeeper: + image: confluentinc/cp-zookeeper:latest + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + # ports: + # - 22181:2181 + + kafka: + image: confluentinc/cp-kafka:latest + depends_on: + - zookeeper + # ports: + # - 29092:29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: false + healthcheck: + test: + [ + "CMD", + "kafka-topics", + "--list", + "--bootstrap-server", + "localhost:9092", + ] + interval: 5s + timeout: 10s + retries: 5 + + init-kafka: + image: confluentinc/cp-kafka:latest + depends_on: + kafka: + condition: service_healthy + entrypoint: ["/bin/sh", "-c"] + command: | + " + # blocks until kafka is reachable + kafka-topics --bootstrap-server kafka:9092 --list + echo -e 'Creating kafka topics' + + # Connect + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-1 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-2 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-3 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-4 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-DLQ --replication-factor 1 --partitions 1 + + # Issue + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-DLQ --replication-factor 1 --partitions 1 + + # Present + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-DLQ --replication-factor 1 --partitions 1 + + # DID Publication State Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-did-state --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-did-state-DLQ --replication-factor 1 --partitions 5 + + # Status List Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-status-list --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-status-list-DLQ --replication-factor 1 --partitions 5 + + tail -f /dev/null + " + healthcheck: + test: + [ + "CMD-SHELL", + "kafka-topics --bootstrap-server kafka:9092 --list | grep -q 'sync-status-list'", + ] + interval: 5s + timeout: 10s + retries: 5 + +volumes: + pg_data_db: + pgadmin: +# Temporary commit network setting due to e2e CI bug +# to be enabled later after debugging +#networks: +# default: +# name: ${NETWORK} diff --git a/infrastructure/shared/nginx/nginx.conf b/infrastructure/shared/nginx/nginx.conf new file mode 100644 index 0000000000..937dc35a3f --- /dev/null +++ b/infrastructure/shared/nginx/nginx.conf @@ -0,0 +1,42 @@ +user nginx; + +events { + worker_connections 1000; +} + +http { + # Docker embedded DNS server (overriding TTL) + resolver 127.0.0.11 valid=5s; + + # Upstreams + upstream cloud_agent_8090 { + server cloud-agent:8090; + } + + upstream cloud_agent_8085 { + server cloud-agent:8085; + } + + # Server configuration + server { + listen 80; + + # Route /cloud-agent/* + location ~ ^/cloud-agent/(.*) { + # Proxy rewrite + set $upstream_servers cloud-agent; + rewrite ^/cloud-agent/(.*) /$1 break; + proxy_pass http://$upstream_servers:8085; + proxy_connect_timeout 5s; + } + + # Route /didcomm* + location ~ ^/didcomm(.*) { + # Proxy rewrite + set $upstream_servers cloud-agent; + rewrite ^/didcomm(.*) /$1 break; + proxy_pass http://$upstream_servers:8090; + proxy_connect_timeout 5s; + } + } +} \ No newline at end of file diff --git a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/CredentialPreview.scala b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/CredentialPreview.scala index 9997acdaf1..4351ca6367 100644 --- a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/CredentialPreview.scala +++ b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/CredentialPreview.scala @@ -27,14 +27,20 @@ import io.circe.generic.semiauto.* */ final case class CredentialPreview( `type`: String = "https://didcomm.org/issue-credential/3.0/credential-credential", + schema_ids: Option[List[String]] = None, schema_id: Option[String] = None, body: CredentialPreviewBody, ) object CredentialPreview { def apply(attributes: Seq[Attribute]) = new CredentialPreview(body = CredentialPreviewBody(attributes)) - def apply(schema_id: Option[String], attributes: Seq[Attribute]) = - new CredentialPreview(schema_id = schema_id, body = CredentialPreviewBody(attributes)) + def apply(schema_ids: Option[List[String]], attributes: Seq[Attribute]) = + new CredentialPreview( + schema_ids = schema_ids, + // Done for backward compatibility + schema_id = schema_ids.flatMap(s => s.headOption), + body = CredentialPreviewBody(attributes) + ) given Encoder[CredentialPreview] = deriveEncoder[CredentialPreview] given Decoder[CredentialPreview] = deriveDecoder[CredentialPreview] diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala index 47fcced892..ed18017d45 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala @@ -7,4 +7,6 @@ opaque type DidCommID = String object DidCommID: def apply(value: String): DidCommID = value def apply(): DidCommID = UUID.randomUUID.toString() - extension (id: DidCommID) def value: String = id + extension (id: DidCommID) + def value: String = id + def uuid: UUID = UUID.fromString(id) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/IssueCredentialRecord.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/IssueCredentialRecord.scala index 2a9dfce4d6..62829323d3 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/IssueCredentialRecord.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/IssueCredentialRecord.scala @@ -23,7 +23,7 @@ final case class IssueCredentialRecord( createdAt: Instant, updatedAt: Option[Instant], thid: DidCommID, - schemaUri: Option[String], + schemaUris: Option[List[String]], credentialDefinitionId: Option[UUID], credentialDefinitionUri: Option[String], credentialFormat: CredentialFormat, @@ -86,7 +86,7 @@ final case class ValidFullIssuedCredentialRecord( id: DidCommID, issuedCredential: Option[IssueCredential], credentialFormat: CredentialFormat, - schemaUri: Option[String], + schemaUris: Option[List[String]], credentialDefinitionUri: Option[String], subjectId: Option[String], keyId: Option[KeyId], diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/ResourceResolutionMethod.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/ResourceResolutionMethod.scala new file mode 100644 index 0000000000..3562e47ab5 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/ResourceResolutionMethod.scala @@ -0,0 +1,17 @@ +package org.hyperledger.identus.pollux.core.model + +import sttp.tapir.Schema +import zio.json.* + +enum ResourceResolutionMethod { + case did + case http +} + +object ResourceResolutionMethod { + given schema: Schema[ResourceResolutionMethod] = Schema.derivedEnumeration.defaultStringBased + given encoder: JsonEncoder[ResourceResolutionMethod] = JsonEncoder[String].contramap(_.toString) + given decoder: JsonDecoder[ResourceResolutionMethod] = JsonDecoder[String].mapOrFail { s => + ResourceResolutionMethod.values.find(_.toString == s).toRight(s"Unknown ResourceResolutionMethod: $s") + } +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala index e1b1acd1f2..f2eddcf151 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.core.model.error +import org.hyperledger.identus.shared.http.GenericUriResolverError import org.hyperledger.identus.shared.json.JsonSchemaError import org.hyperledger.identus.shared.models.{Failure, StatusCode} @@ -46,4 +47,10 @@ object CredentialSchemaError { StatusCode.BadRequest, s"Unsupported credential schema type: ${`type`}" ) + + final case class SchemaDereferencingError(cause: GenericUriResolverError) + extends CredentialSchemaError( + StatusCode.InternalServerError, + s"The schema was not successfully dereferenced: cause=[${cause.userFacingMessage}]" + ) } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaServiceError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaServiceError.scala index 362e2aa59d..f2d19a3d17 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaServiceError.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaServiceError.scala @@ -19,6 +19,12 @@ final case class CredentialSchemaGuidNotFoundError(guid: UUID) s"Credential Schema record cannot be found by `guid`=$guid" ) +final case class CredentialSchemaIdNotFoundError(id: UUID) + extends CredentialSchemaServiceError( + StatusCode.NotFound, + s"Credential Schema record cannot be found by `id`=$id" + ) + final case class CredentialSchemaUpdateError(id: UUID, version: String, author: String, message: String) extends CredentialSchemaServiceError( StatusCode.BadRequest, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala index c71b88ede1..4f4a1d7eef 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala @@ -1,7 +1,7 @@ package org.hyperledger.identus.pollux.core.model.error import org.hyperledger.identus.pollux.core.model.DidCommID -import org.hyperledger.identus.pollux.core.service.URIDereferencerError +import org.hyperledger.identus.shared.http.GenericUriResolverError import org.hyperledger.identus.shared.json.JsonSchemaError import org.hyperledger.identus.shared.models.{Failure, StatusCode} @@ -56,25 +56,13 @@ object PresentationError { "Issued credential not found" ) - final case class InvalidSchemaURIError(schemaUri: String, error: Throwable) - extends PresentationError( - StatusCode.BadRequest, - s"Invalid Schema Uri: $schemaUri, Error: ${error.getMessage}" - ) - - final case class InvalidCredentialDefinitionURIError(credentialDefinitionUri: String, error: Throwable) - extends PresentationError( - StatusCode.BadRequest, - s"Invalid Credential Definition Uri: $credentialDefinitionUri, Error: ${error.getMessage}" - ) - - final case class SchemaURIDereferencingError(error: URIDereferencerError) + final case class SchemaURIDereferencingError(error: GenericUriResolverError) extends PresentationError( error.statusCode, error.userFacingMessage ) - final case class CredentialDefinitionURIDereferencingError(error: URIDereferencerError) + final case class CredentialDefinitionURIDereferencingError(error: GenericUriResolverError) extends PresentationError( error.statusCode, error.userFacingMessage diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialDefinition.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialDefinition.scala index d6d97a45d6..64e715a722 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialDefinition.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialDefinition.scala @@ -1,7 +1,7 @@ package org.hyperledger.identus.pollux.core.model.schema -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod.* import zio.* import zio.json.* @@ -56,7 +56,8 @@ case class CredentialDefinition( keyCorrectnessProofJsonSchemaId: String, keyCorrectnessProof: CorrectnessProof, signatureType: String, - supportRevocation: Boolean + supportRevocation: Boolean, + resolutionMethod: ResourceResolutionMethod ) { def longId = CredentialDefinition.makeLongId(author, guid, version) } @@ -85,11 +86,12 @@ object CredentialDefinition { definitionSchemaId: String, definition: Definition, proofSchemaId: String, - proof: CorrectnessProof + proof: CorrectnessProof, + resolutionMethod: ResourceResolutionMethod ): UIO[CredentialDefinition] = { for { id <- zio.Random.nextUUID - cs <- make(id, in, definitionSchemaId, definition, proofSchemaId, proof) + cs <- make(id, in, definitionSchemaId, definition, proofSchemaId, proof, resolutionMethod) } yield cs } @@ -99,7 +101,8 @@ object CredentialDefinition { definitionSchemaId: String, definition: Definition, keyCorrectnessProofSchemaId: String, - keyCorrectnessProof: CorrectnessProof + keyCorrectnessProof: CorrectnessProof, + resolutionMethod: ResourceResolutionMethod ): UIO[CredentialDefinition] = { for { ts <- zio.Clock.currentDateTime.map( @@ -121,7 +124,8 @@ object CredentialDefinition { keyCorrectnessProofJsonSchemaId = keyCorrectnessProofSchemaId, keyCorrectnessProof = keyCorrectnessProof, signatureType = in.signatureType, - supportRevocation = in.supportRevocation + supportRevocation = in.supportRevocation, + resolutionMethod = resolutionMethod ) } @@ -143,7 +147,8 @@ object CredentialDefinition { author: Option[String] = None, name: Option[String] = None, version: Option[String] = None, - tag: Option[String] = None + tag: Option[String] = None, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) case class FilteredEntries(entries: Seq[CredentialDefinition], count: Long, totalCount: Long) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala index 809f0b1c88..092fb1043b 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala @@ -8,7 +8,8 @@ import org.hyperledger.identus.pollux.core.model.schema.`type`.{ CredentialSchemaType } import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 -import org.hyperledger.identus.pollux.core.service.URIDereferencer +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.json.{JsonSchemaValidator, JsonSchemaValidatorImpl} import zio.* import zio.json.* @@ -52,6 +53,7 @@ case class CredentialSchema( tags: Seq[String], description: String, `type`: String, + resolutionMethod: ResourceResolutionMethod, schema: Schema ) { def longId = CredentialSchema.makeLongId(author, id, version) @@ -65,13 +67,13 @@ object CredentialSchema { def makeGUID(author: String, id: UUID, version: String) = UUID.nameUUIDFromBytes(makeLongId(author, id, version).getBytes) - def make(in: Input): UIO[CredentialSchema] = { + def make(in: Input, resolutionMethod: ResourceResolutionMethod): UIO[CredentialSchema] = { for { id <- zio.Random.nextUUID - cs <- make(id, in) + cs <- make(id, in, resolutionMethod) } yield cs } - def make(id: UUID, in: Input): UIO[CredentialSchema] = { + def make(id: UUID, in: Input, resolutionMethod: ResourceResolutionMethod): UIO[CredentialSchema] = { for { ts <- zio.Clock.currentDateTime.map( _.atZoneSameInstant(ZoneOffset.UTC).toOffsetDateTime @@ -87,6 +89,7 @@ object CredentialSchema { tags = in.tags, description = in.description, `type` = in.`type`, + resolutionMethod = resolutionMethod, schema = in.schema ) } @@ -108,7 +111,8 @@ object CredentialSchema { author: Option[String] = None, name: Option[String] = None, version: Option[String] = None, - tags: Option[String] = None + tags: Option[String] = None, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) case class FilteredEntries(entries: Seq[CredentialSchema], count: Long, totalCount: Long) @@ -116,9 +120,14 @@ object CredentialSchema { given JsonEncoder[CredentialSchema] = DeriveJsonEncoder.gen[CredentialSchema] given JsonDecoder[CredentialSchema] = DeriveJsonDecoder.gen[CredentialSchema] - def resolveJWTSchema(uri: URI, uriDereferencer: URIDereferencer): IO[CredentialSchemaParsingError, Json] = { + def resolveJWTSchema( + uri: URI, + uriResolver: UriResolver + ): IO[CredentialSchemaParsingError | SchemaDereferencingError, Json] = { for { - content <- uriDereferencer.dereference(uri).orDieAsUnmanagedFailure + content <- uriResolver + .resolve(uri.toString) + .mapError(SchemaDereferencingError(_)) json <- ZIO .fromEither(content.fromJson[Json]) .mapError(error => CredentialSchemaParsingError(error)) @@ -127,11 +136,11 @@ object CredentialSchema { def validSchemaValidator( schemaId: String, - uriDereferencer: URIDereferencer - ): IO[InvalidURI | CredentialSchemaParsingError, JsonSchemaValidator] = { + uriResolver: UriResolver + ): IO[InvalidURI | CredentialSchemaParsingError | SchemaDereferencingError, JsonSchemaValidator] = { for { uri <- ZIO.attempt(new URI(schemaId)).mapError(_ => InvalidURI(schemaId)) - json <- resolveJWTSchema(uri, uriDereferencer) + json <- resolveJWTSchema(uri, uriResolver) schemaValidator <- JsonSchemaValidatorImpl .from(json) .orElse( @@ -148,10 +157,13 @@ object CredentialSchema { def validateJWTCredentialSubject( schemaId: String, credentialSubject: String, - uriDereferencer: URIDereferencer - ): IO[InvalidURI | CredentialSchemaParsingError | CredentialSchemaValidationError, Unit] = { + uriResolver: UriResolver + ): IO[ + InvalidURI | CredentialSchemaParsingError | CredentialSchemaValidationError | SchemaDereferencingError, + Unit + ] = { for { - schemaValidator <- validSchemaValidator(schemaId, uriDereferencer) + schemaValidator <- validSchemaValidator(schemaId, uriResolver) _ <- schemaValidator.validate(credentialSubject).mapError(CredentialSchemaValidationError.apply) } yield () } @@ -159,11 +171,10 @@ object CredentialSchema { def validateAnonCredsClaims( schemaId: String, claims: String, - uriDereferencer: URIDereferencer + uriResolver: UriResolver ): IO[InvalidURI | CredentialSchemaParsingError | VCClaimsParsingError | VCClaimValidationError, Unit] = { for { - uri <- ZIO.attempt(new URI(schemaId)).mapError(_ => InvalidURI(schemaId)) - content <- uriDereferencer.dereference(uri).orDieAsUnmanagedFailure + content <- uriResolver.resolve(schemaId).orDieAsUnmanagedFailure validAttrNames <- AnoncredSchemaSerDesV1.schemaSerDes .deserialize(content) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepository.scala index bcf889d2fd..49c08b7d1f 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepository.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.pollux.core.repository import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.Repository.SearchCapability import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{UIO, URIO} @@ -12,7 +13,7 @@ trait CredentialDefinitionRepository with SearchCapability[WalletTask, CredentialDefinition.Filter, CredentialDefinition] { def create(cs: CredentialDefinition): URIO[WalletAccessContext, CredentialDefinition] - def findByGuid(guid: UUID): UIO[Option[CredentialDefinition]] + def findByGuid(guid: UUID, resolutionMethod: ResourceResolutionMethod): UIO[Option[CredentialDefinition]] def update(cs: CredentialDefinition): URIO[WalletAccessContext, CredentialDefinition] diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepository.scala index 2ac6c75c7a..896ef7b971 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepository.scala @@ -79,7 +79,7 @@ trait CredentialRepository { recordId: DidCommID, issue: IssueCredential, issuedRawCredential: String, - schemaUri: Option[String], + schemaUris: Option[List[String]], credentialDefinitionUri: Option[String], protocolState: ProtocolState ): URIO[WalletAccessContext, Unit] diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialSchemaRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialSchemaRepository.scala index d3fa782ba8..dfd2938e01 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialSchemaRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialSchemaRepository.scala @@ -2,6 +2,7 @@ package org.hyperledger.identus.pollux.core.repository import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.Repository.SearchCapability import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{UIO, URIO} @@ -13,11 +14,15 @@ trait CredentialSchemaRepository with SearchCapability[WalletTask, CredentialSchema.Filter, CredentialSchema] { def create(cs: CredentialSchema): URIO[WalletAccessContext, CredentialSchema] - def findByGuid(guid: UUID): UIO[Option[CredentialSchema]] + def findByGuid(guid: UUID, resolutionMethod: ResourceResolutionMethod): UIO[Option[CredentialSchema]] def update(cs: CredentialSchema): URIO[WalletAccessContext, CredentialSchema] - def getAllVersions(id: UUID, author: String): URIO[WalletAccessContext, List[String]] + def getAllVersions( + id: UUID, + author: String, + resolutionMethod: ResourceResolutionMethod + ): URIO[WalletAccessContext, List[CredentialSchema]] def delete(guid: UUID): URIO[WalletAccessContext, CredentialSchema] diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala index 4732fc7ddd..6d8cdbb50d 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala @@ -1,30 +1,61 @@ package org.hyperledger.identus.pollux.core.repository import org.hyperledger.identus.pollux.core.model.* +import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021} +import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.{ + DecodingError, + EncodingError, + IndexOutOfBounds, + InvalidSize +} import org.hyperledger.identus.pollux.vc.jwt.Issuer -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import java.util.UUID trait CredentialStatusListRepository { - def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]] + def createStatusListVC( + jwtIssuer: Issuer, + statusListRegistryUrl: String, + id: UUID + ): IO[Throwable, String] = { + for { + bitString <- BitString.getInstance().mapError { + case InvalidSize(message) => new Throwable(message) + case EncodingError(message) => new Throwable(message) + case DecodingError(message) => new Throwable(message) + case IndexOutOfBounds(message) => new Throwable(message) + } + emptyStatusListCredential <- VCStatusList2021 + .build( + vcId = s"$statusListRegistryUrl/credential-status/$id", + revocationData = bitString, + jwtIssuer = jwtIssuer + ) + .mapError(x => new Throwable(x.msg)) + + credentialWithEmbeddedProof <- emptyStatusListCredential.toJsonWithEmbeddedProof + } yield credentialWithEmbeddedProof.spaces2 + } + + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] + + def getCredentialStatusListsWithCreds(statusListId: UUID): URIO[WalletAccessContext, CredentialStatusListWithCreds] def findById( id: UUID ): UIO[Option[CredentialStatusList]] - def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] + def incrementAndGetStatusListIndex( + jwtIssuer: Issuer, + statusListRegistryUrl: String + ): URIO[WalletAccessContext, (UUID, Int)] def existsForIssueCredentialRecordId( id: DidCommID ): URIO[WalletAccessContext, Boolean] - def createNewForTheWallet( - jwtIssuer: Issuer, - statusListRegistryUrl: String - ): URIO[WalletAccessContext, CredentialStatusList] - def allocateSpaceForCredential( issueCredentialRecordId: DidCommID, credentialStatusListId: UUID, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionService.scala index 1aecbbe7a4..d73752a670 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionService.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.core.service import org.hyperledger.identus.pollux.core.model.error.CredentialDefinitionServiceError import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{IO, ZIO} @@ -16,16 +17,20 @@ trait CredentialDefinitionService { * @return * Created instance of the Credential Definition */ - def create(in: Input): Result[CredentialDefinition] + def create( + in: Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): Result[CredentialDefinition] /** @param guid * Globally unique UUID of the credential definition * @return * The instance of the credential definition or credential service error */ - def getByGUID(guid: UUID): IO[CredentialDefinitionServiceError, CredentialDefinition] - - def delete(guid: UUID): Result[CredentialDefinition] + def getByGUID( + guid: UUID, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): IO[CredentialDefinitionServiceError, CredentialDefinition] def lookup(filter: Filter, skip: Int, limit: Int): Result[FilteredEntries] } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala index cf6a468b21..17c521bd43 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala @@ -12,13 +12,13 @@ import org.hyperledger.identus.pollux.core.model.error.{ } import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.{ CredentialSchemaParsingError, - CredentialSchemaValidationError, - InvalidURI + CredentialSchemaValidationError } import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition.{Filter, FilteredEntries} import org.hyperledger.identus.pollux.core.model.secret.CredentialDefinitionSecret +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.CredentialDefinitionRepository import org.hyperledger.identus.pollux.core.repository.Repository.SearchQuery import org.hyperledger.identus.pollux.core.service.serdes.{ @@ -26,23 +26,25 @@ import org.hyperledger.identus.pollux.core.service.serdes.{ ProofKeyCredentialDefinitionSchemaSerDesV1, PublicCredentialDefinitionSerDesV1 } +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.json.JsonSchemaError import zio.* -import java.net.URI import java.util.UUID import scala.util.Try class CredentialDefinitionServiceImpl( genericSecretStorage: GenericSecretStorage, credentialDefinitionRepository: CredentialDefinitionRepository, - uriDereferencer: URIDereferencer + uriResolver: UriResolver ) extends CredentialDefinitionService { - override def create(in: CredentialDefinition.Input): Result[CredentialDefinition] = { + override def create( + in: CredentialDefinition.Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): Result[CredentialDefinition] = { for { - uri <- ZIO.attempt(new URI(in.schemaId)).mapError(error => InvalidURI(in.schemaId)).orDieAsUnmanagedFailure - content <- uriDereferencer.dereference(uri).orDieAsUnmanagedFailure + content <- uriResolver.resolve(in.schemaId).orDieAsUnmanagedFailure anoncredSchema <- AnoncredSchemaSerDesV1.schemaSerDes .deserialize(content) .mapError(error => CredentialSchemaParsingError(error.error)) @@ -85,7 +87,8 @@ class CredentialDefinitionServiceImpl( PublicCredentialDefinitionSerDesV1.version, publicCredentialDefinitionJson, ProofKeyCredentialDefinitionSchemaSerDesV1.version, - proofKeyCredentialDefinitionJson + proofKeyCredentialDefinitionJson, + resolutionMethod ) createdCredentialDefinition <- credentialDefinitionRepository.create(cd) _ <- genericSecretStorage @@ -103,30 +106,26 @@ class CredentialDefinitionServiceImpl( case error: CredentialDefinitionCreationError => error } - override def delete(guid: UUID): Result[CredentialDefinition] = + override def getByGUID( + guid: UUID, + resolutionMethod: ResourceResolutionMethod + ): IO[CredentialDefinitionServiceError, CredentialDefinition] = { for { - existingOpt <- credentialDefinitionRepository.findByGuid(guid) - _ <- ZIO.fromOption(existingOpt).mapError(_ => CredentialDefinitionGuidNotFoundError(guid)) - result <- credentialDefinitionRepository.delete(guid) + resultOpt <- credentialDefinitionRepository.findByGuid(guid, resolutionMethod) + result <- ZIO.fromOption(resultOpt).mapError(_ => CredentialDefinitionGuidNotFoundError(guid)) } yield result + } override def lookup(filter: CredentialDefinition.Filter, skip: Int, limit: Int): Result[FilteredEntries] = { credentialDefinitionRepository .search(SearchQuery(filter, skip, limit)) .map(sr => FilteredEntries(sr.entries, sr.count.toInt, sr.totalCount.toInt)) } - - override def getByGUID(guid: UUID): IO[CredentialDefinitionServiceError, CredentialDefinition] = { - for { - resultOpt <- credentialDefinitionRepository.findByGuid(guid) - result <- ZIO.fromOption(resultOpt).mapError(_ => CredentialDefinitionGuidNotFoundError(guid)) - } yield result - } } object CredentialDefinitionServiceImpl { val layer: URLayer[ - GenericSecretStorage & CredentialDefinitionRepository & URIDereferencer, + GenericSecretStorage & CredentialDefinitionRepository & UriResolver, CredentialDefinitionService ] = ZLayer.fromFunction(CredentialDefinitionServiceImpl(_, _, _)) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaService.scala index 1fdb69d04e..922d87300f 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaService.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.core.service import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaServiceError import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{IO, ZIO} @@ -15,18 +16,26 @@ trait CredentialSchemaService { * @return * Created instance of the Credential Schema */ - def create(in: Input): Result[CredentialSchema] + def create( + in: Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): Result[CredentialSchema] /** @param guid * Globally unique UUID of the credential schema * @return * The instance of the credential schema or credential service error */ - def getByGUID(guid: UUID): IO[CredentialSchemaServiceError, CredentialSchema] + def getByGUID( + guid: UUID, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): IO[CredentialSchemaServiceError, CredentialSchema] - def update(id: UUID, in: Input): Result[CredentialSchema] - - def delete(id: UUID): Result[CredentialSchema] + def update( + id: UUID, + in: Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): Result[CredentialSchema] def lookup(filter: Filter, skip: Int, limit: Int): Result[FilteredEntries] } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaServiceImpl.scala index ec96d7d678..4854371764 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaServiceImpl.scala @@ -3,12 +3,14 @@ package org.hyperledger.identus.pollux.core.service import org.hyperledger.identus.pollux.core.model.error.{ CredentialSchemaError, CredentialSchemaGuidNotFoundError, + CredentialSchemaIdNotFoundError, CredentialSchemaServiceError, CredentialSchemaUpdateError, CredentialSchemaValidationError } import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema.FilteredEntries +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.CredentialSchemaRepository import org.hyperledger.identus.pollux.core.repository.Repository.SearchQuery import zio.* @@ -18,9 +20,12 @@ import java.util.UUID class CredentialSchemaServiceImpl( credentialSchemaRepository: CredentialSchemaRepository ) extends CredentialSchemaService { - override def create(in: CredentialSchema.Input): Result[CredentialSchema] = { + override def create( + in: CredentialSchema.Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): Result[CredentialSchema] = { for { - credentialSchema <- CredentialSchema.make(in) + credentialSchema <- CredentialSchema.make(in, resolutionMethod) _ <- CredentialSchema.validateCredentialSchema(credentialSchema) createdCredentialSchema <- credentialSchemaRepository.create(credentialSchema) } yield createdCredentialSchema @@ -28,9 +33,12 @@ class CredentialSchemaServiceImpl( CredentialSchemaValidationError(e) } - override def getByGUID(guid: UUID): IO[CredentialSchemaServiceError, CredentialSchema] = { + override def getByGUID( + guid: UUID, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): IO[CredentialSchemaServiceError, CredentialSchema] = { for { - resultOpt <- credentialSchemaRepository.findByGuid(guid) + resultOpt <- credentialSchemaRepository.findByGuid(guid, resolutionMethod) result <- ZIO.fromOption(resultOpt).mapError(_ => CredentialSchemaGuidNotFoundError(guid)) } yield result } @@ -38,25 +46,40 @@ class CredentialSchemaServiceImpl( def getBy( author: String, id: UUID, - version: String + version: String, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ): Result[CredentialSchema] = { - getByGUID(CredentialSchema.makeGUID(author, id, version)) + getByGUID(CredentialSchema.makeGUID(author, id, version), resolutionMethod) } override def update( - guid: UUID, - in: CredentialSchema.Input + id: UUID, + in: CredentialSchema.Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ): Result[CredentialSchema] = { + for { - cs <- CredentialSchema.make(guid, in) + existingVersions <- credentialSchemaRepository.getAllVersions(id, in.author, resolutionMethod) + _ <- + if existingVersions.isEmpty then + ZIO.fail( + CredentialSchemaUpdateError( + id, + in.version, + in.author, + s"No Schema exists of author: ${in.author}, with provided id: $id" + ) + ) + else ZIO.unit + resolutionMethod = existingVersions.head.resolutionMethod + cs <- CredentialSchema.make(id, in, resolutionMethod) _ <- CredentialSchema.validateCredentialSchema(cs).mapError(CredentialSchemaValidationError.apply) - existingVersions <- credentialSchemaRepository.getAllVersions(guid, in.author) - _ <- ZIO.fromOption(existingVersions.headOption).mapError(_ => CredentialSchemaGuidNotFoundError(guid)) - _ <- existingVersions.find(_ > in.version) match { + _ <- ZIO.fromOption(existingVersions.headOption).mapError(_ => CredentialSchemaIdNotFoundError(id)) + _ <- existingVersions.find(_.version > in.version) match { case Some(higherVersion) => ZIO.fail( CredentialSchemaUpdateError( - guid, + id, in.version, in.author, s"Higher version is found: $higherVersion" @@ -65,11 +88,11 @@ class CredentialSchemaServiceImpl( case None => ZIO.succeed(cs) } - _ <- existingVersions.find(_ == in.version) match { + _ <- existingVersions.find(_.version == in.version) match { case Some(existingVersion) => ZIO.fail( CredentialSchemaUpdateError( - guid, + id, in.version, in.author, s"The version already exists: $existingVersion" @@ -81,17 +104,10 @@ class CredentialSchemaServiceImpl( } yield updated } - override def delete(guid: UUID): Result[CredentialSchema] = - for { - existingOpt <- credentialSchemaRepository.findByGuid(guid) - _ <- ZIO.fromOption(existingOpt).mapError(_ => CredentialSchemaGuidNotFoundError(guid)) - result <- credentialSchemaRepository.delete(guid) - } yield result - override def lookup( filter: CredentialSchema.Filter, skip: Int, - limit: Int + limit: Int, ): Result[CredentialSchema.FilteredEntries] = { credentialSchemaRepository .search(SearchQuery(filter, skip, limit)) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala index e1341a3dbb..3414f8a351 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala @@ -27,7 +27,7 @@ trait CredentialService { pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: io.circe.Json, validityPeriod: Option[Double] = None, automaticIssuance: Option[Boolean], @@ -43,7 +43,7 @@ trait CredentialService { pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: io.circe.Json, validityPeriod: Option[Double] = None, automaticIssuance: Option[Boolean], @@ -133,7 +133,7 @@ trait CredentialService { def generateJWTCredential( recordId: DidCommID, - statusListRegistryUrl: String, + statusListRegistryServiceName: String, ): ZIO[WalletAccessContext, RecordNotFound | CredentialRequestValidationFailed, IssueCredentialRecord] def generateSDJWTCredential( diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala index ec86a7fa87..a95e80925e 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala @@ -27,7 +27,8 @@ import org.hyperledger.identus.pollux.prex.{ClaimFormat, Jwt, PresentationDefini import org.hyperledger.identus.pollux.sdjwt.* import org.hyperledger.identus.pollux.vc.jwt.{Issuer as JwtIssuer, *} import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Secp256k1KeyPair} -import org.hyperledger.identus.shared.http.{DataUrlResolver, GenericUriResolver} +import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils @@ -35,15 +36,15 @@ import zio.* import zio.json.* import zio.prelude.ZValidation -import java.net.URI import java.time.{Instant, ZoneId} import java.util.UUID import scala.language.implicitConversions object CredentialServiceImpl { val layer: URLayer[ - CredentialRepository & CredentialStatusListRepository & DidResolver & URIDereferencer & GenericSecretStorage & - CredentialDefinitionService & LinkSecretService & DIDService & ManagedDIDService, + CredentialRepository & CredentialStatusListRepository & DidResolver & UriResolver & GenericSecretStorage & + CredentialDefinitionService & LinkSecretService & DIDService & ManagedDIDService & + Producer[UUID, WalletIdAndRecordId], CredentialService ] = { ZLayer.fromZIO { @@ -51,25 +52,25 @@ object CredentialServiceImpl { credentialRepo <- ZIO.service[CredentialRepository] credentialStatusListRepo <- ZIO.service[CredentialStatusListRepository] didResolver <- ZIO.service[DidResolver] - uriDereferencer <- ZIO.service[URIDereferencer] + uriResolver <- ZIO.service[UriResolver] genericSecretStorage <- ZIO.service[GenericSecretStorage] credDefenitionService <- ZIO.service[CredentialDefinitionService] linkSecretService <- ZIO.service[LinkSecretService] didService <- ZIO.service[DIDService] manageDidService <- ZIO.service[ManagedDIDService] - issueCredentialSem <- Semaphore.make(1) + messageProducer <- ZIO.service[Producer[UUID, WalletIdAndRecordId]] } yield CredentialServiceImpl( credentialRepo, credentialStatusListRepo, didResolver, - uriDereferencer, + uriResolver, genericSecretStorage, credDefenitionService, linkSecretService, didService, manageDidService, 5, - issueCredentialSem + messageProducer ) } } @@ -82,19 +83,21 @@ class CredentialServiceImpl( credentialRepository: CredentialRepository, credentialStatusListRepository: CredentialStatusListRepository, didResolver: DidResolver, - uriDereferencer: URIDereferencer, + uriResolver: UriResolver, genericSecretStorage: GenericSecretStorage, credentialDefinitionService: CredentialDefinitionService, linkSecretService: LinkSecretService, didService: DIDService, managedDIDService: ManagedDIDService, maxRetries: Int = 5, // TODO move to config - issueCredentialSem: Semaphore + messageProducer: Producer[UUID, WalletIdAndRecordId], ) extends CredentialService { import CredentialServiceImpl.* import IssueCredentialRecord.* + private val TOPIC_NAME = "issue" + override def getIssueCredentialRecords( ignoreWithZeroRetries: Boolean, offset: Option[Int], @@ -127,7 +130,7 @@ class CredentialServiceImpl( pairwiseIssuerDID: DidId, kidIssuer: Option[KeyId], thid: DidCommID, - schemaUri: Option[String], + schemaUris: Option[List[String]], validityPeriod: Option[Double], automaticIssuance: Option[Boolean], issuingDID: Option[CanonicalPrismDID], @@ -161,7 +164,7 @@ class CredentialServiceImpl( createdAt = Instant.now, updatedAt = None, thid = thid, - schemaUri = schemaUri, + schemaUris = schemaUris, credentialDefinitionId = credentialDefinitionGUID, credentialDefinitionUri = credentialDefinitionId, credentialFormat = credentialFormat, @@ -188,6 +191,10 @@ class CredentialServiceImpl( count <- credentialRepository .create(record) @@ CustomMetricsAspect .startRecordingTime(s"${record.id}_issuer_offer_pending_to_sent_ms_gauge") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie } yield record } @@ -196,7 +203,7 @@ class CredentialServiceImpl( pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: Json, validityPeriod: Option[Double], automaticIssuance: Option[Boolean], @@ -207,12 +214,12 @@ class CredentialServiceImpl( connectionId: Option[UUID], ): URIO[WalletAccessContext, IssueCredentialRecord] = { for { - _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaId) + _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaIds) attributes <- CredentialService.convertJsonClaimsToAttributes(claims) offer <- createDidCommOfferCredential( pairwiseIssuerDID = pairwiseIssuerDID, pairwiseHolderDID = pairwiseHolderDID, - maybeSchemaId = maybeSchemaId, + maybeSchemaIds = maybeSchemaIds, claims = attributes, thid = thid, UUID.randomUUID().toString, @@ -223,7 +230,7 @@ class CredentialServiceImpl( pairwiseIssuerDID = pairwiseIssuerDID, kidIssuer = kidIssuer, thid = thid, - schemaUri = maybeSchemaId, + schemaUris = maybeSchemaIds, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance, issuingDID = Some(issuingDID), @@ -244,7 +251,7 @@ class CredentialServiceImpl( pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: io.circe.Json, validityPeriod: Option[Double] = None, automaticIssuance: Option[Boolean], @@ -255,12 +262,12 @@ class CredentialServiceImpl( connectionId: Option[UUID], ): URIO[WalletAccessContext, IssueCredentialRecord] = { for { - _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaId) + _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaIds) attributes <- CredentialService.convertJsonClaimsToAttributes(claims) offer <- createDidCommOfferCredential( pairwiseIssuerDID = pairwiseIssuerDID, pairwiseHolderDID = pairwiseHolderDID, - maybeSchemaId = maybeSchemaId, + maybeSchemaIds = maybeSchemaIds, claims = attributes, thid = thid, UUID.randomUUID().toString, @@ -271,7 +278,7 @@ class CredentialServiceImpl( pairwiseIssuerDID = pairwiseIssuerDID, kidIssuer = kidIssuer, thid = thid, - schemaUri = maybeSchemaId, + schemaUris = maybeSchemaIds, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance, issuingDID = Some(issuingDID), @@ -304,7 +311,11 @@ class CredentialServiceImpl( for { credentialDefinition <- getCredentialDefinition(credentialDefinitionGUID) _ <- CredentialSchema - .validateAnonCredsClaims(credentialDefinition.schemaId, claims.noSpaces, uriDereferencer) + .validateAnonCredsClaims( + credentialDefinition.schemaId, + claims.noSpaces, + uriResolver, + ) .orDieAsUnmanagedFailure attributes <- CredentialService.convertJsonClaimsToAttributes(claims) offer <- createAnonCredsDidCommOfferCredential( @@ -320,7 +331,7 @@ class CredentialServiceImpl( pairwiseIssuerDID = pairwiseIssuerDID, kidIssuer = None, thid = thid, - schemaUri = Some(credentialDefinition.schemaId), + schemaUris = Some(List(credentialDefinition.schemaId)), validityPeriod = validityPeriod, automaticIssuance = automaticIssuance, issuingDID = None, @@ -375,7 +386,7 @@ class CredentialServiceImpl( createdAt = Instant.now, updatedAt = None, thid = DidCommID(offer.thid.getOrElse(offer.id)), - schemaUri = None, + schemaUris = None, credentialDefinitionId = None, credentialDefinitionUri = None, credentialFormat = credentialFormat, @@ -438,12 +449,19 @@ class CredentialServiceImpl( private[this] def validateClaimsAgainstSchemaIfAny( claims: Json, - maybeSchemaId: Option[String] - ): UIO[Unit] = maybeSchemaId match - case Some(schemaId) => - CredentialSchema - .validateJWTCredentialSubject(schemaId, claims.noSpaces, uriDereferencer) - .orDieAsUnmanagedFailure + maybeSchemaIds: Option[List[String]] + ): UIO[Unit] = maybeSchemaIds match + case Some(schemaIds) => + for { + _ <- ZIO + .collectAll( + schemaIds.map(schemaId => + CredentialSchema + .validateJWTCredentialSubject(schemaId, claims.noSpaces, uriResolver) + ) + ) + .orDieAsUnmanagedFailure + } yield ZIO.unit case None => ZIO.unit @@ -490,6 +508,10 @@ class CredentialServiceImpl( ) case (format, maybeSubjectId) => ZIO.dieMessage(s"Invalid subjectId input for $format offer acceptance: $maybeSubjectId") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -651,6 +673,10 @@ class CredentialServiceImpl( s"${record.id}_issuance_flow_holder_req_pending_to_generated", "issuance_flow_holder_req_pending_to_generated_ms_gauge" ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -697,6 +723,10 @@ class CredentialServiceImpl( s"${record.id}_issuance_flow_holder_req_pending_to_generated", "issuance_flow_holder_req_pending_to_generated_ms_gauge" ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -717,8 +747,8 @@ class CredentialServiceImpl( ) .orDieWith(_ => RuntimeException(s"No AnonCreds attachment found in the offer")) credentialOffer = anoncreds.AnoncredCredentialOffer(attachmentData) - credDefContent <- uriDereferencer - .dereference(new URI(credentialOffer.getCredDefId)) + credDefContent <- uriResolver + .resolve(credentialOffer.getCredDefId) .orDieAsUnmanagedFailure credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent) linkSecret <- linkSecretService.fetchOrCreate() @@ -741,6 +771,10 @@ class CredentialServiceImpl( ProtocolState.OfferSent ) _ <- credentialRepository.updateWithJWTRequestCredential(record.id, request, ProtocolState.RequestReceived) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -759,6 +793,10 @@ class CredentialServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_issuance_flow_issuer_credential_pending_to_generated" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -806,7 +844,7 @@ class CredentialServiceImpl( processedIssuedCredential, record, attachment, - Some(processedCredential.getSchemaId), + Some(List(processedCredential.getSchemaId)), Some(processedCredential.getCredDefId) ) } yield result @@ -822,7 +860,7 @@ class CredentialServiceImpl( issueCredential: IssueCredential, record: IssueCredentialRecord, attachment: AttachmentDescriptor, - schemaId: Option[String], + schemaId: Option[List[String]], credDefId: Option[String] ) = { credentialRepository @@ -842,8 +880,8 @@ class CredentialServiceImpl( ): URIO[WalletAccessContext, anoncreds.AnoncredCredential] = { for { credential <- ZIO.succeed(anoncreds.AnoncredCredential(new String(credentialBytes))) - credDefContent <- uriDereferencer - .dereference(new URI(credential.getCredDefId)) + credDefContent <- uriResolver + .resolve(credential.getCredDefId) .orDieAsUnmanagedFailure credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent) metadata <- ZIO @@ -903,6 +941,10 @@ class CredentialServiceImpl( s"${record.id}_issuance_flow_issuer_credential_pending_to_generated", "issuance_flow_issuer_credential_pending_to_generated_ms_gauge" ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_issuer_credential_generated_to_sent") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -957,7 +999,7 @@ class CredentialServiceImpl( private def createDidCommOfferCredential( pairwiseIssuerDID: DidId, pairwiseHolderDID: Option[DidId], - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: Seq[Attribute], thid: DidCommID, challenge: String, @@ -965,7 +1007,7 @@ class CredentialServiceImpl( offerFormat: IssueCredentialOfferFormat ): UIO[OfferCredential] = { for { - credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = maybeSchemaId, attributes = claims)) + credentialPreview <- ZIO.succeed(CredentialPreview(schema_ids = maybeSchemaIds, attributes = claims)) body = OfferCredential.Body( goal_code = Some("Offer Credential"), credential_preview = credentialPreview, @@ -1001,7 +1043,7 @@ class CredentialServiceImpl( thid: DidCommID ): URIO[WalletAccessContext, OfferCredential] = { for { - credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = Some(schemaUri), attributes = claims)) + credentialPreview <- ZIO.succeed(CredentialPreview(schema_ids = Some(List(schemaUri)), attributes = claims)) body = OfferCredential.Body( goal_code = Some("Offer Credential"), credential_preview = credentialPreview, @@ -1140,11 +1182,11 @@ class CredentialServiceImpl( maybeId = None, `type` = Set("VerifiableCredential"), // TODO: This information should come from Schema registry by record.schemaId - issuer = Right(CredentialIssuer(jwtIssuer.did.toString, `type` = "Profile")), + issuer = CredentialIssuer(jwtIssuer.did.toString, `type` = "Profile"), issuanceDate = issuanceDate, maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)), - maybeCredentialSchema = record.schemaUri.map(id => - Left(org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE)) + maybeCredentialSchema = record.schemaUris.map(ids => + ids.map(id => org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE)) ), maybeCredentialStatus = Some(credentialStatus), credentialSubject = claims.add("id", jwtPresentation.iss.asJson).asJson, @@ -1265,32 +1307,26 @@ class CredentialServiceImpl( record: IssueCredentialRecord, statusListRegistryUrl: String, jwtIssuer: JwtIssuer - ): URIO[WalletAccessContext, CredentialStatus] = { - val effect = for { - lastStatusList <- credentialStatusListRepository.getLatestOfTheWallet - currentStatusList <- lastStatusList - .fold(credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl))( - ZIO.succeed(_) - ) - size = currentStatusList.size - lastUsedIndex = currentStatusList.lastUsedIndex - statusListToBeUsed <- - if lastUsedIndex < size then ZIO.succeed(currentStatusList) - else credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl) + ): URIO[WalletAccessContext, CredentialStatus] = + for { + cslAndIndex <- credentialStatusListRepository.incrementAndGetStatusListIndex( + jwtIssuer, + statusListRegistryUrl + ) + statusListId = cslAndIndex._1 + indexInStatusList = cslAndIndex._2 _ <- credentialStatusListRepository.allocateSpaceForCredential( issueCredentialRecordId = record.id, - credentialStatusListId = statusListToBeUsed.id, - statusListIndex = statusListToBeUsed.lastUsedIndex + 1 + credentialStatusListId = statusListId, + statusListIndex = indexInStatusList ) } yield CredentialStatus( - id = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}#${statusListToBeUsed.lastUsedIndex + 1}", + id = s"$statusListRegistryUrl/credential-status/$statusListId#$indexInStatusList", `type` = "StatusList2021Entry", statusPurpose = StatusPurpose.Revocation, - statusListIndex = lastUsedIndex + 1, - statusListCredential = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}" + statusListIndex = indexInStatusList, + statusListCredential = s"$statusListRegistryUrl/credential-status/$statusListId" ) - issueCredentialSem.withPermit(effect) - } override def generateAnonCredsCredential( recordId: DidCommID @@ -1425,12 +1461,6 @@ class CredentialServiceImpl( ZIO.fail(CredentialRequestValidationFailed("domain/challenge proof validation failed")) clock = java.time.Clock.system(ZoneId.systemDefault) - - genericUriResolver = GenericUriResolver( - Map( - "data" -> DataUrlResolver(), - ) - ) verificationResult <- JwtPresentation .verify( jwt, @@ -1440,7 +1470,7 @@ class CredentialServiceImpl( verifyDates = false, leeway = Duration.Zero ) - )(didResolver, genericUriResolver)(clock) + )(didResolver, uriResolver)(clock) .mapError(errors => CredentialRequestValidationFailed(errors*)) result <- verificationResult match diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala index 5046688d45..500fdf4c29 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala @@ -28,7 +28,7 @@ class CredentialServiceNotifier( pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: Json, validityPeriod: Option[Double], automaticIssuance: Option[Boolean], @@ -44,7 +44,7 @@ class CredentialServiceNotifier( pairwiseHolderDID, kidIssuer, thid, - maybeSchemaId, + maybeSchemaIds, claims, validityPeriod, automaticIssuance, @@ -61,7 +61,7 @@ class CredentialServiceNotifier( pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: io.circe.Json, validityPeriod: Option[Double] = None, automaticIssuance: Option[Boolean], @@ -77,7 +77,7 @@ class CredentialServiceNotifier( pairwiseHolderDID, kidIssuer, thid, - maybeSchemaId, + maybeSchemaIds, claims, validityPeriod, automaticIssuance, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala index 5a186d2826..418b3faa0c 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala @@ -6,7 +6,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialStatusListServi StatusListNotFound, StatusListNotFoundForIssueCredentialRecord } -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import java.util.UUID @@ -20,7 +20,9 @@ trait CredentialStatusListService { id: DidCommID ): ZIO[WalletAccessContext, StatusListNotFoundForIssueCredentialRecord | InvalidRoleForOperation, Unit] - def getCredentialsAndItsStatuses: UIO[Seq[CredentialStatusListWithCreds]] + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] + + def getCredentialStatusListWithCreds(statusListId: UUID): URIO[WalletAccessContext, CredentialStatusListWithCreds] def updateStatusListCredential( id: UUID, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala index 92565a8559..ef752f8648 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala @@ -8,7 +8,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialStatusListServi } import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.Role import org.hyperledger.identus.pollux.core.repository.CredentialStatusListRepository -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import java.util.UUID @@ -18,8 +18,11 @@ class CredentialStatusListServiceImpl( credentialStatusListRepository: CredentialStatusListRepository, ) extends CredentialStatusListService { - def getCredentialsAndItsStatuses: UIO[Seq[CredentialStatusListWithCreds]] = - credentialStatusListRepository.getCredentialStatusListsWithCreds + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] = + credentialStatusListRepository.getCredentialStatusListIds + + def getCredentialStatusListWithCreds(statusListId: UUID): URIO[WalletAccessContext, CredentialStatusListWithCreds] = + credentialStatusListRepository.getCredentialStatusListsWithCreds(statusListId) def getById(id: UUID): IO[StatusListNotFound, CredentialStatusList] = for { diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala new file mode 100644 index 0000000000..4a0113d0ad --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala @@ -0,0 +1,29 @@ +package org.hyperledger.identus.pollux.core.service + +import org.hyperledger.identus.pollux.core.service.uriResolvers.* +import org.hyperledger.identus.pollux.vc.jwt.DidResolver +import org.hyperledger.identus.shared.http.{DataUrlResolver, GenericUriResolver, GenericUriResolverError, UriResolver} +import zio.* +import zio.http.* + +class GenericUriResolverImpl(client: Client, didResolver: DidResolver) extends UriResolver { + private val httpUrlResolver = HttpUrlResolver(client) + private val genericUriResolver = new GenericUriResolver( + Map( + "http" -> httpUrlResolver, + "https" -> httpUrlResolver, + "data" -> DataUrlResolver(), + "resource" -> ResourceUrlResolver(Map.empty), + "did" -> DidUrlResolver(httpUrlResolver, didResolver) + ) + ) + override def resolve(uri: String): IO[GenericUriResolverError, String] = { + + genericUriResolver.resolve(uri) + } + +} + +object GenericUriResolverImpl { + val layer: URLayer[Client & DidResolver, UriResolver] = ZLayer.fromFunction(GenericUriResolverImpl(_, _)) +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/HttpURIDereferencerImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/HttpURIDereferencerImpl.scala deleted file mode 100644 index b38aaaf8ba..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/HttpURIDereferencerImpl.scala +++ /dev/null @@ -1,41 +0,0 @@ -package org.hyperledger.identus.pollux.core.service - -import org.hyperledger.identus.pollux.core.service.URIDereferencerError.* -import zio.* -import zio.http.* - -import java.net.URI -import java.nio.charset.StandardCharsets - -class HttpURIDereferencerImpl(client: Client) extends URIDereferencer { - - override def dereference(uri: URI): IO[URIDereferencerError, String] = { - val program = for { - url <- ZIO.fromOption(URL.fromURI(uri)).mapError(_ => InvalidURI(uri)) - response <- client - .request(Request(url = url)) - .mapError(t => ConnectionError(t.getMessage)) - body <- response.status match { - case Status.Ok => - response.body.asString.mapError(t => ResponseProcessingError(t.getMessage)) - case Status.NotFound => - ZIO.fail(ResourceNotFound(uri)) - case status if status.isError => - response.body.asStream - .take(1024) // Only take the first 1024 bytes from the response body (if any). - .runCollect - .map(c => new String(c.toArray, StandardCharsets.UTF_8)) - .orDie - .flatMap(errorMessage => ZIO.fail(UnexpectedUpstreamResponseReceived(status.code, Some(errorMessage)))) - case status => - ZIO.fail(UnexpectedUpstreamResponseReceived(status.code)) - } - } yield body - program.provideSomeLayer(zio.Scope.default) - } - -} - -object HttpURIDereferencerImpl { - val layer: URLayer[Client, URIDereferencer] = ZLayer.fromFunction(HttpURIDereferencerImpl(_)) -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala index 9fe6e171fa..e9be6a2cb5 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala @@ -1,17 +1,23 @@ package org.hyperledger.identus.pollux.core.service -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.{CredentialSchemaParsingError, InvalidURI} +import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.{ + CredentialSchemaParsingError, + InvalidURI, + SchemaDereferencingError +} import org.hyperledger.identus.pollux.core.model.oid4vci.{CredentialConfiguration, CredentialIssuer} import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.model.CredentialFormat import org.hyperledger.identus.pollux.core.repository.OID4VCIIssuerMetadataRepository import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataServiceError.{ CredentialConfigurationNotFound, + DuplicateCredentialConfigId, InvalidSchemaId, IssuerIdNotFound, UnsupportedCredentialFormat } import org.hyperledger.identus.shared.db.Errors.UnexpectedAffectedRow +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.{Failure, StatusCode, WalletAccessContext} import zio.* @@ -44,6 +50,12 @@ object OID4VCIIssuerMetadataServiceError { s"Invalid schemaId $schemaId. $msg" ) + final case class DuplicateCredentialConfigId(id: String) + extends OID4VCIIssuerMetadataServiceError( + StatusCode.Conflict, + s"Duplicated credential configuration id: $id" + ) + final case class UnsupportedCredentialFormat(format: CredentialFormat) extends OID4VCIIssuerMetadataServiceError( StatusCode.BadRequest, @@ -67,7 +79,11 @@ trait OID4VCIIssuerMetadataService { format: CredentialFormat, configurationId: String, schemaId: String - ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] + ): ZIO[ + WalletAccessContext, + InvalidSchemaId | UnsupportedCredentialFormat | IssuerIdNotFound | DuplicateCredentialConfigId, + CredentialConfiguration + ] def getCredentialConfigurations( issuerId: UUID ): IO[IssuerIdNotFound, Seq[CredentialConfiguration]] @@ -81,7 +97,7 @@ trait OID4VCIIssuerMetadataService { ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, Unit] } -class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataRepository, uriDereferencer: URIDereferencer) +class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataRepository, uriResolver: UriResolver) extends OID4VCIIssuerMetadataService { override def createCredentialIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, CredentialIssuer] = @@ -127,17 +143,25 @@ class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataReposito format: CredentialFormat, configurationId: String, schemaId: String - ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] = { + ): ZIO[ + WalletAccessContext, + InvalidSchemaId | UnsupportedCredentialFormat | IssuerIdNotFound | DuplicateCredentialConfigId, + CredentialConfiguration + ] = { for { + _ <- getCredentialIssuer(issuerId) + _ <- getCredentialConfigurationById(issuerId, configurationId).flip + .mapError(_ => DuplicateCredentialConfigId(configurationId)) _ <- format match { case CredentialFormat.JWT => ZIO.unit case f => ZIO.fail(UnsupportedCredentialFormat(f)) } schemaUri <- ZIO.attempt(new URI(schemaId)).mapError(t => InvalidSchemaId(schemaId, t.getMessage)) _ <- CredentialSchema - .validSchemaValidator(schemaUri.toString(), uriDereferencer) + .validSchemaValidator(schemaUri.toString(), uriResolver) .catchAll { case e: InvalidURI => ZIO.fail(InvalidSchemaId(schemaId, e.userFacingMessage)) + case e: SchemaDereferencingError => ZIO.fail(InvalidSchemaId(schemaId, e.userFacingMessage)) case e: CredentialSchemaParsingError => ZIO.fail(InvalidSchemaId(schemaId, e.cause)) } now <- ZIO.clockWith(_.instant) @@ -179,7 +203,7 @@ class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataReposito } object OID4VCIIssuerMetadataServiceImpl { - def layer: URLayer[OID4VCIIssuerMetadataRepository & URIDereferencer, OID4VCIIssuerMetadataService] = { + def layer: URLayer[OID4VCIIssuerMetadataRepository & UriResolver, OID4VCIIssuerMetadataService] = { ZLayer.fromFunction(OID4VCIIssuerMetadataServiceImpl(_, _)) } } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala index 94aa1af79b..20426ec477 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala @@ -105,7 +105,7 @@ trait PresentationService { def findPresentationRecord( recordId: DidCommID - ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] + ): URIO[WalletAccessContext, Option[PresentationRecord]] def findPresentationRecordByThreadId( thid: DidCommID diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala index 67d68b928c..3d18a25bbe 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala @@ -19,13 +19,14 @@ import org.hyperledger.identus.pollux.core.repository.{CredentialRepository, Pre import org.hyperledger.identus.pollux.core.service.serdes.* import org.hyperledger.identus.pollux.sdjwt.{CredentialCompact, HolderPrivateKey, PresentationCompact, SDJWT} import org.hyperledger.identus.pollux.vc.jwt.* +import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils import zio.* import zio.json.* -import java.net.URI import java.time.Instant import java.util.{Base64 as JBase64, UUID} import java.util as ju @@ -33,15 +34,18 @@ import scala.util.chaining.* import scala.util.Try private class PresentationServiceImpl( - uriDereferencer: URIDereferencer, + uriResolver: UriResolver, linkSecretService: LinkSecretService, presentationRepository: PresentationRepository, credentialRepository: CredentialRepository, - maxRetries: Int = 5, // TODO move to config + messageProducer: Producer[UUID, WalletIdAndRecordId], + maxRetries: Int = 5, // TODO move to config, ) extends PresentationService { import PresentationRecord.* + private val TOPIC_NAME = "present" + override def markPresentationGenerated( recordId: DidCommID, presentation: Presentation @@ -57,6 +61,10 @@ private class PresentationServiceImpl( ) @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_generated_to_sent_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -239,7 +247,7 @@ private class PresentationServiceImpl( ) presentationPayload <- createAnoncredPresentationPayloadFromCredential( issuedCredentials, - issuedValidCredentials.flatMap(_.schemaUri), + issuedValidCredentials.flatMap(_.schemaUris.getOrElse(List())), issuedValidCredentials.flatMap(_.credentialDefinitionUri), requestPresentation, anoncredCredentialProof.credentialProofs @@ -298,7 +306,7 @@ private class PresentationServiceImpl( override def findPresentationRecord( recordId: DidCommID - ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] = + ): URIO[WalletAccessContext, Option[PresentationRecord]] = presentationRepository.findPresentationRecord(recordId) override def findPresentationRecordByThreadId( @@ -459,6 +467,10 @@ private class PresentationServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_verifier_req_pending_to_sent_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie } yield record } @@ -531,6 +543,10 @@ private class PresentationServiceImpl( ) _ <- presentationRepository.createPresentationRecord(record) _ <- ZIO.logDebug(s"Received and created the RequestPresentation: $request") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie } yield record } @@ -765,8 +781,7 @@ private class PresentationServiceImpl( private def resolveSchema(schemaUri: String): IO[PresentationError, (String, AnoncredSchemaDef)] = { for { - uri <- ZIO.attempt(new URI(schemaUri)).mapError(e => InvalidSchemaURIError(schemaUri, e)) - content <- uriDereferencer.dereference(uri).mapError(e => SchemaURIDereferencingError(e)) + content <- uriResolver.resolve(schemaUri).mapError(e => SchemaURIDereferencingError(e)) anoncredSchema <- AnoncredSchemaSerDesV1.schemaSerDes .deserialize(content) @@ -785,10 +800,9 @@ private class PresentationServiceImpl( credentialDefinitionUri: String ): IO[PresentationError, (String, AnoncredCredentialDefinition)] = { for { - uri <- ZIO - .attempt(new URI(credentialDefinitionUri)) - .mapError(e => InvalidCredentialDefinitionURIError(credentialDefinitionUri, e)) - content <- uriDereferencer.dereference(uri).mapError(e => CredentialDefinitionURIDereferencingError(e)) + content <- uriResolver + .resolve(credentialDefinitionUri) + .mapError(e => CredentialDefinitionURIDereferencingError(e)) _ <- PublicCredentialDefinitionSerDesV1.schemaSerDes .validate(content) @@ -815,6 +829,10 @@ private class PresentationServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -843,6 +861,10 @@ private class PresentationServiceImpl( ) @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -877,6 +899,10 @@ private class PresentationServiceImpl( ) @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(record.id) } yield record } @@ -963,6 +989,10 @@ private class PresentationServiceImpl( .startRecordingTime( s"${record.id}_present_proof_flow_verifier_presentation_received_to_verification_success_or_failure_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(record.id) } yield record } @@ -979,6 +1009,10 @@ private class PresentationServiceImpl( requestPresentation = createDidCommRequestPresentationFromProposal(request) _ <- presentationRepository .updateWithRequestPresentation(recordId, requestPresentation, ProtocolState.PresentationPending) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -995,6 +1029,10 @@ private class PresentationServiceImpl( record <- getRecordFromThreadId(thid) _ <- presentationRepository .updateWithProposePresentation(record.id, proposePresentation, ProtocolState.ProposalReceived) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(record.id) } yield record } @@ -1307,8 +1345,9 @@ private class PresentationServiceImpl( object PresentationServiceImpl { val layer: URLayer[ - URIDereferencer & LinkSecretService & PresentationRepository & CredentialRepository, + UriResolver & LinkSecretService & PresentationRepository & CredentialRepository & + Producer[UUID, WalletIdAndRecordId], PresentationService ] = - ZLayer.fromFunction(PresentationServiceImpl(_, _, _, _)) + ZLayer.fromFunction(PresentationServiceImpl(_, _, _, _, _)) } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala index 80d358cdbf..350c161100 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala @@ -14,6 +14,7 @@ import org.hyperledger.identus.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCr import org.hyperledger.identus.shared.models.* import zio.* import zio.json.* +import zio.URIO import java.time.Instant import java.util.UUID @@ -275,7 +276,7 @@ class PresentationServiceNotifier( override def findPresentationRecord( recordId: DidCommID - ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] = + ): URIO[WalletAccessContext, Option[PresentationRecord]] = svc.findPresentationRecord(recordId) override def findPresentationRecordByThreadId( diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/ResourceURIDereferencerImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/ResourceURIDereferencerImpl.scala deleted file mode 100644 index 7b79ed8f2c..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/ResourceURIDereferencerImpl.scala +++ /dev/null @@ -1,37 +0,0 @@ -package org.hyperledger.identus.pollux.core.service - -import org.hyperledger.identus.pollux.core.service.URIDereferencerError.ResourceNotFound -import zio.* - -import java.net.URI - -class ResourceURIDereferencerImpl(extraResources: Map[String, String]) extends URIDereferencer { - - override def dereference(uri: URI): IO[URIDereferencerError, String] = { - for { - scheme <- ZIO.succeed(uri.getScheme) - body <- scheme match - case "resource" => - val inputStream = this.getClass.getResourceAsStream(uri.getPath) - if (inputStream != null) - val content = scala.io.Source.fromInputStream(inputStream).mkString - inputStream.close() - ZIO.succeed(content) - else ZIO.fail(ResourceNotFound(uri)) - case _ => - extraResources - .get(uri.toString) - .map(ZIO.succeed(_)) - .getOrElse(ZIO.fail(ResourceNotFound(uri))) - } yield body - } - -} - -object ResourceURIDereferencerImpl { - def layer: ULayer[ResourceURIDereferencerImpl] = - ZLayer.succeed(new ResourceURIDereferencerImpl(Map.empty)) - - def layerWithExtraResources: URLayer[Map[String, String], ResourceURIDereferencerImpl] = - ZLayer.fromFunction(ResourceURIDereferencerImpl(_)) -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/URIDereferencer.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/URIDereferencer.scala deleted file mode 100644 index fa80836744..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/URIDereferencer.scala +++ /dev/null @@ -1,49 +0,0 @@ -package org.hyperledger.identus.pollux.core.service - -import org.hyperledger.identus.shared.models.{Failure, StatusCode} -import zio.IO - -import java.net.URI - -trait URIDereferencer { - def dereference(uri: URI): IO[URIDereferencerError, String] -} - -sealed trait URIDereferencerError( - val statusCode: StatusCode, - val userFacingMessage: String -) extends Failure { - override val namespace: String = "URIDereferencer" -} - -object URIDereferencerError { - final case class InvalidURI(uri: URI) - extends URIDereferencerError( - StatusCode.UnprocessableContent, - s"The URI to dereference is invalid: uri=[$uri]" - ) - - final case class ConnectionError(cause: String) - extends URIDereferencerError( - StatusCode.BadGateway, - s"An error occurred while connecting to the URI's underlying server: cause=[$cause]" - ) - - final case class ResourceNotFound(uri: URI) - extends URIDereferencerError( - StatusCode.NotFound, - s"The resource was not found on the URI's underlying server: uri=[$uri]" - ) - - final case class ResponseProcessingError(cause: String) - extends URIDereferencerError( - StatusCode.BadGateway, - s"An error occurred while processing the URI's underlying server response: cause=[$cause]" - ) - - final case class UnexpectedUpstreamResponseReceived(status: Int, content: Option[String] = None) - extends URIDereferencerError( - StatusCode.BadGateway, - s"An unexpected response was received from the URI's underlying server: status=[$status], content=[${content.getOrElse("n/a")}]" - ) -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala new file mode 100644 index 0000000000..8e3f779ce5 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala @@ -0,0 +1,131 @@ +package org.hyperledger.identus.pollux.core.service.uriResolvers + +import io.lemonlabs.uri.{Url, UrlPath} +import org.hyperledger.identus.pollux.vc.jwt +import org.hyperledger.identus.pollux.vc.jwt.* +import org.hyperledger.identus.shared.crypto.Sha256Hash +import org.hyperledger.identus.shared.http.{GenericUriResolverError, UriResolver} +import org.hyperledger.identus.shared.models.{PrismEnvelopeData, StatusCode} +import zio.* +import zio.json.* + +class DidUrlResolver(httpUrlResolver: HttpUrlResolver, didResolver: DidResolver) extends UriResolver { + import DidUrlResolver.* + + def resolve(uri: String): IO[GenericUriResolverError, String] = { + + for { + parsed <- ZIO.fromTry(Url.parseTry(uri)).mapError(_ => InvalidURI(uri)) + maybeResourceService = parsed.query.param("resourceService") + maybeResourcePath = parsed.query.param("resourcePath") + maybeResourceHash = parsed.query.param("resourceHash") + serviceAndPath <- ZIO + .fromOption(maybeResourceService zip maybeResourcePath) + .mapError(_ => MissingRequiredParams(uri)) + (resourceService, resourcePath) = serviceAndPath + didStr = parsed.removeQueryString().toString + didDocument <- didResolver.resolve(didStr).flatMap { + case DIDResolutionFailed(err) => + err match + case InvalidDid(message) => ZIO.fail(DidResolutionError(didStr, message)) + case NotFound(message) => ZIO.fail(DidResolutionError(didStr, message)) + case RepresentationNotSupported(message) => ZIO.fail(DidResolutionError(didStr, message)) + case InvalidPublicKeyLength(message) => ZIO.fail(DidResolutionError(didStr, message)) + case InvalidPublicKeyType(message) => ZIO.fail(DidResolutionError(didStr, message)) + case UnsupportedPublicKeyType(message) => ZIO.fail(DidResolutionError(didStr, message)) + case jwt.Error(error, message) => ZIO.fail(DidResolutionError(didStr, message)) + case DIDResolutionSucceeded(didDocument, didDocumentMetadata) => ZIO.succeed(didDocument) + } + service <- ZIO + .fromOption( + didDocument.service.find(x => x.id == s"$didStr#$resourceService" && x.`type` == "LinkedResourceV1") + ) + .mapError(_ => + DidDocumentParsingError( + s"""Service with id: "$resourceService" and type: "LinkedResourceV1" not found inside DID document""" + ) + ) + baseUrl <- ZIO + .fromOption(service.serviceEndpoint.asString) + .mapError(_ => DidDocumentParsingError("serviceEndpoint is not a string")) + + path <- ZIO.fromOption(UrlPath.parseOption(resourcePath)).mapError(_ => InvalidUrlPath(resourcePath)) + finalUrl <- ZIO + .fromTry(Url.parseTry(baseUrl).map(x => x.withPath(path)).map(_.toString)) + .mapError(_ => InvalidURI(baseUrl)) + result <- httpUrlResolver.resolve(finalUrl) + + validatedResult <- result.fromJson[PrismEnvelopeData] match { + case Right(env) => validateResourceIntegrity(env, maybeResourceHash) + case Left(err) => + ZIO.debug("Error parsing response as PrismEnvelope. Falling back to plain json") *> ZIO.succeed(result) + } + + } yield validatedResult + + } + + private def validateResourceIntegrity( + envelope: PrismEnvelopeData, + maybeResourceHash: Option[String] + ): IO[DidUrlResolverError, String] = { + val envelopeAsStr = envelope.toJson + maybeResourceHash.fold(ZIO.succeed(envelopeAsStr)) { hash => + val computedHash = Sha256Hash.compute(envelope.resource.getBytes()).hexEncoded + if (computedHash == hash) ZIO.succeed(envelopeAsStr) + else ZIO.fail(InvalidHash(hash, computedHash)) + } + } + +} + +object DidUrlResolver { + + class DidUrlResolverError(statusCode: StatusCode, userFacingMessage: String) + extends GenericUriResolverError(statusCode, userFacingMessage) + + final case class InvalidURI(uri: String) + extends DidUrlResolverError( + StatusCode.UnprocessableContent, + s"The URI to resolve is invalid: uri=[$uri]" + ) + + final case class InvalidUrlPath(path: String) + extends DidUrlResolverError( + StatusCode.UnprocessableContent, + s"Invalid URL path: $path" + ) + + final case class MissingRequiredParams(url: String) + extends DidUrlResolverError( + StatusCode.UnprocessableContent, + s"DID URL must have resourcePath and resourceService query parameters, got invalid URL: $url" + ) + + final case class DidResolutionError(didStr: String, reason: String) + extends DidUrlResolverError( + StatusCode.InternalServerError, + s"Error resolving DID: $didStr, error: $reason" + ) + + final case class DidDocumentParsingError(customMessage: String) + extends DidUrlResolverError( + StatusCode.InternalServerError, + s"Error parsing DID document: $customMessage" + ) + + final case class InvalidHash(expectedHash: String, computedHash: String) + extends DidUrlResolverError( + StatusCode.UnprocessableContent, + s"Invalid hash, expected: $expectedHash, computed: $computedHash" + ) + + final case class InvalidResponseFromResourceServer(resourceUrl: String) + extends DidUrlResolverError( + StatusCode.UnprocessableContent, + s"Invalid Json response, expected an envelope, resourceUrl: $resourceUrl" + ) + + val layer: URLayer[HttpUrlResolver & DidResolver, DidUrlResolver] = + ZLayer.fromFunction(DidUrlResolver(_, _)) +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/HttpUrlResolver.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/HttpUrlResolver.scala new file mode 100644 index 0000000000..535311b648 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/HttpUrlResolver.scala @@ -0,0 +1,75 @@ +package org.hyperledger.identus.pollux.core.service.uriResolvers + +import org.hyperledger.identus.shared.http.{GenericUriResolverError, UriResolver} +import org.hyperledger.identus.shared.models.StatusCode +import zio.* +import zio.http.* + +import java.nio.charset.StandardCharsets + +class HttpUrlResolver(client: Client) extends UriResolver { + import HttpUriResolver.* + override def resolve(uri: String): IO[GenericUriResolverError, String] = { + val program = for { + url <- ZIO.fromEither(URL.decode(uri)).mapError(_ => InvalidURI(uri)) + response <- client + .request(Request(url = url)) + .mapError(t => ConnectionError(t.getMessage)) + body <- response.status match { + case Status.Ok => + response.body.asString.mapError(t => ResponseProcessingError(t.getMessage)) + case Status.NotFound => + ZIO.fail(ResourceNotFound(uri)) + case status if status.isError => + response.body.asStream + .take(1024) // Only take the first 1024 bytes from the response body (if any). + .runCollect + .map(c => new String(c.toArray, StandardCharsets.UTF_8)) + .orDie + .flatMap(errorMessage => ZIO.fail(UnexpectedUpstreamResponseReceived(status.code, Some(errorMessage)))) + case status => + ZIO.fail(UnexpectedUpstreamResponseReceived(status.code)) + } + } yield body + program.provideSomeLayer(zio.Scope.default) + } + +} + +object HttpUriResolver { + val layer: URLayer[Client, HttpUrlResolver] = ZLayer.fromFunction(HttpUrlResolver(_)) + + class HttpUriResolverError(statusCode: StatusCode, userFacingMessage: String) + extends GenericUriResolverError(statusCode, userFacingMessage) + + final case class InvalidURI(uri: String) + extends HttpUriResolverError( + StatusCode.UnprocessableContent, + s"The URI to resolve is invalid: uri=[$uri]" + ) + + final case class ConnectionError(cause: String) + extends HttpUriResolverError( + StatusCode.BadGateway, + s"An error occurred while connecting to the URI's underlying server: cause=[$cause]" + ) + + final case class ResourceNotFound(uri: String) + extends HttpUriResolverError( + StatusCode.NotFound, + s"The resource was not found on the URI's underlying server: uri=[$uri]" + ) + + final case class ResponseProcessingError(cause: String) + extends HttpUriResolverError( + StatusCode.BadGateway, + s"An error occurred while processing the URI's underlying server response: cause=[$cause]" + ) + + final case class UnexpectedUpstreamResponseReceived(status: Int, content: Option[String] = None) + extends HttpUriResolverError( + StatusCode.BadGateway, + s"An unexpected response was received from the URI's underlying server: status=[$status], content=[${content.getOrElse("n/a")}]" + ) + +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/ResourceUrlResolver.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/ResourceUrlResolver.scala new file mode 100644 index 0000000000..0e95320ed8 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/ResourceUrlResolver.scala @@ -0,0 +1,57 @@ +package org.hyperledger.identus.pollux.core.service.uriResolvers + +import org.hyperledger.identus.shared.http.{GenericUriResolverError, InvalidUri, UriResolver} +import org.hyperledger.identus.shared.models.StatusCode +import zio.* + +import java.net.URI +import scala.util.Try + +class ResourceUrlResolver(extraResources: Map[String, String]) extends UriResolver { + import ResourceUrlResolver.* + + def resolve(uri: String): IO[GenericUriResolverError, String] = { + for { + javaUri <- ZIO.fromTry(Try(URI(uri))).mapError(_ => InvalidUri(uri)) + scheme <- ZIO.succeed(javaUri.getScheme) + body <- scheme match + case "resource" => + val inputStream = this.getClass.getResourceAsStream(javaUri.getPath) + if (inputStream != null) + val content = scala.io.Source.fromInputStream(inputStream).mkString + inputStream.close() + ZIO.succeed(content) + else ZIO.fail(ResourceNotFound(uri)) + case _ => + extraResources + .get(uri) + .map(ZIO.succeed(_)) + .getOrElse(ZIO.fail(ResourceNotFound(uri))) + } yield body + + } + +} + +class ResourceUrlResolverError(statusCode: StatusCode, userFacingMessage: String) + extends GenericUriResolverError(statusCode, userFacingMessage) + +object ResourceUrlResolver { + def layer: ULayer[ResourceUrlResolver] = + ZLayer.succeed(new ResourceUrlResolver(Map.empty)) + + def layerWithExtraResources: URLayer[Map[String, String], ResourceUrlResolver] = + ZLayer.fromFunction(ResourceUrlResolver(_)) + + final case class InvalidURI(uri: String) + extends ResourceUrlResolverError( + StatusCode.UnprocessableContent, + s"The URI to resolve is invalid: uri=[$uri]" + ) + + final case class ResourceNotFound(uri: String) + extends ResourceUrlResolverError( + StatusCode.NotFound, + s"The resource was not found on the URI's underlying server: uri=[$uri]" + ) +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala index 7ad3c9588d..2d43eb120f 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala @@ -1,15 +1,20 @@ package org.hyperledger.identus.pollux.core.service.verification import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema -import org.hyperledger.identus.pollux.core.service.URIDereferencer -import org.hyperledger.identus.pollux.vc.jwt.{CredentialPayload, DidResolver, JWT, JWTVerification, JwtCredential} -import org.hyperledger.identus.pollux.vc.jwt.CredentialPayload.Implicits +import org.hyperledger.identus.pollux.vc.jwt.{ + CredentialPayload, + CredentialSchema as JwtCredentialSchema, + DidResolver, + JWT, + JWTVerification, + JwtCredential +} +import org.hyperledger.identus.shared.http.UriResolver import zio.* import java.time.OffsetDateTime -class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDereferencer) - extends VcVerificationService { +class VcVerificationServiceImpl(didResolver: DidResolver, uriResolver: UriResolver) extends VcVerificationService { override def verify( vcVerificationRequests: List[VcVerificationRequest] ): IO[VcVerificationServiceError, List[VcVerificationResult]] = { @@ -49,19 +54,22 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe decodedJwt <- JwtCredential .decodeJwt(JWT(credential)) - .mapError(error => VcVerificationServiceError.UnexpectedError(s"Unable decode JWT: $error")) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Unable to decode JWT: $error")) credentialSchema <- ZIO .fromOption(decodedJwt.maybeCredentialSchema) .mapError(error => VcVerificationServiceError.UnexpectedError(s"Missing Credential Schema: $error")) - credentialSchemas = credentialSchema.fold(List(_), identity) + credentialSchemas = credentialSchema match { + case schema: JwtCredentialSchema => List(schema) + case schemaList: List[JwtCredentialSchema] => schemaList + } result <- ZIO.collectAll( credentialSchemas.map(credentialSchema => CredentialSchema .validSchemaValidator( credentialSchema.id, - uriDereferencer + uriResolver ) .mapError(error => VcVerificationServiceError.UnexpectedError(s"Schema Validator Failed: $error")) ) @@ -98,7 +106,10 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe ZIO .fromOption(decodedJwt.maybeCredentialSchema) .mapError(error => VcVerificationServiceError.UnexpectedError(s"Missing Credential Schema: $error")) - credentialSchemas = credentialSchema.fold(List(_), identity) + credentialSchemas = credentialSchema match { + case schema: JwtCredentialSchema => List(schema) + case schemaList: List[JwtCredentialSchema] => schemaList + } result <- ZIO.collectAll( credentialSchemas.map(credentialSchema => @@ -106,7 +117,7 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe .validateJWTCredentialSubject( credentialSchema.id, CredentialPayload.Implicits.jwtVcEncoder(decodedJwt.vc).noSpaces, - uriDereferencer + uriResolver ) .mapError(error => VcVerificationServiceError.UnexpectedError(s"JWT Credential Subject Validation Failed: $error") @@ -268,6 +279,6 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe } object VcVerificationServiceImpl { - val layer: URLayer[DidResolver & URIDereferencer, VcVerificationService] = + val layer: URLayer[DidResolver & UriResolver, VcVerificationService] = ZLayer.fromFunction(VcVerificationServiceImpl(_, _)) } diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala index 341565de22..940db21325 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala @@ -5,6 +5,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.Cre import org.hyperledger.identus.pollux.core.model.schema.`type`.{AnoncredSchemaType, CredentialJsonSchemaType} import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 import org.hyperledger.identus.pollux.core.model.schema.AnoncredSchemaTypeSpec.test +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.json.JsonSchemaError.JsonValidationErrors import zio.json.* import zio.json.ast.Json @@ -29,6 +30,7 @@ object CredentialSchemaSpec extends ZIOSpecDefault { tags = Seq("tag1", "tag2"), description = "Json Schema", `type` = CredentialJsonSchemaType.VC_JSON_SCHEMA_URI, + resolutionMethod = ResourceResolutionMethod.http, schema = innerJsonSchema.fromJson[Json].getOrElse(Json.Null) ) } @@ -44,6 +46,7 @@ object CredentialSchemaSpec extends ZIOSpecDefault { tags = Seq("tag1", "tag2"), description = "Anoncred Schema", `type` = AnoncredSchemaSerDesV1.version, + resolutionMethod = ResourceResolutionMethod.http, schema = innerJsonSchema.fromJson[Json].getOrElse(Json.Null) ) } diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala index 1b444499c4..439a3de513 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala @@ -38,15 +38,20 @@ class CredentialDefinitionRepositoryInMemory( } yield record } - override def findByGuid(guid: UUID): UIO[Option[CredentialDefinition]] = { + override def findByGuid( + guid: UUID, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): UIO[Option[CredentialDefinition]] = { for { storeRefs <- walletRefs.get - storeRefOption <- ZIO.filter(storeRefs.values)(storeRef => storeRef.get.map(_.contains(guid))).map(_.headOption) + storeRefOption <- ZIO + .filter(storeRefs.values)(storeRef => storeRef.get.map(x => x.contains(guid))) + .map(_.headOption) record <- storeRefOption match { case Some(storeRef) => storeRef.get.map(_.get(guid)) case None => ZIO.none } - } yield record + } yield record.fold(None)(x => if x.resolutionMethod == resolutionMethod then Some(x) else None) } override def update(cs: CredentialDefinition): URIO[WalletAccessContext, CredentialDefinition] = { diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala index aa7d5390f2..d59c0cd30d 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala @@ -104,7 +104,7 @@ class CredentialRepositoryInMemory( recordId: DidCommID, issue: IssueCredential, issuedRawCredential: String, - schemaUri: Option[String], + schemaUris: Option[List[String]], credentialDefinitionUri: Option[String], protocolState: ProtocolState ): URIO[WalletAccessContext, Unit] = { @@ -117,7 +117,7 @@ class CredentialRepositoryInMemory( recordId, record.copy( updatedAt = Some(Instant.now), - schemaUri = schemaUri, + schemaUris = schemaUris, credentialDefinitionUri = credentialDefinitionUri, issueCredentialData = Some(issue), issuedCredentialRaw = Some(issuedRawCredential), @@ -155,7 +155,7 @@ class CredentialRepositoryInMemory( recordId.contains( rec.id ) && rec.issueCredentialData.isDefined - && rec.schemaUri.isDefined + && rec.schemaUris.isDefined && rec.credentialDefinitionUri.isDefined && rec.credentialFormat == CredentialFormat.AnonCreds ) @@ -164,7 +164,7 @@ class CredentialRepositoryInMemory( rec.id, rec.issueCredentialData, rec.credentialFormat, - rec.schemaUri, + rec.schemaUris, rec.credentialDefinitionUri, rec.subjectId, rec.keyId, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositorySpecSuite.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositorySpecSuite.scala index fc61ba2f73..c598c4c876 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositorySpecSuite.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositorySpecSuite.scala @@ -21,7 +21,7 @@ object CredentialRepositorySpecSuite { createdAt = Instant.now, updatedAt = None, thid = DidCommID(), - schemaUri = None, + schemaUris = None, credentialDefinitionId = None, credentialDefinitionUri = None, credentialFormat = credentialFormat, @@ -373,7 +373,7 @@ object CredentialRepositorySpecSuite { aRecord.id, issueCredential, "RAW_CREDENTIAL_DATA", - Some("schemaUri"), + Some(List("schemaUri")), Some("credentialDefinitionUri"), ProtocolState.CredentialReceived ) @@ -383,7 +383,7 @@ object CredentialRepositorySpecSuite { assertTrue(updatedRecord.get.issueCredentialData.contains(issueCredential)) && assertTrue(updatedRecord.get.issuedCredentialRaw.contains("RAW_CREDENTIAL_DATA")) assertTrue(updatedRecord.get.credentialDefinitionUri.contains("credentialDefinitionUri")) - assertTrue(updatedRecord.get.schemaUri.contains("schemaUri")) + assertTrue(updatedRecord.get.schemaUris.getOrElse(List.empty).contains("schemaUri")) } }, test("updateFail (fail one retry) updates record") { diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala index d86e8a5d95..3e4f885f2b 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala @@ -1,15 +1,9 @@ package org.hyperledger.identus.pollux.core.repository -import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, PrismDID} +import org.hyperledger.identus.castor.core.model.did.PrismDID import org.hyperledger.identus.pollux.core.model.* -import org.hyperledger.identus.pollux.vc.jwt.{revocation, Issuer, StatusPurpose} -import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021} -import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.{ - DecodingError, - EncodingError, - IndexOutOfBounds, - InvalidSize -} +import org.hyperledger.identus.pollux.vc.jwt.{Issuer, StatusPurpose} +import org.hyperledger.identus.pollux.vc.jwt.revocation.BitString import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -73,63 +67,69 @@ class CredentialStatusListRepositoryInMemory( exists = stores.flatMap(_.values).exists(_.issueCredentialRecordId == id) } yield exists - def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = for { - storageRef <- walletToStatusListStorageRefs - storage <- storageRef.get - latest = storage.toSeq - .sortBy(_._2.createdAt) { (x, y) => if x.isAfter(y) then -1 else 1 /* DESC */ } - .headOption - .map(_._2) - } yield latest - - def createNewForTheWallet( + override def incrementAndGetStatusListIndex( jwtIssuer: Issuer, statusListRegistryUrl: String - ): URIO[WalletAccessContext, CredentialStatusList] = { + ): URIO[WalletAccessContext, (UUID, Int)] = + def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = for { + storageRef <- walletToStatusListStorageRefs + storage <- storageRef.get + latest = storage.toSeq + .sortBy(_._2.createdAt) { (x, y) => if x.isAfter(y) then -1 else 1 /* DESC */ } + .headOption + .map(_._2) + } yield latest - val id = UUID.randomUUID() - val issued = Instant.now() - val issuerDid = jwtIssuer.did - val canonical = PrismDID.fromString(issuerDid.toString).fold(e => throw RuntimeException(e), _.asCanonical) + def createNewForTheWallet( + id: UUID, + jwtIssuer: Issuer, + issued: Instant, + credentialStr: String + ): URIO[WalletAccessContext, CredentialStatusList] = { + val issuerDid = jwtIssuer.did + val canonical = PrismDID.fromString(issuerDid.toString).fold(e => throw RuntimeException(e), _.asCanonical) - val embeddedProofCredential = for { - bitString <- BitString.getInstance().mapError { - case InvalidSize(message) => new Throwable(message) - case EncodingError(message) => new Throwable(message) - case DecodingError(message) => new Throwable(message) - case IndexOutOfBounds(message) => new Throwable(message) - } - emptyJwtCredential <- VCStatusList2021 - .build( - vcId = s"$statusListRegistryUrl/credential-status/$id", - revocationData = bitString, - jwtIssuer = jwtIssuer + for { + storageRef <- walletToStatusListStorageRefs + walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) + newCredentialStatusList = CredentialStatusList( + id = id, + walletId = walletId, + issuer = canonical, + issued = issued, + purpose = StatusPurpose.Revocation, + statusListCredential = credentialStr, + size = BitString.MIN_SL2021_SIZE, + lastUsedIndex = 0, + createdAt = Instant.now(), + updatedAt = None ) - .mapError(x => new Throwable(x.msg)) + _ <- storageRef.update(r => r + (newCredentialStatusList.id -> newCredentialStatusList)) + } yield newCredentialStatusList + } - credentialWithEmbeddedProof <- emptyJwtCredential.toJsonWithEmbeddedProof - } yield credentialWithEmbeddedProof.spaces2 + def updateLastUsedIndex(statusListId: UUID, lastUsedIndex: Int) = + for { + walletToStatusListStorageRef <- walletToStatusListStorageRefs + _ <- walletToStatusListStorageRef.update(r => { + val value = r.get(statusListId) + value.fold(r) { v => + val updated = v.copy(lastUsedIndex = lastUsedIndex, updatedAt = Some(Instant.now)) + r.updated(statusListId, updated) + } + }) + } yield () for { - credential <- embeddedProofCredential.orDie - storageRef <- walletToStatusListStorageRefs - walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) - newCredentialStatusList = CredentialStatusList( - id = id, - walletId = walletId, - issuer = canonical, - issued = issued, - purpose = StatusPurpose.Revocation, - statusListCredential = credential, - size = BitString.MIN_SL2021_SIZE, - lastUsedIndex = 0, - createdAt = Instant.now(), - updatedAt = None - ) - _ <- storageRef.update(r => r + (newCredentialStatusList.id -> newCredentialStatusList)) - } yield newCredentialStatusList - - } + id <- ZIO.succeed(UUID.randomUUID()) + newStatusListVC <- createStatusListVC(jwtIssuer, statusListRegistryUrl, id).orDie + maybeStatusList <- getLatestOfTheWallet + statusList <- maybeStatusList match + case Some(csl) if csl.lastUsedIndex < csl.size => ZIO.succeed(csl) + case _ => createNewForTheWallet(id, jwtIssuer, Instant.now(), newStatusListVC) + newIndex = statusList.lastUsedIndex + 1 + _ <- updateLastUsedIndex(statusList.id, newIndex) + } yield (statusList.id, newIndex) def allocateSpaceForCredential( issueCredentialRecordId: DidCommID, @@ -150,14 +150,6 @@ class CredentialStatusListRepositoryInMemory( for { credentialInStatusListStorageRef <- statusListToCredInStatusListStorageRefs(credentialStatusListId) _ <- credentialInStatusListStorageRef.update(r => r + (newCredentialInStatusList.id -> newCredentialInStatusList)) - walletToStatusListStorageRef <- walletToStatusListStorageRefs - _ <- walletToStatusListStorageRef.update(r => { - val value = r.get(credentialStatusListId) - value.fold(r) { v => - val updated = v.copy(lastUsedIndex = statusListIndex, updatedAt = Some(Instant.now)) - r.updated(credentialStatusListId, updated) - } - }) } yield () } @@ -186,37 +178,39 @@ class CredentialStatusListRepositoryInMemory( } yield () } - def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]] = { + override def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] = for { statusListsRefs <- allStatusListsStorageRefs statusLists <- statusListsRefs.get - statusListWithCredEffects = statusLists.map { (id, statusList) => - val credsinStatusListEffect = statusListToCredInStatusListStorageRefs(id).flatMap(_.get.map(_.values.toList)) - credsinStatusListEffect.map { credsInStatusList => - CredentialStatusListWithCreds( - id = id, - walletId = statusList.walletId, - issuer = statusList.issuer, - issued = statusList.issued, - purpose = statusList.purpose, - statusListCredential = statusList.statusListCredential, - size = statusList.size, - lastUsedIndex = statusList.lastUsedIndex, - credentials = credsInStatusList.map { cred => - CredInStatusList( - id = cred.id, - issueCredentialRecordId = cred.issueCredentialRecordId, - statusListIndex = cred.statusListIndex, - isCanceled = cred.isCanceled, - isProcessed = cred.isProcessed, - ) - } - ) - } + } yield statusLists.values.toList.map(csl => (csl.walletId, csl.id)) - }.toList - res <- ZIO.collectAll(statusListWithCredEffects) - } yield res + def getCredentialStatusListsWithCreds( + statusListId: UUID + ): URIO[WalletAccessContext, CredentialStatusListWithCreds] = { + for { + statusListsRefs <- allStatusListsStorageRefs + statusLists <- statusListsRefs.get + statusList = statusLists(statusListId) + credsInStatusList <- statusListToCredInStatusListStorageRefs(statusList.id).flatMap(_.get.map(_.values.toList)) + } yield CredentialStatusListWithCreds( + id = statusList.id, + walletId = statusList.walletId, + issuer = statusList.issuer, + issued = statusList.issued, + purpose = statusList.purpose, + statusListCredential = statusList.statusListCredential, + size = statusList.size, + lastUsedIndex = statusList.lastUsedIndex, + credentials = credsInStatusList.map { cred => + CredInStatusList( + id = cred.id, + issueCredentialRecordId = cred.issueCredentialRecordId, + statusListIndex = cred.statusListIndex, + isCanceled = cred.isCanceled, + isProcessed = cred.isProcessed, + ) + } + ) } def updateStatusListCredential( diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala index 35353652ab..88ada5aa74 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala @@ -4,6 +4,7 @@ import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemo import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition import org.hyperledger.identus.pollux.core.repository.CredentialDefinitionRepositoryInMemory +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.models.WalletId.* import zio.* @@ -15,7 +16,7 @@ trait CredentialDefinitionServiceSpecHelper { protected val defaultWalletLayer = ZLayer.succeed(WalletAccessContext(WalletId.default)) protected val credentialDefinitionServiceLayer = - GenericSecretStorageInMemory.layer ++ CredentialDefinitionRepositoryInMemory.layer ++ ResourceURIDereferencerImpl.layer >>> + GenericSecretStorageInMemory.layer ++ CredentialDefinitionRepositoryInMemory.layer ++ ResourceUrlResolver.layer >>> CredentialDefinitionServiceImpl.layer ++ defaultWalletLayer val defaultDefinition = diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala index a63890f44a..0e15ad2d55 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala @@ -14,6 +14,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError.* import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.{ProtocolState, Role} +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.{CredentialIssuer, JWT, JwtCredential, JwtCredentialPayload} import org.hyperledger.identus.shared.models.{KeyId, UnmanagedFailureException, WalletAccessContext, WalletId} import zio.* @@ -33,7 +34,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS ).provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> credentialServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ) @@ -75,14 +76,14 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS thid = thid, pairwiseIssuerDID = pairwiseIssuerDid, pairwiseHolderDID = pairwiseHolderDid, - maybeSchemaId = None, + maybeSchemaIds = None, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance ) } yield { assertTrue(record.thid == thid) && assertTrue(record.updatedAt.isEmpty) && - assertTrue(record.schemaUri.isEmpty) && + assertTrue(record.schemaUris.getOrElse(List.empty).isEmpty) && assertTrue(record.validityPeriod == validityPeriod) && assertTrue(record.automaticIssuance == automaticIssuance) && assertTrue(record.role == Role.Issuer) && @@ -148,7 +149,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS thid = thid, pairwiseIssuerDID = pairwiseIssuerDid, pairwiseHolderDID = pairwiseHolderDid, - maybeSchemaId = Some("resource:///vc-schema-example.json"), + maybeSchemaIds = Some(List("resource:///vc-schema-example.json")), claims = claims, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance @@ -158,7 +159,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS assertTrue(record.thid == thid) && assertTrue(record.updatedAt.isEmpty) && assertTrue( - record.schemaUri.contains("resource:///vc-schema-example.json") + record.schemaUris.getOrElse(List.empty).contains("resource:///vc-schema-example.json") ) && assertTrue(record.validityPeriod == validityPeriod) && assertTrue(record.automaticIssuance == automaticIssuance) && @@ -208,7 +209,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS thid = thid, pairwiseIssuerDID = pairwiseIssuerDid, pairwiseHolderDID = pairwiseHolderDid, - maybeSchemaId = Some("resource:///vc-schema-example.json"), + maybeSchemaIds = Some(List("resource:///vc-schema-example.json")), claims = claims, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance @@ -287,7 +288,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS } yield { assertTrue(holderRecord.thid.toString == offer.thid.get) && assertTrue(holderRecord.updatedAt.isEmpty) && - assertTrue(holderRecord.schemaUri.isEmpty) && + assertTrue(holderRecord.schemaUris.getOrElse(List.empty).isEmpty) && assertTrue(holderRecord.validityPeriod.isEmpty) && assertTrue(holderRecord.automaticIssuance.isEmpty) && assertTrue(holderRecord.role == Role.Holder) && @@ -522,7 +523,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS // Issuer generates credential credentialGenerateRecord <- issuerSvc.generateJWTCredential( issuerRecordId, - "https://test-status-list.registry" + "status-list-registry" ) decodedJWT <- credentialGenerateRecord.issueCredentialData.get.attachments.head.data match { case MyBase64(value) => @@ -538,11 +539,9 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS _ <- holderSvc.receiveCredentialIssue(issueCredential) } yield assertTrue( decodedJWT.issuer == - Right( - CredentialIssuer( - id = decodedJWT.iss, - `type` = "Profile" - ) + CredentialIssuer( + id = decodedJWT.iss, + `type` = "Profile" ) ) }.provideSomeLayer( @@ -592,7 +591,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS .service[CredentialService] .provideSomeLayer( holderCredDefResolverLayer >>> - ResourceURIDereferencerImpl.layerWithExtraResources >>> + ResourceUrlResolver.layerWithExtraResources >>> credentialServiceLayer ) offerCredential <- ZIO.fromEither(OfferCredential.readFromMessage(msg)) diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifierSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifierSpec.scala index 3e3c5bc6b3..fd71889562 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifierSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifierSpec.scala @@ -101,7 +101,7 @@ object CredentialServiceNotifierSpec extends MockSpecDefault with CredentialServ _ <- svc.markOfferSent(issuerRecordId) _ <- svc.receiveCredentialRequest(requestCredential()) _ <- svc.acceptCredentialRequest(issuerRecordId) - _ <- svc.generateJWTCredential(issuerRecordId, "https://test-status-list.registry") + _ <- svc.generateJWTCredential(issuerRecordId, "status-list-registry") _ <- svc.markCredentialSent(issuerRecordId) consumer <- ens.consumer[IssueCredentialRecord]("Issue") events <- consumer.poll(50) diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala index 4f61c5e123..8ae2fd6602 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala @@ -3,7 +3,6 @@ package org.hyperledger.identus.pollux.core.service import io.circe.Json import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService -import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage import org.hyperledger.identus.castor.core.model.did.PrismDID import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.mercury.model.{AttachmentDescriptor, DidId} @@ -17,6 +16,8 @@ import org.hyperledger.identus.pollux.core.repository.{ } import org.hyperledger.identus.pollux.prex.{ClaimFormat, Ldp, PresentationDefinition} import org.hyperledger.identus.pollux.vc.jwt.* +import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{MessagingService, MessagingServiceConfig, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -32,14 +33,16 @@ trait CredentialServiceSpecHelper { CredentialDefinitionRepositoryInMemory.layer >>> CredentialDefinitionServiceImpl.layer protected val credentialServiceLayer - : URLayer[DIDService & ManagedDIDService & URIDereferencer, CredentialService & CredentialDefinitionService] = - ZLayer.makeSome[DIDService & ManagedDIDService & URIDereferencer, CredentialService & CredentialDefinitionService]( + : URLayer[DIDService & ManagedDIDService & UriResolver, CredentialService & CredentialDefinitionService] = + ZLayer.makeSome[DIDService & ManagedDIDService & UriResolver, CredentialService & CredentialDefinitionService]( CredentialRepositoryInMemory.layer, CredentialStatusListRepositoryInMemory.layer, ZLayer.fromFunction(PrismDidResolver(_)), credentialDefinitionServiceLayer, GenericSecretStorageInMemory.layer, LinkSecretServiceImpl.layer, + (MessagingServiceConfig.inMemoryLayer >>> MessagingService.serviceLayer >>> + MessagingService.producerLayer[UUID, WalletIdAndRecordId]).orDie, CredentialServiceImpl.layer ) @@ -107,7 +110,7 @@ trait CredentialServiceSpecHelper { pairwiseIssuerDID: DidId = DidId("did:prism:issuer"), pairwiseHolderDID: Option[DidId] = Some(DidId("did:prism:holder-pairwise")), thid: DidCommID = DidCommID(), - maybeSchemaId: Option[String] = None, + maybeSchemaIds: Option[List[String]] = None, claims: Json = io.circe.parser .parse(""" |{ @@ -130,7 +133,7 @@ trait CredentialServiceSpecHelper { pairwiseHolderDID = pairwiseHolderDID, kidIssuer = None, thid = thid, - maybeSchemaId = maybeSchemaId, + maybeSchemaIds = maybeSchemaIds, claims = claims, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala index 41b7b472bc..eff67638bc 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala @@ -22,7 +22,7 @@ object MockCredentialService extends Mock[CredentialService] { DidId, Option[DidId], DidCommID, - Option[String], + Option[List[String]], Json, Option[Double], Option[Boolean], @@ -41,7 +41,7 @@ object MockCredentialService extends Mock[CredentialService] { DidId, Option[DidId], DidCommID, - Option[String], + Option[List[String]], Json, Option[Double], Option[Boolean], @@ -130,7 +130,7 @@ object MockCredentialService extends Mock[CredentialService] { pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: Json, validityPeriod: Option[Double], automaticIssuance: Option[Boolean], @@ -145,7 +145,7 @@ object MockCredentialService extends Mock[CredentialService] { pairwiseIssuerDID, pairwiseHolderDID, thid, - maybeSchemaId, + maybeSchemaIds, claims, validityPeriod, automaticIssuance, @@ -161,7 +161,7 @@ object MockCredentialService extends Mock[CredentialService] { pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: Json, validityPeriod: Option[Double], automaticIssuance: Option[Boolean], @@ -176,7 +176,7 @@ object MockCredentialService extends Mock[CredentialService] { pairwiseIssuerDID, pairwiseHolderDID, thid, - maybeSchemaId, + maybeSchemaIds, claims, validityPeriod, automaticIssuance, @@ -252,9 +252,9 @@ object MockCredentialService extends Mock[CredentialService] { override def generateJWTCredential( recordId: DidCommID, - statusListRegistryUrl: String, + statusListRegistryServiceName: String, ): ZIO[WalletAccessContext, RecordNotFound | CredentialRequestValidationFailed, IssueCredentialRecord] = - proxy(GenerateJWTCredential, recordId, statusListRegistryUrl) + proxy(GenerateJWTCredential, recordId, statusListRegistryServiceName) override def generateSDJWTCredential( recordId: DidCommID, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala index 4f58c2f570..dad75bfcee 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala @@ -16,7 +16,7 @@ import org.hyperledger.identus.pollux.core.service.serdes.{AnoncredCredentialPro import org.hyperledger.identus.pollux.sdjwt.{HolderPrivateKey, PresentationCompact} import org.hyperledger.identus.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCredentialPayload} import org.hyperledger.identus.shared.models.* -import zio.{mock, Duration, IO, UIO, URLayer, ZIO, ZLayer} +import zio.{mock, Duration, IO, UIO, URIO, URLayer, ZIO, ZLayer} import zio.json.* import zio.mock.{Mock, Proxy} @@ -329,7 +329,8 @@ object MockPresentationService extends Mock[PresentationService] { state: PresentationRecord.ProtocolState* ): IO[PresentationError, Seq[PresentationRecord]] = ??? - override def findPresentationRecord(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = ??? + override def findPresentationRecord(recordId: DidCommID): URIO[WalletAccessContext, Option[PresentationRecord]] = + ??? override def findPresentationRecordByThreadId( thid: DidCommID diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala index 8d2d3614bb..082d107fd4 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala @@ -96,7 +96,7 @@ object OID4VCIIssuerMetadataServiceSpecSuite { exit1 <- createCredConfig("not a uri").exit exit2 <- createCredConfig("http://localhost/schema").exit } yield assert(exit1)(failsWithA[InvalidSchemaId]) && - assert(exit2)(dies(anything)) + assert(exit2)(failsWithA[InvalidSchemaId]) }, test("list credential configurations for non-existing issuer should fail") { for { diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala index b1feb03cd2..981a187601 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala @@ -521,7 +521,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp aIssueCredentialRecord.id, issueCredential, rawCredentialData, - Some("SchemaId"), + Some(List("SchemaId")), Some("CredDefId"), IssueCredentialRecord.ProtocolState.CredentialReceived ) @@ -865,7 +865,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp createdAt = Instant.now, updatedAt = None, thid = DidCommID(), - schemaUri = Some(schemaId), + schemaUris = Some(List(schemaId)), credentialDefinitionId = Some(credentialDefinitionDb.guid), credentialDefinitionUri = Some(credentialDefinitionId), credentialFormat = CredentialFormat.AnonCreds, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala index 8e08c6445f..1e59c4704a 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala @@ -1,6 +1,5 @@ package org.hyperledger.identus.pollux.core.service -import com.nimbusds.jose.jwk.* import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.castor.core.model.did.DID import org.hyperledger.identus.mercury.{AgentPeerService, PeerDID} @@ -10,8 +9,11 @@ import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.model.error.PresentationError import org.hyperledger.identus.pollux.core.repository.* import org.hyperledger.identus.pollux.core.service.serdes.* +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.crypto.KmpSecp256k1KeyOps +import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{MessagingService, MessagingServiceConfig, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -26,21 +28,23 @@ trait PresentationServiceSpecHelper { AgentPeerService.makeLayer(PeerDID.makePeerDid(serviceEndpoint = Some("http://localhost:9099"))) val genericSecretStorageLayer = GenericSecretStorageInMemory.layer - val uriDereferencerLayer = ResourceURIDereferencerImpl.layer + val uriResolverLayer = ResourceUrlResolver.layer val credentialDefLayer = - CredentialDefinitionRepositoryInMemory.layer ++ uriDereferencerLayer >>> CredentialDefinitionServiceImpl.layer + CredentialDefinitionRepositoryInMemory.layer ++ uriResolverLayer >>> CredentialDefinitionServiceImpl.layer val linkSecretLayer = genericSecretStorageLayer >+> LinkSecretServiceImpl.layer val presentationServiceLayer = ZLayer.make[ - PresentationService & CredentialDefinitionService & URIDereferencer & LinkSecretService & PresentationRepository & + PresentationService & CredentialDefinitionService & UriResolver & LinkSecretService & PresentationRepository & CredentialRepository ]( PresentationServiceImpl.layer, credentialDefLayer, - uriDereferencerLayer, + uriResolverLayer, linkSecretLayer, PresentationRepositoryInMemory.layer, - CredentialRepositoryInMemory.layer + CredentialRepositoryInMemory.layer, + (MessagingServiceConfig.inMemoryLayer >>> MessagingService.serviceLayer >>> + MessagingService.producerLayer[UUID, WalletIdAndRecordId]).orDie, ) ++ defaultWalletLayer def createIssuer(did: String): Issuer = { @@ -133,7 +137,7 @@ trait PresentationServiceSpecHelper { createdAt = Instant.now, updatedAt = None, thid = DidCommID(), - schemaUri = None, + schemaUris = None, credentialDefinitionId = None, credentialDefinitionUri = None, credentialFormat = credentialFormat, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolverSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolverSpec.scala new file mode 100644 index 0000000000..8d24db9ec9 --- /dev/null +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolverSpec.scala @@ -0,0 +1,192 @@ +package org.hyperledger.identus.pollux.core.service.uriResolvers + +import io.circe.* +import io.lemonlabs.uri.Url +import org.hyperledger.identus.pollux.vc.jwt.* +import org.hyperledger.identus.shared.crypto.Sha256Hash +import org.hyperledger.identus.shared.json.Json as JsonUtils +import org.hyperledger.identus.shared.models.PrismEnvelopeData +import org.hyperledger.identus.shared.utils.Base64Utils +import zio.* +import zio.json.* +import zio.test.* +import zio.test.Assertion.* + +import java.time.Instant + +object DidUrlResolverSpec extends ZIOSpecDefault { + + private val schema = """ + |{ + | "guid":"ef3e4135-8fcf-3ce7-b5bb-df37defc13f6", + | "id":"e33a6de7-1f93-404f-9f12-9bd7b397fd2c", + | "longId":"did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a/e33a6de7-1f93-404f-9f12-9bd7b397fd2c?version=1.0.0", + | "name":"driving-license", + | "version":"1.0.0", + | "tags":[ + | "driving", + | "license" + | ], + | "description":"Driving License Schema", + | "type":"https://w3c-ccg.github.io/vc-json-schemas/schema/2.0/schema.json", + | "schema":{ + | "$id":"https://example.com/driving-license-1.0.0", + | "$schema":"https://json-schema.org/draft/2020-12/schema", + | "description":"Driving License", + | "type":"object", + | "properties":{ + | "emailAddress":{ + | "type":"string", + | "format":"email" + | }, + | "givenName":{ + | "type":"string" + | }, + | "familyName":{ + | "type":"string" + | }, + | "dateOfIssuance":{ + | "type":"string", + | "format":"date-time" + | }, + | "drivingLicenseID":{ + | "type":"string" + | }, + | "drivingClass":{ + | "type":"integer" + | } + | }, + | "required":[ + | "emailAddress", + | "familyName", + | "dateOfIssuance", + | "drivingLicenseID", + | "drivingClass" + | ], + | "additionalProperties":true + | }, + | "author":"did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a", + | "authored":"2024-06-20T15:17:41.049526Z", + | "kind":"CredentialSchema", + | "self":"/schema-registry/schemas/ef3e4135-8fcf-3ce7-b5bb-df37defc13f6" + |} + |""".stripMargin + + private val normalizedSchema = JsonUtils.canonicalizeToJcs(schema).toOption.get + private val encodedSchema = Base64Utils.encodeURL(normalizedSchema.getBytes) + + private val schemaHash = Sha256Hash.compute(encodedSchema.getBytes()).hexEncoded + + private val testDidUrl = Url + .parse( + s"did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a?resourceService=agent-base-url&resourcePath=schema-registry/schemas/did-url/ef3e4135-8fcf-3ce7-b5bb-df37defc13f6&resourceHash=$schemaHash" + ) + .toString + + class MockHttpUrlResolver extends HttpUrlResolver(null) { + // Mock implementation, always resolves some schema + override def resolve(uri: String) = { + + val responseEnvelope = PrismEnvelopeData( + resource = encodedSchema, + url = uri + ) + + ZIO.succeed(responseEnvelope.toJson) + + } + } + + private val didResolverLayer = ZLayer.succeed(new DidResolver { + // mock implementation, always resolves the same DID + override def resolve(didUrl: String) = ZIO.succeed( + DIDResolutionSucceeded( + DIDDocument( + id = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a", + alsoKnowAs = Vector.empty[String], + controller = Vector("did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a"), + verificationMethod = Vector( + VerificationMethod( + id = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a#auth-1", + `type` = "JsonWebKey2020", + controller = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a", + publicKeyBase58 = Option.empty, + publicKeyBase64 = Option.empty, + publicKeyJwk = Some( + JsonWebKey( + crv = Some("secp256k1"), + x = Some("HFmBco2W7GT7n-JTx6R0Cd3fV0GpOxuWWC0Uu-B4vik"), + y = Some("1wwJuzZ4e898lWyLjwHi3H83602JI-8ErcWt08czqfI"), + kty = "EC" + ) + ), + publicKeyHex = Option.empty, + publicKeyMultibase = Option.empty, + blockchainAccountId = Option.empty, + ethereumAddress = Option.empty + ), + VerificationMethod( + id = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a#issue-1", + `type` = "JsonWebKey2020", + controller = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a", + publicKeyBase58 = Option.empty, + publicKeyBase64 = Option.empty, + publicKeyJwk = Some( + JsonWebKey( + crv = Some("secp256k1"), + x = Some("CXIFl2R18ameLD-ykSOGKQoCBVbFM5oulkc2vIrJtS4"), + y = Some("D2QYNi6-A9z1lxpRjKbocKSTvNAIsNVslBjlzegYyUA"), + kty = "EC" + ) + ), + publicKeyHex = Option.empty, + publicKeyMultibase = Option.empty, + blockchainAccountId = Option.empty, + ethereumAddress = Option.empty + ) + ), + authentication = Vector( + "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a#auth-1" + ), + assertionMethod = Vector( + "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a#issue-1" + ), + service = Vector( + Service( + id = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a#agent-base-url", + `type` = "LinkedResourceV1", + serviceEndpoint = Json.fromString("https://agent-url.com") + ) + ) + ), + DIDDocumentMetadata( + created = Some(Instant.parse("2024-06-20T15:16:39Z")), + updated = Some(Instant.parse("2024-06-20T15:16:39Z")), + deactivated = Some(false) + ) + ) + ) + }) + private val httpUrlResolver = ZLayer.succeed(new MockHttpUrlResolver) + + override def spec = { + suite("DidUrlResolverSpec")( + test("Should resolve a DID url correctly") { + for { + didResolver <- ZIO.service[DidResolver] + httpUrlResolver <- ZIO.service[HttpUrlResolver] + didUrlResolver = new DidUrlResolver(httpUrlResolver, didResolver) + response <- didUrlResolver.resolve(testDidUrl) + responseEnvelope <- ZIO.fromEither(response.fromJson[PrismEnvelopeData]) + } yield { + assert(responseEnvelope.url)( + equalTo(testDidUrl) + ) + assert(responseEnvelope.resource)( + equalTo(encodedSchema) + ) + } + } + ).provide(didResolverLayer, httpUrlResolver) + } +} diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala index 62be6e2e87..25daf11202 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala @@ -4,7 +4,7 @@ import io.circe.* import io.circe.syntax.* import org.hyperledger.identus.agent.walletapi.service.MockManagedDIDService import org.hyperledger.identus.castor.core.service.MockDIDService -import org.hyperledger.identus.pollux.core.service.ResourceURIDereferencerImpl +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.pollux.vc.jwt.CredentialPayload.Implicits.* import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} @@ -27,17 +27,15 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -81,7 +79,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -94,17 +92,15 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -148,7 +144,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -161,17 +157,15 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -215,7 +209,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( issuerDidServiceExpectations.toLayer ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -228,17 +222,15 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -289,7 +281,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -302,17 +294,15 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -360,7 +350,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -373,17 +363,15 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "resource:///vc-schema-personal.json", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "resource:///vc-schema-personal.json", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -431,7 +419,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -444,22 +432,20 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Right( - List( - CredentialSchema( - id = "resource:///vc-schema-personal.json", - `type` = "JsonSchemaValidator2018" - ), - CredentialSchema( - id = "resource:///vc-schema-driver-license.json", - `type` = "JsonSchemaValidator2018" - ) + List( + CredentialSchema( + id = "resource:///vc-schema-personal.json", + `type` = "JsonSchemaValidator2018" + ), + CredentialSchema( + id = "resource:///vc-schema-driver-license.json", + `type` = "JsonSchemaValidator2018" ) ) ), @@ -511,7 +497,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -524,22 +510,20 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Right( - List( - CredentialSchema( - id = "resource:///vc-schema-personal.json", - `type` = "JsonSchemaValidator2018" - ), - CredentialSchema( - id = "resource:///vc-schema-driver-license.json", - `type` = "JsonSchemaValidator2018" - ) + List( + CredentialSchema( + id = "resource:///vc-schema-personal.json", + `type` = "JsonSchemaValidator2018" + ), + CredentialSchema( + id = "resource:///vc-schema-driver-license.json", + `type` = "JsonSchemaValidator2018" ) ) ), @@ -591,7 +575,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -605,17 +589,15 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -659,7 +641,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -673,17 +655,15 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -727,7 +707,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -741,17 +721,15 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -795,7 +773,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -809,17 +787,15 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -863,7 +839,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ) diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala index c2f52148ea..73e9c21021 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala @@ -3,8 +3,9 @@ package org.hyperledger.identus.pollux.core.service.verification import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} import org.hyperledger.identus.castor.core.model.did.VerificationRelationship import org.hyperledger.identus.castor.core.service.{DIDService, MockDIDService} -import org.hyperledger.identus.pollux.core.service.{ResourceURIDereferencerImpl, URIDereferencer} +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.* +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.models.WalletId.* import zio.* @@ -37,12 +38,12 @@ trait VcVerificationServiceSpecHelper { MockManagedDIDService.empty >>> ZLayer.fromFunction(PrismDidResolver(_)) protected val vcVerificationServiceLayer: ZLayer[Any, Nothing, VcVerificationService & WalletAccessContext] = - emptyDidResolverLayer ++ ResourceURIDereferencerImpl.layer >>> + emptyDidResolverLayer ++ ResourceUrlResolver.layer >>> VcVerificationServiceImpl.layer ++ defaultWalletLayer protected val someVcVerificationServiceLayer - : URLayer[DIDService & ManagedDIDService & URIDereferencer, VcVerificationService] = - ZLayer.makeSome[DIDService & ManagedDIDService & URIDereferencer, VcVerificationService]( + : URLayer[DIDService & ManagedDIDService & UriResolver, VcVerificationService] = + ZLayer.makeSome[DIDService & ManagedDIDService & UriResolver, VcVerificationService]( ZLayer.fromFunction(PrismDidResolver(_)), VcVerificationServiceImpl.layer ) diff --git a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala index 60b025826e..90f1c40507 100644 --- a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala +++ b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala @@ -74,7 +74,7 @@ object PresentationSubmissionVerificationSpec extends ZIOSpecDefault { maybeTermsOfUse = None, maybeValidFrom = None, maybeValidUntil = None, - maybeIssuer = Some(Left(iss)) + maybeIssuer = Some(iss) ), nbf = jwtCredentialNbf, aud = Set.empty, diff --git a/pollux/sql-doobie/src/main/resources/sql/pollux/V28__support_multiple_credential_schema.sql b/pollux/sql-doobie/src/main/resources/sql/pollux/V28__support_multiple_credential_schema.sql new file mode 100644 index 0000000000..5dd92dedb2 --- /dev/null +++ b/pollux/sql-doobie/src/main/resources/sql/pollux/V28__support_multiple_credential_schema.sql @@ -0,0 +1,9 @@ +ALTER TABLE public.issue_credential_records + ADD COLUMN schema_uris VARCHAR(500)[]; + +UPDATE public.issue_credential_records +SET schema_uris = ARRAY[schema_uri] +WHERE schema_uri IS NOT NULL; + +ALTER TABLE public.issue_credential_records + DROP COLUMN schema_uri; \ No newline at end of file diff --git a/pollux/sql-doobie/src/main/resources/sql/pollux/V29__add_resolution_method_to_schema_and_cred_definition.sql b/pollux/sql-doobie/src/main/resources/sql/pollux/V29__add_resolution_method_to_schema_and_cred_definition.sql new file mode 100644 index 0000000000..a85df4838b --- /dev/null +++ b/pollux/sql-doobie/src/main/resources/sql/pollux/V29__add_resolution_method_to_schema_and_cred_definition.sql @@ -0,0 +1,10 @@ +-- Create the enum type +CREATE TYPE resolution_method_enum AS ENUM ('http', 'did'); + +-- Add the column to credential_definition table +ALTER TABLE public.credential_definition + ADD COLUMN resolution_method resolution_method_enum NOT NULL DEFAULT 'http'; + +-- Add the column to credential_schema table +ALTER TABLE public.credential_schema + ADD COLUMN resolution_method resolution_method_enum NOT NULL DEFAULT 'http'; diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialDefinition.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialDefinition.scala index 8f82f8e1e8..75c54e4eac 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialDefinition.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialDefinition.scala @@ -5,6 +5,7 @@ import io.getquill.context.json.PostgresJsonExtensions import io.getquill.doobie.DoobieContext import io.getquill.idiom.* import org.hyperledger.identus.pollux.core.model.schema.{CorrectnessProof, Definition} +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.models.WalletId import java.time.temporal.ChronoUnit @@ -27,6 +28,7 @@ case class CredentialDefinition( keyCorrectnessProof: JsonValue[CorrectnessProof], signatureType: String, supportRevocation: Boolean, + resolutionMethod: ResourceResolutionMethod, walletId: WalletId ) { lazy val uniqueConstraintKey = author + name + version @@ -56,6 +58,7 @@ object CredentialDefinition { schemaId = m.schemaId, signatureType = m.signatureType, supportRevocation = m.supportRevocation, + resolutionMethod = m.resolutionMethod, walletId = walletId ) @@ -77,12 +80,17 @@ object CredentialDefinition { keyCorrectnessProof = db.keyCorrectnessProof.value, schemaId = db.schemaId, signatureType = db.signatureType, - supportRevocation = db.supportRevocation + supportRevocation = db.supportRevocation, + resolutionMethod = db.resolutionMethod ) } } -object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with PostgresJsonExtensions { +object CredentialDefinitionSql + extends DoobieContext.Postgres(SnakeCase) + with PostgresJsonExtensions + with PostgresEnumEncoders { + def insert(credentialDefinition: CredentialDefinition) = run { quote( query[CredentialDefinition] @@ -90,8 +98,13 @@ object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with Po ).returning(cs => cs) } - def findByGUID(guid: UUID) = run { - quote(query[CredentialDefinition].filter(_.guid == lift(guid)).take(1)) + def findByGUID(guid: UUID, resolutionMethod: ResourceResolutionMethod) = run { + quote( + query[CredentialDefinition] + .filter(_.guid == lift(guid)) + .filter(_.resolutionMethod == lift(resolutionMethod)) + .take(1) + ) } def findByID(id: UUID) = run { @@ -143,7 +156,8 @@ object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with Po authorOpt: Option[String] = None, nameOpt: Option[String] = None, versionOpt: Option[String] = None, - tagOpt: Option[String] = None + tagOpt: Option[String] = None, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) = run { val q = idOpt.fold(quote(query[CredentialDefinition]))(id => @@ -158,6 +172,7 @@ object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with Po tagOpt .fold(quote(true))(tag => quote(cs.tags.contains(lift(tag)))) ) + .filter(_.resolutionMethod == lift(resolutionMethod)) .size } @@ -168,7 +183,8 @@ object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with Po versionOpt: Option[String] = None, tagOpt: Option[String] = None, offset: Int = 0, - limit: Int = 1000 + limit: Int = 1000, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) = run { val q = idOpt.fold(quote(query[CredentialDefinition]))(id => @@ -183,6 +199,7 @@ object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with Po tagOpt .fold(quote(true))(tag => quote(cs.tags.contains(lift(tag)))) ) + .filter(_.resolutionMethod == lift(resolutionMethod)) .sortBy(cs => cs.id) .drop(offset) .take(limit) diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialSchema.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialSchema.scala index 358a175d70..7d1346f3bb 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialSchema.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialSchema.scala @@ -5,6 +5,7 @@ import io.getquill.context.json.PostgresJsonExtensions import io.getquill.doobie.DoobieContext import io.getquill.idiom.* import org.hyperledger.identus.pollux.core.model.schema.Schema +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.models.WalletId import java.time.temporal.ChronoUnit @@ -22,6 +23,7 @@ case class CredentialSchema( description: String, `type`: String, schema: JsonValue[Schema], + resolutionMethod: ResourceResolutionMethod, walletId: WalletId ) { lazy val uniqueConstraintKey = author + name + version @@ -47,6 +49,7 @@ object CredentialSchema { description = m.description, `type` = m.`type`, schema = JsonValue(m.schema), + resolutionMethod = m.resolutionMethod, walletId = walletId ) @@ -63,12 +66,17 @@ object CredentialSchema { tags = db.tags, description = db.description, `type` = db.`type`, + resolutionMethod = db.resolutionMethod, schema = db.schema.value ) } } -object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with PostgresJsonExtensions { +object CredentialSchemaSql + extends DoobieContext.Postgres(SnakeCase) + with PostgresJsonExtensions + with PostgresEnumEncoders { + def insert(schema: CredentialSchema) = run { quote( query[CredentialSchema] @@ -76,21 +84,27 @@ object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with Postgr ).returning(cs => cs) } - def findByGUID(guid: UUID) = run { - quote(query[CredentialSchema].filter(_.guid == lift(guid)).take(1)) + def findByGUID(guid: UUID, resolutionMethod: ResourceResolutionMethod) = run { + quote( + query[CredentialSchema] + .filter(_.guid == lift(guid)) + .filter(_.resolutionMethod == lift(resolutionMethod)) + .take(1) + ) } + // NOTE: this function is not used def findByID(id: UUID) = run { quote(query[CredentialSchema].filter(_.id == lift(id))) } - def getAllVersions(id: UUID, author: String) = run { + def getAllVersions(id: UUID, author: String, resolutionMethod: ResourceResolutionMethod) = run { quote( query[CredentialSchema] .filter(_.id == lift(id)) .filter(_.author == lift(author)) + .filter(_.resolutionMethod == lift(resolutionMethod)) .sortBy(_.version)(ord = Ord.asc) - .map(_.version) ) } @@ -129,10 +143,16 @@ object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with Postgr authorOpt: Option[String] = None, nameOpt: Option[String] = None, versionOpt: Option[String] = None, - tagOpt: Option[String] = None + tagOpt: Option[String] = None, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) = run { val q = - idOpt.fold(quote(query[CredentialSchema]))(id => quote(query[CredentialSchema].filter(cs => cs.id == lift(id)))) + idOpt.fold(quote(query[CredentialSchema]))(id => + quote( + query[CredentialSchema] + .filter(cs => cs.id == lift(id)) + ) + ) q.dynamic .filterOpt(authorOpt)((cs, author) => quote(cs.author.like(author))) @@ -142,6 +162,7 @@ object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with Postgr tagOpt .fold(quote(true))(tag => quote(cs.tags.contains(lift(tag)))) ) + .filter(_.resolutionMethod == lift(resolutionMethod)) .size } @@ -152,10 +173,16 @@ object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with Postgr versionOpt: Option[String] = None, tagOpt: Option[String] = None, offset: Int = 0, - limit: Int = 1000 + limit: Int = 1000, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) = run { val q = - idOpt.fold(quote(query[CredentialSchema]))(id => quote(query[CredentialSchema].filter(cs => cs.id == lift(id)))) + idOpt.fold(quote(query[CredentialSchema]))(id => + quote( + query[CredentialSchema] + .filter(cs => cs.id == lift(id)) + ) + ) q.dynamic .filterOpt(authorOpt)((cs, author) => quote(cs.author.like(author))) @@ -165,6 +192,7 @@ object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with Postgr tagOpt .fold(quote(true))(tag => quote(cs.tags.contains(lift(tag)))) ) + .filter(_.resolutionMethod == lift(resolutionMethod)) .sortBy(cs => cs.id) .drop(offset) .take(limit) diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala index 652b137fc4..4e4c2bf491 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala @@ -1,7 +1,13 @@ package org.hyperledger.identus.pollux.sql.model +import doobie.* +import doobie.postgres.* +import doobie.postgres.implicits.* +import io.getquill.doobie.DoobieContext import io.getquill.MappedEncoding +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.models.WalletId +import org.postgresql.util.PGobject import java.util.UUID @@ -10,4 +16,36 @@ package object db { given MappedEncoding[WalletId, UUID] = MappedEncoding(_.toUUID) given MappedEncoding[UUID, WalletId] = MappedEncoding(WalletId.fromUUID) + given mappedDecoderResourceResolutionMethod: MappedEncoding[ResourceResolutionMethod, String] = + MappedEncoding[ResourceResolutionMethod, String](_.toString) + + given mappedEncoderResourceResolutionMethod: MappedEncoding[String, ResourceResolutionMethod] = + MappedEncoding[String, ResourceResolutionMethod] { + case "did" => ResourceResolutionMethod.did + case "http" => ResourceResolutionMethod.http + case other => throw new IllegalArgumentException(s"Unknown ResourceResolutionMethod: $other") + } + + trait PostgresEnumEncoders { + this: DoobieContext.Postgres[_] => + + given encoderResourceResolutionMethod: Encoder[ResourceResolutionMethod] = encoder[ResourceResolutionMethod]( + java.sql.Types.OTHER, + (index: Index, value: ResourceResolutionMethod, row: PrepareRow) => { + val pgObj = new PGobject() + pgObj.setType("resolution_method_enum") + pgObj.setValue(value.toString) + row.setObject(index, pgObj, java.sql.Types.OTHER) + } + ) + + given decoderResourceResolutionMethod: Decoder[ResourceResolutionMethod] = decoder(row => + index => + row.getObject(index).toString match { + case "did" => ResourceResolutionMethod.did + case "http" => ResourceResolutionMethod.http + } + ) + } + } diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala index db884407a3..b61a711bf2 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala @@ -20,10 +20,7 @@ given statusPurposeGet: Get[StatusPurpose] = Get[String].map { case purpose => throw RuntimeException(s"Invalid status purpose - $purpose") } -given statusPurposePut: Put[StatusPurpose] = Put[String].contramap { - case StatusPurpose.Revocation => StatusPurpose.Revocation.str - case StatusPurpose.Suspension => StatusPurpose.Suspension.str -} +given statusPurposePut: Put[StatusPurpose] = Put[String].contramap(_.toString) given urlGet: Get[URL] = Get[String].map(s => URI.create(s).toURL()) given urlPut: Put[URL] = Put[String].contramap(_.toString()) diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialDefinitionRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialDefinitionRepository.scala index 5dda0f6298..bd093d8de0 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialDefinitionRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialDefinitionRepository.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.sql.repository import doobie.* import doobie.implicits.* import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.{CredentialDefinitionRepository, Repository} import org.hyperledger.identus.pollux.core.repository.Repository.* import org.hyperledger.identus.pollux.sql.model.db.{ @@ -31,9 +32,9 @@ case class JdbcCredentialDefinitionRepository(xa: Transactor[ContextAwareTask], ) } - override def findByGuid(guid: UUID): UIO[Option[CredentialDefinition]] = { + override def findByGuid(guid: UUID, resolutionMethod: ResourceResolutionMethod): UIO[Option[CredentialDefinition]] = { CredentialDefinitionSql - .findByGUID(guid) + .findByGUID(guid, resolutionMethod) .transact(xb) .orDie .map( @@ -85,7 +86,8 @@ case class JdbcCredentialDefinitionRepository(xa: Transactor[ContextAwareTask], versionOpt = query.filter.version, tagOpt = query.filter.tag, offset = query.skip, - limit = query.limit + limit = query.limit, + resolutionMethod = query.filter.resolutionMethod ) .transactWallet(xa) .orDie @@ -96,7 +98,8 @@ case class JdbcCredentialDefinitionRepository(xa: Transactor[ContextAwareTask], authorOpt = query.filter.author, nameOpt = query.filter.name, versionOpt = query.filter.version, - tagOpt = query.filter.tag + tagOpt = query.filter.tag, + resolutionMethod = query.filter.resolutionMethod ) .transactWallet(xa) .orDie diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialRepository.scala index 339e2946ad..151ebd9e3f 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialRepository.scala @@ -72,7 +72,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_uri, + | schema_uris, | credential_definition_id, | credential_definition_uri, | credential_format, @@ -98,7 +98,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | ${record.createdAt}, | ${record.updatedAt}, | ${record.thid}, - | ${record.schemaUri}, + | ${record.schemaUris}, | ${record.credentialDefinitionId}, | ${record.credentialDefinitionUri}, | ${record.credentialFormat}, @@ -142,7 +142,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_uri, + | schema_uris, | credential_definition_id, | credential_definition_uri, | credential_format, @@ -214,7 +214,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_uri, + | schema_uris, | credential_definition_id, | credential_definition_uri, | credential_format, @@ -278,7 +278,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_uri, + | schema_uris, | credential_definition_id, | credential_definition_uri, | credential_format, @@ -323,7 +323,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_uri, + | schema_uris, | credential_definition_id, | credential_definition_uri, | credential_format, @@ -502,13 +502,13 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | id, | issue_credential_data, | credential_format, - | schema_uri, + | schema_uris, | credential_definition_uri, | subject_id | FROM public.issue_credential_records | WHERE 1=1 | AND issue_credential_data IS NOT NULL - | AND schema_uri IS NOT NULL + | AND schema_uris IS NOT NULL | AND credential_definition_uri IS NOT NULL | AND credential_format = 'AnonCreds' | AND $inClauseFragment @@ -538,14 +538,14 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ recordId: DidCommID, issue: IssueCredential, issuedRawCredential: String, - schemaUri: Option[String], + schemaUris: Option[List[String]], credentialDefinitionUri: Option[String], protocolState: ProtocolState ): URIO[WalletAccessContext, Unit] = { val cxnIO = sql""" | UPDATE public.issue_credential_records | SET - | schema_uri = $schemaUri, + | schema_uris = $schemaUris, | credential_definition_uri = $credentialDefinitionUri, | issue_credential_data = $issue, | issued_credential_raw = $issuedRawCredential, diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialSchemaRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialSchemaRepository.scala index 606a1e577d..2c51771a68 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialSchemaRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialSchemaRepository.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.sql.repository import doobie.* import doobie.implicits.* import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.{CredentialSchemaRepository, Repository} import org.hyperledger.identus.pollux.core.repository.Repository.* import org.hyperledger.identus.pollux.sql.model.db.{CredentialSchema as CredentialSchemaRow, CredentialSchemaSql} @@ -27,9 +28,9 @@ case class JdbcCredentialSchemaRepository(xa: Transactor[ContextAwareTask], xb: ) } - override def findByGuid(guid: UUID): UIO[Option[CredentialSchema]] = { + override def findByGuid(guid: UUID, resolutionMethod: ResourceResolutionMethod): UIO[Option[CredentialSchema]] = { CredentialSchemaSql - .findByGUID(guid) + .findByGUID(guid, resolutionMethod) .transact(xb) .orDie .map( @@ -38,6 +39,7 @@ case class JdbcCredentialSchemaRepository(xa: Transactor[ContextAwareTask], xb: ) } + // NOTE: this function is not used anywhere override def update(cs: CredentialSchema): URIO[WalletAccessContext, CredentialSchema] = { ZIO.serviceWithZIO[WalletAccessContext](ctx => CredentialSchemaSql @@ -48,13 +50,19 @@ case class JdbcCredentialSchemaRepository(xa: Transactor[ContextAwareTask], xb: ) } - def getAllVersions(id: UUID, author: String): URIO[WalletAccessContext, List[String]] = { + def getAllVersions( + id: UUID, + author: String, + resolutionMethod: ResourceResolutionMethod + ): URIO[WalletAccessContext, List[CredentialSchema]] = { CredentialSchemaSql - .getAllVersions(id, author) + .getAllVersions(id, author, resolutionMethod) .transactWallet(xa) .orDie + .map(_.map(CredentialSchemaRow.toModel)) } + // NOTE: this function is not used anywhere override def delete(guid: UUID): URIO[WalletAccessContext, CredentialSchema] = { CredentialSchemaSql .delete(guid) @@ -81,7 +89,8 @@ case class JdbcCredentialSchemaRepository(xa: Transactor[ContextAwareTask], xb: versionOpt = query.filter.version, tagOpt = query.filter.tags, offset = query.skip, - limit = query.limit + limit = query.limit, + resolutionMethod = query.filter.resolutionMethod ) .transactWallet(xa) .orDie @@ -92,7 +101,8 @@ case class JdbcCredentialSchemaRepository(xa: Transactor[ContextAwareTask], xb: authorOpt = query.filter.author, nameOpt = query.filter.name, versionOpt = query.filter.version, - tagOpt = query.filter.tags + tagOpt = query.filter.tags, + resolutionMethod = query.filter.resolutionMethod ) .transactWallet(xa) .orDie diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala index e6508496f6..f4b27410cf 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala @@ -1,6 +1,8 @@ package org.hyperledger.identus.pollux.sql.repository +import cats.implicits.toFunctorOps import doobie.* +import doobie.free.connection.ConnectionOp import doobie.implicits.* import doobie.postgres.* import doobie.postgres.implicits.* @@ -8,8 +10,7 @@ import org.hyperledger.identus.castor.core.model.did.* import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.repository.CredentialStatusListRepository import org.hyperledger.identus.pollux.vc.jwt.{Issuer, StatusPurpose} -import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, BitStringError, VCStatusList2021} -import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.* +import org.hyperledger.identus.pollux.vc.jwt.revocation.BitString import org.hyperledger.identus.shared.db.ContextAwareTask import org.hyperledger.identus.shared.db.Implicits.* import org.hyperledger.identus.shared.db.Implicits.given @@ -18,7 +19,7 @@ import zio.* import zio.interop.catz.* import java.time.Instant -import java.util.UUID +import java.util.{Objects, UUID} class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: Transactor[Task]) extends CredentialStatusListRepository { @@ -47,9 +48,19 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T .orDie } - def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = { + override def incrementAndGetStatusListIndex( + jwtIssuer: Issuer, + statusListRegistryUrl: String + ): URIO[WalletAccessContext, (UUID, Int)] = { - val cxnIO = + def acquireAdvisoryLock(walletId: WalletId): ConnectionIO[Unit] = { + // Should be specific to this process + val PROCESS_UNIQUE_ID = 235457 + val hashCode = Objects.hash(walletId.hashCode(), PROCESS_UNIQUE_ID) + sql"SELECT pg_advisory_xact_lock($hashCode)".query[Unit].unique.void + } + + def getLatestOfTheWallet: ConnectionIO[Option[CredentialStatusList]] = sql""" | SELECT | id, @@ -62,72 +73,70 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T | last_used_index, | created_at, | updated_at - | FROM public.credential_status_lists order by created_at DESC limit 1 + | FROM public.credential_status_lists + | ORDER BY created_at DESC limit 1 |""".stripMargin .query[CredentialStatusList] .option - cxnIO - .transactWallet(xa) - .orDie - - } + def createNewForTheWallet( + id: UUID, + issuerDid: String, + issued: Instant, + credentialStr: String + ): ConnectionIO[CredentialStatusList] = + sql""" + |INSERT INTO public.credential_status_lists ( + | id, + | issuer, + | issued, + | purpose, + | status_list_credential, + | size, + | last_used_index, + | wallet_id + | ) + |VALUES ( + | $id, + | $issuerDid, + | $issued, + | ${StatusPurpose.Revocation}::public.enum_credential_status_list_purpose, + | $credentialStr::JSON, + | ${BitString.MIN_SL2021_SIZE}, + | 0, + | current_setting('app.current_wallet_id')::UUID + | ) + |RETURNING id, wallet_id, issuer, issued, purpose, status_list_credential, size, last_used_index, created_at, updated_at + """.stripMargin + .query[CredentialStatusList] + .unique - def createNewForTheWallet( - jwtIssuer: Issuer, - statusListRegistryUrl: String - ): URIO[WalletAccessContext, CredentialStatusList] = { - - val id = UUID.randomUUID() - val issued = Instant.now() - val issuerDid = jwtIssuer.did.toString - - val credentialWithEmbeddedProof = for { - bitString <- BitString.getInstance().mapError { - case InvalidSize(message) => new Throwable(message) - case EncodingError(message) => new Throwable(message) - case DecodingError(message) => new Throwable(message) - case IndexOutOfBounds(message) => new Throwable(message) - } - emptyStatusListCredential <- VCStatusList2021 - .build( - vcId = s"$statusListRegistryUrl/credential-status/$id", - revocationData = bitString, - jwtIssuer = jwtIssuer - ) - .mapError(x => new Throwable(x.msg)) - - credentialWithEmbeddedProof <- emptyStatusListCredential.toJsonWithEmbeddedProof - } yield credentialWithEmbeddedProof.spaces2 + def updateLastUsedIndex(statusListId: UUID, lastUsedIndex: Int): ConnectionIO[Int] = + sql""" + | UPDATE public.credential_status_lists + | SET + | last_used_index = $lastUsedIndex, + | updated_at = ${Instant.now()} + | WHERE + | id = $statusListId + |""".stripMargin.update.run (for { - credentialStr <- credentialWithEmbeddedProof - query = sql""" - |INSERT INTO public.credential_status_lists ( - | id, - | issuer, - | issued, - | purpose, - | status_list_credential, - | size, - | last_used_index, - | wallet_id - | ) - |VALUES ( - | $id, - | $issuerDid, - | $issued, - | ${StatusPurpose.Revocation}::public.enum_credential_status_list_purpose, - | $credentialStr::JSON, - | ${BitString.MIN_SL2021_SIZE}, - | 0, - | current_setting('app.current_wallet_id')::UUID - | ) - |RETURNING id, wallet_id, issuer, issued, purpose, status_list_credential, size, last_used_index, created_at, updated_at - """.stripMargin.query[CredentialStatusList].unique - newStatusList <- query.transactWallet(xa) - } yield newStatusList).orDie - + id <- ZIO.succeed(UUID.randomUUID()) + newStatusListVC <- createStatusListVC(jwtIssuer, statusListRegistryUrl, id) + walletCtx <- ZIO.service[WalletAccessContext] + walletId = walletCtx.walletId + cnxIO = for { + _ <- acquireAdvisoryLock(walletId) + maybeStatusList <- getLatestOfTheWallet + statusList <- maybeStatusList match + case Some(csl) if csl.lastUsedIndex < csl.size => cats.free.Free.pure[ConnectionOp, CredentialStatusList](csl) + case _ => createNewForTheWallet(id, jwtIssuer.did.toString, Instant.now(), newStatusListVC) + newIndex = statusList.lastUsedIndex + 1 + _ <- updateLastUsedIndex(statusList.id, newIndex) + } yield (statusList.id, newIndex) + result <- cnxIO.transactWallet(xa) + } yield result).orDie } def allocateSpaceForCredential( @@ -212,9 +221,24 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T } yield () } - def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]] = { + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] = { + val cxnIO = + sql""" + | SELECT + | wallet_id, + | id + | FROM public.credential_status_lists + |""".stripMargin + .query[(WalletId, UUID)] + .to[Seq] + cxnIO + .transact(xb) + .orDie + } - // Might need to add wallet Id in the select query, because I'm selecting all of them + def getCredentialStatusListsWithCreds( + statusListId: UUID + ): URIO[WalletAccessContext, CredentialStatusListWithCreds] = { val cxnIO = sql""" | SELECT @@ -233,42 +257,35 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T | cisl.is_processed | FROM public.credential_status_lists csl | LEFT JOIN public.credentials_in_status_list cisl ON csl.id = cisl.credential_status_list_id + | WHERE + | csl.id = $statusListId |""".stripMargin .query[CredentialStatusListWithCred] .to[List] - - val credentialStatusListsWithCredZio = cxnIO - .transact(xb) - .orDie - - for { - credentialStatusListsWithCred <- credentialStatusListsWithCredZio - } yield { - credentialStatusListsWithCred - .groupBy(_.credentialStatusListId) - .map { case (id, items) => - CredentialStatusListWithCreds( - id, - items.head.walletId, - items.head.issuer, - items.head.issued, - items.head.purpose, - items.head.statusListCredential, - items.head.size, - items.head.lastUsedIndex, - items.map { item => - CredInStatusList( - item.credentialInStatusListId, - item.issueCredentialRecordId, - item.statusListIndex, - item.isCanceled, - item.isProcessed, - ) - } + .transactWallet(xa) + .orDie + + cxnIO.map(items => + CredentialStatusListWithCreds( + statusListId, + items.head.walletId, + items.head.issuer, + items.head.issued, + items.head.purpose, + items.head.statusListCredential, + items.head.size, + items.head.lastUsedIndex, + items.map { item => + CredInStatusList( + item.credentialInStatusListId, + item.issueCredentialRecordId, + item.statusListIndex, + item.isCanceled, + item.isProcessed, ) } - .toList - } + ) + ) } def updateStatusListCredential( diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala index 1e477640c1..71b4cad3dc 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala @@ -459,7 +459,6 @@ class JdbcPresentationRepository( | id = $recordId | AND protocol_state = $from """.stripMargin.update - cxnIO.run .transactWallet(xa) .orDie diff --git a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala index bdae719bd9..346ef2b580 100644 --- a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala +++ b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.core.service +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.sql.repository.JdbcOID4VCIIssuerMetadataRepository import org.hyperledger.identus.sharedtest.containers.PostgresTestContainerSupport import org.hyperledger.identus.test.container.MigrationAspects @@ -16,7 +17,7 @@ object OID4VCIIssuerMetadataServiceSpec extends ZIOSpecDefault, PostgresTestCont private val testEnvironmentLayer = ZLayer.make[OID4VCIIssuerMetadataService]( OID4VCIIssuerMetadataServiceImpl.layer, JdbcOID4VCIIssuerMetadataRepository.layer, - ResourceURIDereferencerImpl.layer, + ResourceUrlResolver.layer, contextAwareTransactorLayer, systemTransactorLayer ) diff --git a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala index 87f290aee7..9ace492546 100644 --- a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala +++ b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala @@ -4,6 +4,7 @@ import com.dimafeng.testcontainers.PostgreSQLContainer import doobie.* import doobie.util.transactor.Transactor import io.getquill.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.sql.model.db.{CredentialDefinition, CredentialDefinitionSql} import org.hyperledger.identus.shared.db.ContextAwareTask import org.hyperledger.identus.shared.db.Implicits.* @@ -93,6 +94,7 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr signatureType <- credentialDefinitionSignatureType supportRevocation <- credentialDefinitionSupportRevocation walletId <- Gen.fromZIO(ZIO.serviceWith[WalletAccessContext](_.walletId)) + resolutionMethod <- Gen.fromIterable(ResourceResolutionMethod.values) } yield CredentialDefinition( guid = id, id = id, @@ -109,6 +111,7 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr schemaId = schemaId, signatureType = signatureType, supportRevocation = supportRevocation, + resolutionMethod = resolutionMethod, walletId = walletId ).withTruncatedTimestamp() @@ -136,7 +139,7 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr expected <- Generators.credentialDefinition.runCollectN(1).map(_.head) _ <- CredentialDefinitionSql.insert(expected).transactWallet(tx) actual <- CredentialDefinitionSql - .findByGUID(expected.guid) + .findByGUID(expected.guid, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -147,7 +150,7 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr .update(updatedExpected) .transactWallet(tx) updatedActual2 <- CredentialDefinitionSql - .findByGUID(expected.id) + .findByGUID(expected.id, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -157,7 +160,7 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr deleted <- CredentialDefinitionSql.delete(expected.guid).transactWallet(tx) notFound <- CredentialDefinitionSql - .findByGUID(expected.guid) + .findByGUID(expected.guid, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -196,17 +199,24 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr firstActual = generatedCredentialDefinitions.head firstExpected <- CredentialDefinitionSql - .findByGUID(firstActual.guid) + .findByGUID(firstActual.guid, firstActual.resolutionMethod) .transactWallet(tx) .map(_.headOption) credentialDefinitionCreated = assert(firstActual)(equalTo(firstExpected.get)) totalCount <- CredentialDefinitionSql.totalCount.transactWallet(tx) - lookupCount <- CredentialDefinitionSql.lookupCount().transactWallet(tx) + lookupCountHttpCredDef <- CredentialDefinitionSql + .lookupCount(resolutionMethod = ResourceResolutionMethod.http) + .transactWallet(tx) + lookupCountDidCredDef <- CredentialDefinitionSql + .lookupCount(resolutionMethod = ResourceResolutionMethod.did) + .transactWallet(tx) totalCountIsN = assert(totalCount)(equalTo(generatedCredentialDefinitions.length)) - lookupCountIsN = assert(lookupCount)(equalTo(generatedCredentialDefinitions.length)) + lookupCountIsN = assert(lookupCountHttpCredDef + lookupCountDidCredDef)( + equalTo(generatedCredentialDefinitions.length) + ) } yield allCredentialDefinitionsHaveUniqueId && allCredentialDefinitionsHaveUniqueConstraint && diff --git a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala index e26b6d5193..91aa5d6388 100644 --- a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala +++ b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala @@ -4,6 +4,7 @@ import com.dimafeng.testcontainers.PostgreSQLContainer import doobie.* import doobie.util.transactor.Transactor import io.getquill.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.sql.model.db.{CredentialSchema, CredentialSchemaSql} import org.hyperledger.identus.shared.db.ContextAwareTask import org.hyperledger.identus.shared.db.Implicits.* @@ -75,6 +76,7 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo authored = OffsetDateTime.now(ZoneOffset.UTC) id = UUID.randomUUID() walletId <- Gen.fromZIO(ZIO.serviceWith[WalletAccessContext](_.walletId)) + resolutionMethod <- Gen.fromIterable(ResourceResolutionMethod.values) } yield CredentialSchema( guid = id, id = id, @@ -86,7 +88,8 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo author = author, authored = authored, tags = tags, - walletId = walletId + walletId = walletId, + resolutionMethod = resolutionMethod ).withTruncatedTimestamp() private val unique = mutable.Set.empty[String] @@ -123,12 +126,12 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo record <- Generators.schema.runCollectN(1).map(_.head).provide(wallet1) _ <- CredentialSchemaSql.insert(record).transactWallet(tx).provide(wallet1) ownRecord <- CredentialSchemaSql - .findByGUID(record.guid) + .findByGUID(record.guid, record.resolutionMethod) .transactWallet(tx) .map(_.headOption) .provide(wallet1) crossRecord <- CredentialSchemaSql - .findByGUID(record.guid) + .findByGUID(record.guid, record.resolutionMethod) .transactWallet(tx) .map(_.headOption) .provide(wallet2) @@ -173,7 +176,7 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo expected <- Generators.schema.runCollectN(1).map(_.head) _ <- CredentialSchemaSql.insert(expected).transactWallet(tx) actual <- CredentialSchemaSql - .findByGUID(expected.guid) + .findByGUID(expected.guid, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -184,7 +187,7 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo .update(updatedExpected) .transactWallet(tx) updatedActual2 <- CredentialSchemaSql - .findByGUID(expected.id) + .findByGUID(expected.id, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -194,7 +197,7 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo deleted <- CredentialSchemaSql.delete(expected.guid).transactWallet(tx) notFound <- CredentialSchemaSql - .findByGUID(expected.guid) + .findByGUID(expected.guid, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -231,17 +234,22 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo firstActual = generatedSchemas.head firstExpected <- CredentialSchemaSql - .findByGUID(firstActual.guid) + .findByGUID(firstActual.guid, firstActual.resolutionMethod) .transactWallet(tx) .map(_.headOption) schemaCreated = assert(firstActual)(equalTo(firstExpected.get)) totalCount <- CredentialSchemaSql.totalCount.transactWallet(tx) - lookupCount <- CredentialSchemaSql.lookupCount().transactWallet(tx) + lookupCountHttpSchemas <- CredentialSchemaSql + .lookupCount(resolutionMethod = ResourceResolutionMethod.http) + .transactWallet(tx) + lookupCountDidSchemas <- CredentialSchemaSql + .lookupCount(resolutionMethod = ResourceResolutionMethod.did) + .transactWallet(tx) totalCountIsN = assert(totalCount)(equalTo(generatedSchemas.length)) - lookupCountIsN = assert(lookupCount)(equalTo(generatedSchemas.length)) + lookupCountIsN = assert(lookupCountHttpSchemas + lookupCountDidSchemas)(equalTo(generatedSchemas.length)) } yield allSchemasHaveUniqueId && allSchemasHaveUniqueConstraint && diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala index fc5a4d8c28..33ebac2154 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala @@ -44,7 +44,7 @@ case class DIDDocumentMetadata( created: Option[Instant] = Option.empty, updated: Option[Instant] = Option.empty, deactivated: Option[Boolean] = Option.empty, - versionId: Option[Instant] = Option.empty, + versionId: Option[Instant] = Option.empty, // TODO: this probably should not be an instant, it should be a string nextUpdate: Option[Instant] = Option.empty, nextVersionId: Option[Instant] = Option.empty, equivalentId: Option[Instant] = Option.empty, diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala index cb7d84dd0b..5638ba028a 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala @@ -30,9 +30,9 @@ case class W3cVerifiableCredentialPayload(payload: W3cCredentialPayload, proof: case class JwtVerifiableCredentialPayload(jwt: JWT) extends VerifiableCredentialPayload -enum StatusPurpose(val str: String) { - case Revocation extends StatusPurpose("Revocation") - case Suspension extends StatusPurpose("Suspension") +enum StatusPurpose { + case Revocation + case Suspension } case class CredentialStatus( @@ -77,9 +77,9 @@ sealed trait CredentialPayload { def maybeValidUntil: Option[Instant] - def issuer: Either[String, CredentialIssuer] + def issuer: String | CredentialIssuer - def maybeCredentialStatus: Option[CredentialStatus] + def maybeCredentialStatus: Option[CredentialStatus | List[CredentialStatus]] def maybeRefreshService: Option[RefreshService] @@ -87,13 +87,16 @@ sealed trait CredentialPayload { def maybeTermsOfUse: Option[Json] - def maybeCredentialSchema: Option[Either[CredentialSchema, List[CredentialSchema]]] + def maybeCredentialSchema: Option[CredentialSchema | List[CredentialSchema]] def credentialSubject: Json def toJwtCredentialPayload: JwtCredentialPayload = JwtCredentialPayload( - iss = issuer.fold(identity, _.id), + iss = issuer match { + case string: String => string + case credentialIssuer: CredentialIssuer => credentialIssuer.id + }, maybeSub = maybeSub, vc = JwtVc( `@context` = `@context`, @@ -137,12 +140,12 @@ sealed trait CredentialPayload { case class JwtVc( `@context`: Set[String], `type`: Set[String], - maybeCredentialSchema: Option[Either[CredentialSchema, List[CredentialSchema]]], + maybeCredentialSchema: Option[CredentialSchema | List[CredentialSchema]], credentialSubject: Json, maybeValidFrom: Option[Instant], maybeValidUntil: Option[Instant], - maybeIssuer: Option[Either[String, CredentialIssuer]], - maybeCredentialStatus: Option[CredentialStatus], + maybeIssuer: Option[String | CredentialIssuer], + maybeCredentialStatus: Option[CredentialStatus | List[CredentialStatus]], maybeRefreshService: Option[RefreshService], maybeEvidence: Option[Json], maybeTermsOfUse: Option[Json] @@ -167,19 +170,19 @@ case class JwtCredentialPayload( override val credentialSubject = vc.credentialSubject override val maybeValidFrom = vc.maybeValidFrom override val maybeValidUntil = vc.maybeValidUntil - override val issuer = vc.maybeIssuer.getOrElse(Left(iss)) + override val issuer = vc.maybeIssuer.getOrElse(iss) } case class W3cCredentialPayload( override val `@context`: Set[String], override val `type`: Set[String], maybeId: Option[String], - issuer: Either[String, CredentialIssuer], + issuer: String | CredentialIssuer, issuanceDate: Instant, maybeExpirationDate: Option[Instant], - override val maybeCredentialSchema: Option[Either[CredentialSchema, List[CredentialSchema]]], + override val maybeCredentialSchema: Option[CredentialSchema | List[CredentialSchema]], override val credentialSubject: Json, - override val maybeCredentialStatus: Option[CredentialStatus], + override val maybeCredentialStatus: Option[CredentialStatus | List[CredentialStatus]], override val maybeRefreshService: Option[RefreshService], override val maybeEvidence: Option[Json], override val maybeTermsOfUse: Option[Json], @@ -236,14 +239,19 @@ object CredentialPayload { ("statusListCredential", credentialStatus.statusListCredential.asJson) ) - implicit val eitherStringOrCredentialIssuerEncoder: Encoder[Either[String, CredentialIssuer]] = { - case Left(value) => Json.fromString(value) - case Right(issuer) => issuer.asJson + implicit val credentialStatusOrListEncoder: Encoder[CredentialStatus | List[CredentialStatus]] = Encoder.instance { + case status: CredentialStatus => Encoder[CredentialStatus].apply(status) + case statusList: List[CredentialStatus] => Encoder[List[CredentialStatus]].apply(statusList) } - implicit val eitherCredentialSchemaOrListEncoder: Encoder[Either[CredentialSchema, List[CredentialSchema]]] = { - case Left(credentialSchema) => credentialSchema.asJson - case Right(credentialSchemas) => credentialSchemas.asJson + implicit val stringOrCredentialIssuerEncoder: Encoder[String | CredentialIssuer] = Encoder.instance { + case string: String => Encoder[String].apply(string) + case credentialIssuer: CredentialIssuer => Encoder[CredentialIssuer].apply(credentialIssuer) + } + + implicit val credentialSchemaOrListEncoder: Encoder[CredentialSchema | List[CredentialSchema]] = Encoder.instance { + case schema: CredentialSchema => Encoder[CredentialSchema].apply(schema) + case schemaList: List[CredentialSchema] => Encoder[List[CredentialSchema]].apply(schemaList) } implicit val w3cCredentialPayloadEncoder: Encoder[W3cCredentialPayload] = @@ -370,13 +378,20 @@ object CredentialPayload { ) } - implicit val eitherStringOrCredentialIssuerDecoder: Decoder[Either[String, CredentialIssuer]] = - Decoder[String].map(Left(_)).or(Decoder[CredentialIssuer].map(Right(_))) + implicit val stringOrCredentialIssuerDecoder: Decoder[String | CredentialIssuer] = + Decoder[String] + .map(schema => schema: String | CredentialIssuer) + .or(Decoder[CredentialIssuer].map(schema => schema: String | CredentialIssuer)) - implicit val eitherCredentialSchemaOrListDecoder: Decoder[Either[CredentialSchema, List[CredentialSchema]]] = + implicit val credentialSchemaOrListDecoder: Decoder[CredentialSchema | List[CredentialSchema]] = Decoder[CredentialSchema] - .map(Left(_)) - .or(Decoder[List[CredentialSchema]].map(Right(_))) + .map(schema => schema: CredentialSchema | List[CredentialSchema]) + .or(Decoder[List[CredentialSchema]].map(schema => schema: CredentialSchema | List[CredentialSchema])) + + implicit val credentialStatusOrListDecoder: Decoder[CredentialStatus | List[CredentialStatus]] = + Decoder[CredentialStatus] + .map(status => status: CredentialStatus | List[CredentialStatus]) + .or(Decoder[List[CredentialStatus]].map(status => status: CredentialStatus | List[CredentialStatus])) implicit val w3cCredentialPayloadDecoder: Decoder[W3cCredentialPayload] = (c: HCursor) => @@ -390,16 +405,16 @@ object CredentialPayload { .as[Set[String]] .orElse(c.downField("type").as[String].map(Set(_))) maybeId <- c.downField("id").as[Option[String]] - issuer <- c.downField("issuer").as[Either[String, CredentialIssuer]] + issuer <- c.downField("issuer").as[String | CredentialIssuer] issuanceDate <- c.downField("issuanceDate").as[Instant] maybeExpirationDate <- c.downField("expirationDate").as[Option[Instant]] maybeValidFrom <- c.downField("validFrom").as[Option[Instant]] maybeValidUntil <- c.downField("validUntil").as[Option[Instant]] maybeCredentialSchema <- c .downField("credentialSchema") - .as[Option[Either[CredentialSchema, List[CredentialSchema]]]] + .as[Option[CredentialSchema | List[CredentialSchema]]] credentialSubject <- c.downField("credentialSubject").as[Json] - maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus]] + maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus | List[CredentialStatus]]] maybeRefreshService <- c.downField("refreshService").as[Option[RefreshService]] maybeEvidence <- c.downField("evidence").as[Option[Json]] maybeTermsOfUse <- c.downField("termsOfUse").as[Option[Json]] @@ -436,15 +451,15 @@ object CredentialPayload { .orElse(c.downField("type").as[String].map(Set(_))) maybeCredentialSchema <- c .downField("credentialSchema") - .as[Option[Either[CredentialSchema, List[CredentialSchema]]]] + .as[Option[CredentialSchema | List[CredentialSchema]]] credentialSubject <- c.downField("credentialSubject").as[Json] - maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus]] + maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus | List[CredentialStatus]]] maybeRefreshService <- c.downField("refreshService").as[Option[RefreshService]] maybeEvidence <- c.downField("evidence").as[Option[Json]] maybeTermsOfUse <- c.downField("termsOfUse").as[Option[Json]] maybeValidFrom <- c.downField("validFrom").as[Option[Instant]] maybeValidUntil <- c.downField("validUntil").as[Option[Instant]] - maybeIssuer <- c.downField("issuer").as[Option[Either[String, CredentialIssuer]]] + maybeIssuer <- c.downField("issuer").as[Option[String | CredentialIssuer]] } yield { JwtVc( `@context` = `@context`, @@ -832,7 +847,7 @@ object JwtCredential { } yield Validation.validateWith(signatureValidation, dateVerification, revocationVerification)((a, _, _) => a) } - private def verifyRevocationStatusJwt(jwt: JWT)(uriResolver: UriResolver): IO[String, Validation[String, Unit]] = { + def verifyRevocationStatusJwt(jwt: JWT)(uriResolver: UriResolver): IO[String, Validation[String, Unit]] = { val decodeJWT = ZIO .fromTry(JwtCirce.decodeRaw(jwt.value, options = JwtOptions(false, false, false))) @@ -842,12 +857,19 @@ object JwtCredential { decodedJWT <- decodeJWT jwtCredentialPayload <- ZIO.fromEither(decode[JwtCredentialPayload](decodedJWT)).mapError(_.getMessage) credentialStatus = jwtCredentialPayload.vc.maybeCredentialStatus - result = credentialStatus.fold(ZIO.succeed(Validation.unit))(status => - CredentialVerification.verifyCredentialStatus(status)(uriResolver) + .map { + { + case status: CredentialStatus => List(status) + case statusList: List[CredentialStatus] => statusList + } + } + .getOrElse(List.empty) + results <- ZIO.collectAll( + credentialStatus.map(status => CredentialVerification.verifyCredentialStatus(status)(uriResolver)) ) + result = Validation.validateAll(results).flatMap(_ => Validation.unit) } yield result - - res.flatten + res } } @@ -888,7 +910,12 @@ object W3CCredential { )(didResolver: DidResolver): IO[String, Validation[String, Unit]] = { JWTVerification.validateEncodedJwt(payload.proof.jwt, proofPurpose)(didResolver: DidResolver)(claim => Validation.fromEither(decode[W3cCredentialPayload](claim).left.map(_.toString)) - )(_.issuer.fold(identity, _.id)) + )(vc => + vc.issuer match { + case string: String => string + case credentialIssuer: CredentialIssuer => credentialIssuer.id + } + ) } def verifyDates(w3cPayload: W3cVerifiableCredentialPayload, leeway: TemporalAmount)(implicit @@ -917,11 +944,20 @@ object W3CCredential { private def verifyRevocationStatusW3c( w3cPayload: W3cVerifiableCredentialPayload, )(uriResolver: UriResolver): IO[String, Validation[String, Unit]] = { - // If credential does not have credential status list, it does not support revocation - // and we assume revocation status is valid. - w3cPayload.payload.maybeCredentialStatus.fold(ZIO.succeed(Validation.unit))(status => - CredentialVerification.verifyCredentialStatus(status)(uriResolver) - ) + val credentialStatus = w3cPayload.payload.maybeCredentialStatus + .map { + { + case status: CredentialStatus => List(status) + case statusList: List[CredentialStatus] => statusList + } + } + .getOrElse(List.empty) + for { + results <- ZIO.collectAll( + credentialStatus.map(status => CredentialVerification.verifyCredentialStatus(status)(uriResolver)) + ) + result = Validation.validateAll(results).flatMap(_ => Validation.unit) + } yield result } def verify(w3cPayload: W3cVerifiableCredentialPayload, options: CredentialVerification.CredentialVerificationOptions)( diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala index 8629b9cc1d..e4ea76cb6b 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala @@ -52,7 +52,7 @@ object VCStatusList2021 { } yield { val claims = JsonObject() .add("type", "StatusList2021".asJson) - .add("statusPurpose", purpose.str.asJson) + .add("statusPurpose", purpose.toString.asJson) .add("encodedList", encodedBitString.asJson) val w3Credential = W3cCredentialPayload( `@context` = Set( @@ -61,7 +61,7 @@ object VCStatusList2021 { ), maybeId = Some(vcId), `type` = Set("VerifiableCredential", "StatusList2021Credential"), - issuer = Left(jwtIssuer.did.toString), + issuer = jwtIssuer.did.toString, issuanceDate = Instant.now, maybeExpirationDate = None, maybeCredentialSchema = None, diff --git a/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala b/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala index 7572353859..8222a4fe64 100644 --- a/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala +++ b/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala @@ -7,6 +7,7 @@ import io.circe.* import io.circe.syntax.* import org.hyperledger.identus.castor.core.model.did.{DID, VerificationRelationship} import org.hyperledger.identus.pollux.vc.jwt.CredentialPayload.Implicits.* +import org.hyperledger.identus.pollux.vc.jwt.StatusPurpose.Revocation import org.hyperledger.identus.shared.http.* import zio.* import zio.prelude.Validation @@ -62,7 +63,11 @@ object JWTVerificationTest extends ZIOSpecDefault { |} |""".stripMargin - private def createJwtCredential(issuer: IssuerWithKey, issuerAsObject: Boolean = false): JWT = { + private def createJwtCredential( + issuer: IssuerWithKey, + issuerAsObject: Boolean = false, + credentialStatus: Option[CredentialStatus | List[CredentialStatus]] = None + ): JWT = { val validFrom = Instant.parse("2010-01-05T00:00:00Z") // ISSUANCE DATE val jwtCredentialNbf = Instant.parse("2010-01-01T00:00:00Z") // ISSUANCE DATE val validUntil = Instant.parse("2010-01-09T00:00:00Z") // EXPIRATION DATE @@ -75,15 +80,15 @@ object JWTVerificationTest extends ZIOSpecDefault { `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), maybeCredentialSchema = None, credentialSubject = Json.obj("id" -> Json.fromString("1")), - maybeCredentialStatus = None, + maybeCredentialStatus = credentialStatus, maybeRefreshService = None, maybeEvidence = None, maybeTermsOfUse = None, maybeValidFrom = Some(validFrom), maybeValidUntil = Some(validUntil), maybeIssuer = Some( - if (issuerAsObject) Right(CredentialIssuer(issuer.issuer.did.toString, "Profile")) - else Left(issuer.issuer.did.toString) + if (issuerAsObject) CredentialIssuer(issuer.issuer.did.toString, "Profile") + else issuer.issuer.did.toString ) ), nbf = jwtCredentialNbf, // ISSUANCE DATE @@ -190,6 +195,51 @@ object JWTVerificationTest extends ZIOSpecDefault { ) ) }, + test("fail verification if proof is valid but credential is revoked at the give status list index given list") { + val revokedStatus: List[CredentialStatus] = List( + org.hyperledger.identus.pollux.vc.jwt.CredentialStatus( + id = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9#1", + statusPurpose = StatusPurpose.Revocation, + `type` = "StatusList2021Entry", + statusListCredential = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9", + statusListIndex = 1 + ), + org.hyperledger.identus.pollux.vc.jwt.CredentialStatus( + id = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9#2", + statusPurpose = StatusPurpose.Suspension, + `type` = "StatusList2021Entry", + statusListCredential = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9", + statusListIndex = 1 + ) + ) + + val urlResolver = new UriResolver { + override def resolve(uri: String): IO[GenericUriResolverError, String] = { + ZIO.succeed(statusListCredentialString) + } + } + + val genericUriResolver = GenericUriResolver( + Map( + "data" -> DataUrlResolver(), + "http" -> urlResolver, + "https" -> urlResolver + ) + ) + val issuer = createUser("did:prism:issuer") + val jwtCredential = createJwtCredential(issuer, credentialStatus = Some(revokedStatus)) + + for { + validation <- JwtCredential.verifyRevocationStatusJwt(jwtCredential)(genericUriResolver) + } yield assertTrue( + validation.fold( + chunk => + chunk.length == 2 && chunk.head.contentEquals("Credential is revoked") && chunk.tail.head + .contentEquals("Credential is revoked"), + _ => false + ) + ) + }, test("validate dates happy path") { val issuer = createUser("did:prism:issuer") val jwtCredential = createJwtCredential(issuer) @@ -211,12 +261,41 @@ object JWTVerificationTest extends ZIOSpecDefault { .decodeJwt(jwtCredential) jwtWithObjectIssuer <- JwtCredential .decodeJwt(jwtCredentialWithObjectIssuer) - jwtWithObjectIssuerIssuer = jwtWithObjectIssuer.vc.maybeIssuer.get.toOption.get.id - jwtIssuer = jwt.vc.maybeIssuer.get.left.toOption.get + jwtWithObjectIssuerIssuer = jwtWithObjectIssuer.vc.maybeIssuer.get match { + case string: String => string + case credentialIssuer: CredentialIssuer => credentialIssuer.id + } + jwtIssuer = jwt.vc.maybeIssuer.get match { + case string: String => string + case credentialIssuer: CredentialIssuer => credentialIssuer.id + } } yield assertTrue( jwtWithObjectIssuerIssuer.equals(jwtIssuer) ) }, + test("validate credential status list") { + val issuer = createUser("did:prism:issuer") + val status = CredentialStatus(id = "id", `type` = "type", statusPurpose = Revocation, 1, "1") + val encodedJwtWithStatusList = createJwtCredential( + issuer, + false, + Some(List(status)) + ) + val econdedJwtWithStatusObject = createJwtCredential(issuer, true, Some(status)) + for { + decodeJwtWithStatusList <- JwtCredential + .decodeJwt(encodedJwtWithStatusList) + decodeJwtWithStatusObject <- JwtCredential + .decodeJwt(econdedJwtWithStatusObject) + statusFromList = decodeJwtWithStatusList.vc.maybeCredentialStatus.map { + case list: List[CredentialStatus] => list.head + case _: CredentialStatus => throw new IllegalStateException("List expected") + }.get + statusFromObjet = decodeJwtWithStatusObject.vc.maybeCredentialStatus.get + } yield assertTrue( + statusFromList.equals(statusFromObjet) + ) + }, test("validate dates should fail given after valid until") { val issuer = createUser("did:prism:issuer") val jwtCredential = createJwtCredential(issuer) diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/http/DataUrlResolver.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/http/DataUrlResolver.scala new file mode 100644 index 0000000000..e5719d0b7b --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/http/DataUrlResolver.scala @@ -0,0 +1,17 @@ +package org.hyperledger.identus.shared.http + +import io.lemonlabs.uri.DataUrl +import zio.* + +class DataUrlResolver extends UriResolver { + override def resolve(dataUrl: String): IO[GenericUriResolverError, String] = { + + DataUrl.parseOption(dataUrl).fold(ZIO.fail(InvalidUri(dataUrl))) { url => + ZIO.succeed(String(url.data, url.mediaType.charset)) + } + } +} + +object DataUrlResolver { + val layer: ULayer[DataUrlResolver] = ZLayer.succeed(new DataUrlResolver) +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala index f8b5741f3a..7b628d66f8 100644 --- a/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala @@ -1,7 +1,13 @@ package org.hyperledger.identus.shared.http -import io.lemonlabs.uri.{DataUrl, Uri, Url, Urn} +import io.lemonlabs.uri.{Uri, Url, Urn} +import org.hyperledger.identus.shared.models.{Failure, PrismEnvelopeData, StatusCode} +import org.hyperledger.identus.shared.utils.Base64Utils import zio.* +import zio.json.* + +import scala.util +import scala.util.Try trait UriResolver { @@ -12,41 +18,54 @@ trait UriResolver { class GenericUriResolver(resolvers: Map[String, UriResolver]) extends UriResolver { override def resolve(uri: String): IO[GenericUriResolverError, String] = { - val parsedUri = Uri.parse(uri) - parsedUri match - case url: Url => - url.schemeOption.fold(ZIO.fail(InvalidUri(uri)))(schema => - resolvers.get(schema).fold(ZIO.fail(UnsupportedUriSchema(schema)))(resolver => resolver.resolve(uri)) - ) - - case Urn(path) => ZIO.fail(InvalidUri(uri)) // Must be a URL - } - -} - -class DataUrlResolver extends UriResolver { - override def resolve(dataUrl: String): IO[GenericUriResolverError, String] = { - - DataUrl.parseOption(dataUrl).fold(ZIO.fail(InvalidUri(dataUrl))) { url => - ZIO.succeed(String(url.data, url.mediaType.charset)) - } + val parsedUri = Uri.parseTry(uri) + + ZIO.debug(s"Resolving resource from uri: $uri") *> + ZIO.fromTry(parsedUri).mapError(_ => InvalidUri(uri)).flatMap { + case url: Url => + url.schemeOption.fold(ZIO.fail(InvalidUri(uri)))(schema => + resolvers.get(schema).fold(ZIO.fail(UnsupportedUriSchema(schema))) { resolver => + resolver.resolve(uri).flatMap { res => + schema match + case "did" => + res.fromJson[PrismEnvelopeData] match + case Right(env) => + ZIO + .fromTry(Try(Base64Utils.decodeUrlToString(env.resource))) + .mapError(_ => DidUriResponseNotEnvelope(uri)) + case Left(err) => + ZIO.debug(s"Failed to parse response as PrismEnvelope: $err") *> + ZIO.debug("Falling back to returning the response as is") *> + ZIO.succeed(res) + case _ => ZIO.succeed(res) + } + } + ) + + case Urn(path) => ZIO.fail(InvalidUri(uri)) // Must be a URL + } } } -sealed trait GenericUriResolverError { +trait GenericUriResolverError(val statusCode: StatusCode, val userFacingMessage: String) extends Failure { + override val namespace: String = "UriResolver" def toThrowable: Throwable = { this match case InvalidUri(uri) => new RuntimeException(s"Invalid URI: $uri") case UnsupportedUriSchema(schema) => new RuntimeException(s"Unsupported URI schema: $schema") - case SchemaSpecificResolutionError(schema, error) => - new RuntimeException(s"Error resolving ${schema} URL: ${error.getMessage}") } } -case class InvalidUri(uri: String) extends GenericUriResolverError +case class DidUriResponseNotEnvelope(uri: String) + extends GenericUriResolverError( + StatusCode.UnprocessableContent, + s"The response of DID uri resolution was not prism envelope: uri=[$uri]" + ) -case class UnsupportedUriSchema(schema: String) extends GenericUriResolverError +case class InvalidUri(uri: String) + extends GenericUriResolverError(StatusCode.UnprocessableContent, s"The URI to dereference is invalid: uri=[$uri]") -case class SchemaSpecificResolutionError(schema: String, error: Throwable) extends GenericUriResolverError +case class UnsupportedUriSchema(schema: String) + extends GenericUriResolverError(StatusCode.UnprocessableContent, s"Unsupported URI schema: $schema") diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala new file mode 100644 index 0000000000..8c3a60e56e --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala @@ -0,0 +1,106 @@ +package org.hyperledger.identus.shared.messaging + +import org.hyperledger.identus.shared.messaging.kafka.{InMemoryMessagingService, ZKafkaMessagingServiceImpl} +import zio.{durationInt, Cause, Duration, EnvironmentTag, RIO, RLayer, Task, URIO, URLayer, ZIO, ZLayer} + +import java.time.Instant +trait MessagingService { + def makeConsumer[K, V](groupId: String)(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Consumer[K, V]] + def makeProducer[K, V]()(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Producer[K, V]] +} + +object MessagingService { + + case class RetryStep(topicName: String, consumerCount: Int, consumerBackoff: Duration, nextTopicName: Option[String]) + + object RetryStep { + def apply(topicName: String, consumerCount: Int, consumerBackoff: Duration, nextTopicName: String): RetryStep = + RetryStep(topicName, consumerCount, consumerBackoff, Some(nextTopicName)) + } + + def consumeWithRetryStrategy[K: EnvironmentTag, V: EnvironmentTag, HR]( + groupId: String, + handler: Message[K, V] => RIO[HR, Unit], + steps: Seq[RetryStep] + )(implicit kSerde: Serde[K], vSerde: Serde[V]): RIO[HR & Producer[K, V] & MessagingService, Unit] = { + for { + messagingService <- ZIO.service[MessagingService] + messageProducer <- ZIO.service[Producer[K, V]] + _ <- ZIO.foreachPar(steps) { step => + ZIO.foreachPar(1 to step.consumerCount)(_ => + for { + consumer <- messagingService.makeConsumer[K, V](groupId) + _ <- consumer + .consume[HR](step.topicName) { m => + for { + // Wait configured backoff before processing message + millisSpentInQueue <- ZIO.succeed(Instant.now().toEpochMilli - m.timestamp) + sleepDelay = step.consumerBackoff.toMillis - millisSpentInQueue + _ <- ZIO.when(sleepDelay > 0)(ZIO.sleep(Duration.fromMillis(sleepDelay))) + _ <- handler(m) + .catchAll { t => + for { + _ <- ZIO.logErrorCause(s"Error processing message: ${m.key} ", Cause.fail(t)) + _ <- step.nextTopicName match + case Some(name) => + messageProducer + .produce(name, m.key, m.value) + .catchAll(t => + ZIO.logErrorCause("Unable to send message to the next topic", Cause.fail(t)) + ) + case None => ZIO.unit + } yield () + } + .catchAllDefect(t => ZIO.logErrorCause(s"Defect processing message: ${m.key} ", Cause.fail(t))) + } yield () + } + .debug + .fork + } yield () + ) + } + } yield () + } + + def consume[K: EnvironmentTag, V: EnvironmentTag, HR]( + groupId: String, + topicName: String, + consumerCount: Int, + handler: Message[K, V] => RIO[HR, Unit] + )(implicit kSerde: Serde[K], vSerde: Serde[V]): RIO[HR & Producer[K, V] & MessagingService, Unit] = + consumeWithRetryStrategy(groupId, handler, Seq(RetryStep(topicName, consumerCount, 0.seconds, None))) + + val serviceLayer: URLayer[MessagingServiceConfig, MessagingService] = + ZLayer + .service[MessagingServiceConfig] + .flatMap(config => + if (config.get.kafkaEnabled) ZKafkaMessagingServiceImpl.layer + else InMemoryMessagingService.layer + ) + + def producerLayer[K: EnvironmentTag, V: EnvironmentTag](implicit + kSerde: Serde[K], + vSerde: Serde[V] + ): RLayer[MessagingService, Producer[K, V]] = ZLayer.fromZIO(for { + messagingService <- ZIO.service[MessagingService] + producer <- messagingService.makeProducer[K, V]() + } yield producer) + + def consumerLayer[K: EnvironmentTag, V: EnvironmentTag](groupId: String)(implicit + kSerde: Serde[K], + vSerde: Serde[V] + ): RLayer[MessagingService, Consumer[K, V]] = ZLayer.fromZIO(for { + messagingService <- ZIO.service[MessagingService] + consumer <- messagingService.makeConsumer[K, V](groupId) + } yield consumer) + +} + +case class Message[K, V](key: K, value: V, offset: Long, timestamp: Long) + +trait Consumer[K, V] { + def consume[HR](topic: String, topics: String*)(handler: Message[K, V] => URIO[HR, Unit]): RIO[HR, Unit] +} +trait Producer[K, V] { + def produce(topic: String, key: K, value: V): Task[Unit] +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala new file mode 100644 index 0000000000..dd63c1a424 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala @@ -0,0 +1,58 @@ +package org.hyperledger.identus.shared.messaging + +import zio.{ULayer, ZLayer} + +import java.time.Duration + +case class MessagingServiceConfig( + connectFlow: ConsumerJobConfig, + issueFlow: ConsumerJobConfig, + presentFlow: ConsumerJobConfig, + didStateSync: ConsumerJobConfig, + statusListSync: ConsumerJobConfig, + inMemoryQueueCapacity: Int, + kafkaEnabled: Boolean, + kafka: Option[KafkaConfig] +) + +final case class ConsumerJobConfig( + consumerCount: Int, + retryStrategy: Option[ConsumerRetryStrategy] +) + +final case class ConsumerRetryStrategy( + maxRetries: Int, + initialDelay: Duration, + maxDelay: Duration +) + +final case class KafkaConfig( + bootstrapServers: String, + consumers: KafkaConsumersConfig +) + +final case class KafkaConsumersConfig( + autoCreateTopics: Boolean, + maxPollRecords: Int, + maxPollInterval: Duration, + pollTimeout: Duration, + rebalanceSafeCommits: Boolean +) + +object MessagingServiceConfig { + + val inMemoryLayer: ULayer[MessagingServiceConfig] = + ZLayer.succeed( + MessagingServiceConfig( + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + 100, + false, + None + ) + ) + +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala new file mode 100644 index 0000000000..94eadf3849 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala @@ -0,0 +1,55 @@ +package org.hyperledger.identus.shared.messaging + +import org.hyperledger.identus.shared.models.WalletId + +import java.nio.charset.StandardCharsets +import java.nio.ByteBuffer +import java.util.UUID + +case class ByteArrayWrapper(ba: Array[Byte]) + +trait Serde[T] { + def serialize(t: T): Array[Byte] + def deserialize(ba: Array[Byte]): T +} + +object Serde { + given byteArraySerde: Serde[ByteArrayWrapper] = new Serde[ByteArrayWrapper] { + override def serialize(t: ByteArrayWrapper): Array[Byte] = t.ba + override def deserialize(ba: Array[Byte]): ByteArrayWrapper = ByteArrayWrapper(ba) + } + + given stringSerde: Serde[String] = new Serde[String] { + override def serialize(t: String): Array[Byte] = t.getBytes() + override def deserialize(ba: Array[Byte]): String = new String(ba, StandardCharsets.UTF_8) + } + + given intSerde: Serde[Int] = new Serde[Int] { + override def serialize(t: Int): Array[Byte] = { + val buffer = java.nio.ByteBuffer.allocate(4) + buffer.putInt(t) + buffer.array() + } + override def deserialize(ba: Array[Byte]): Int = ByteBuffer.wrap(ba).getInt() + } + + given uuidSerde: Serde[UUID] = new Serde[UUID] { + override def serialize(t: UUID): Array[Byte] = { + val buffer = java.nio.ByteBuffer.allocate(16) + buffer.putLong(t.getMostSignificantBits) + buffer.putLong(t.getLeastSignificantBits) + buffer.array() + } + override def deserialize(ba: Array[Byte]): UUID = { + val byteBuffer = ByteBuffer.wrap(ba) + val high = byteBuffer.getLong + val low = byteBuffer.getLong + new UUID(high, low) + } + } + + given walletIdSerde(using uuidSerde: Serde[UUID]): Serde[WalletId] = new Serde[WalletId] { + override def serialize(w: WalletId): Array[Byte] = uuidSerde.serialize(w.toUUID) + override def deserialize(ba: Array[Byte]): WalletId = WalletId.fromUUID(uuidSerde.deserialize(ba)) + } +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala new file mode 100644 index 0000000000..ff1c9e8d76 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala @@ -0,0 +1,20 @@ +package org.hyperledger.identus.shared.messaging + +import zio.json.{DecoderOps, DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} + +import java.nio.charset.StandardCharsets +import java.util.UUID + +case class WalletIdAndRecordId(walletId: UUID, recordId: UUID) + +object WalletIdAndRecordId { + given encoder: JsonEncoder[WalletIdAndRecordId] = DeriveJsonEncoder.gen[WalletIdAndRecordId] + given decoder: JsonDecoder[WalletIdAndRecordId] = DeriveJsonDecoder.gen[WalletIdAndRecordId] + given ser: Serde[WalletIdAndRecordId] = new Serde[WalletIdAndRecordId] { + override def serialize(t: WalletIdAndRecordId): Array[Byte] = t.toJson.getBytes(StandardCharsets.UTF_8) + override def deserialize(ba: Array[Byte]): WalletIdAndRecordId = + new String(ba, StandardCharsets.UTF_8) + .fromJson[WalletIdAndRecordId] + .getOrElse(throw RuntimeException("Deserialization Error WalletIdAndRecordId")) + } +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala new file mode 100644 index 0000000000..54d8c935c2 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala @@ -0,0 +1,146 @@ +package org.hyperledger.identus.shared.messaging.kafka + +import org.hyperledger.identus.shared.messaging.* +import org.hyperledger.identus.shared.messaging.kafka.InMemoryMessagingService.* +import zio.* +import zio.concurrent.ConcurrentMap +import zio.stream.* + +import java.util.concurrent.TimeUnit + +case class ConsumerGroupKey(groupId: GroupId, topic: Topic) + +class InMemoryMessagingService( + topicQueues: ConcurrentMap[Topic, (Queue[Message[_, _]], Ref[Offset])], + queueCapacity: Int, + processedMessagesMap: ConcurrentMap[ + ConsumerGroupKey, + ConcurrentMap[Offset, TimeStamp] + ] +) extends MessagingService { + + override def makeConsumer[K, V](groupId: String)(using kSerde: Serde[K], vSerde: Serde[V]): Task[Consumer[K, V]] = { + ZIO.succeed(new InMemoryConsumer[K, V](groupId, topicQueues, processedMessagesMap)) + } + + override def makeProducer[K, V]()(using kSerde: Serde[K], vSerde: Serde[V]): Task[Producer[K, V]] = + ZIO.succeed(new InMemoryProducer[K, V](topicQueues, queueCapacity)) +} + +class InMemoryConsumer[K, V]( + groupId: GroupId, + topicQueues: ConcurrentMap[Topic, (Queue[Message[_, _]], Ref[Offset])], + processedMessagesMap: ConcurrentMap[ConsumerGroupKey, ConcurrentMap[Offset, TimeStamp]] +) extends Consumer[K, V] { + override def consume[HR](topic: String, topics: String*)(handler: Message[K, V] => URIO[HR, Unit]): RIO[HR, Unit] = { + val allTopics = topic +: topics + def getQueueStream(topic: String): ZStream[Any, Nothing, (String, Message[K, V])] = + ZStream.repeatZIO { + topicQueues.get(topic).flatMap { + case Some((queue, _)) => + ZIO.debug(s"Connected to queue for topic $topic in group $groupId") *> + ZIO.succeed(ZStream.fromQueue(queue).collect { case msg: Message[K, V] @unchecked => (topic, msg) }) + case None => + ZIO.sleep(1.second) *> ZIO.succeed(ZStream.empty) + } + }.flatten + + val streams = allTopics.map(getQueueStream) + ZStream + .mergeAllUnbounded()(streams: _*) + .tap { case (topic, msg) => ZIO.log(s"Processing message in group $groupId, topic:$topic : $msg") } + .filterZIO { case (topic, msg) => + for { + currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) + key = ConsumerGroupKey(groupId, topic) + topicProcessedMessages <- processedMessagesMap.get(key).flatMap { + case Some(map) => ZIO.succeed(map) + case None => + for { + newMap <- ConcurrentMap.empty[Offset, TimeStamp] + _ <- processedMessagesMap.put(key, newMap) + } yield newMap + } + isNew <- topicProcessedMessages + .putIfAbsent(Offset(msg.offset), TimeStamp(currentTime)) + .map(_.isEmpty) + } yield isNew + } + .mapZIO { case (_, msg) => handler(msg) } + .tap(_ => ZIO.log(s"Message processed in group $groupId, topic:$topic")) + .runDrain + } +} + +class InMemoryProducer[K, V]( + topicQueues: ConcurrentMap[Topic, (Queue[Message[_, _]], Ref[Offset])], + queueCapacity: Int +) extends Producer[K, V] { + override def produce(topic: String, key: K, value: V): Task[Unit] = for { + queueAndOffsetRef <- topicQueues.get(topic).flatMap { + case Some(qAndOffSetRef) => ZIO.succeed(qAndOffSetRef) + case None => + for { + newQueue <- Queue.sliding[Message[_, _]](queueCapacity) + newOffSetRef <- Ref.make(Offset(0L)) + _ <- topicQueues.put(topic, (newQueue, newOffSetRef)) + } yield (newQueue, newOffSetRef) + } + (queue, offsetRef) = queueAndOffsetRef + currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) + messageId <- offsetRef.updateAndGet(x => Offset(x.value + 1)) // unique atomic id incremented per topic + _ <- queue.offer(Message(key, value, messageId.value, currentTime)) + } yield () +} + +object InMemoryMessagingService { + type Topic = String + type GroupId = String + + opaque type Offset = Long + object Offset: + def apply(value: Long): Offset = value + extension (id: Offset) def value: Long = id + + opaque type TimeStamp = Long + object TimeStamp: + def apply(value: Long): TimeStamp = value + extension (ts: TimeStamp) def value: Long = ts + + val layer: URLayer[MessagingServiceConfig, MessagingService] = + ZLayer.fromZIO { + for { + config <- ZIO.service[MessagingServiceConfig] + queueMap <- ConcurrentMap.empty[Topic, (Queue[Message[_, _]], Ref[Offset])] + processedMessagesMap <- ConcurrentMap.empty[ConsumerGroupKey, ConcurrentMap[Offset, TimeStamp]] + _ <- cleanupTaskForProcessedMessages(processedMessagesMap) + } yield new InMemoryMessagingService(queueMap, config.inMemoryQueueCapacity, processedMessagesMap) + } + + private def cleanupTaskForProcessedMessages( + processedMessagesMap: ConcurrentMap[ConsumerGroupKey, ConcurrentMap[Offset, TimeStamp]], + maxAge: Duration = 60.minutes // Maximum age for entries + ): UIO[Unit] = { + def cleanupOldEntries(map: ConcurrentMap[Offset, TimeStamp]): UIO[Unit] = for { + currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) + entries <- map.toList + _ <- ZIO.foreachDiscard(entries) { case (key, timestamp) => + if (currentTime - timestamp > maxAge.toMillis) + map.remove(key) *> ZIO.log(s"Removed old entry with key: $key and timestamp: $timestamp") + else + ZIO.unit + } + } yield () + + (for { + entries <- processedMessagesMap.toList + _ <- ZIO.foreachDiscard(entries) { case (key, map) => + ZIO.log(s"Cleaning up entries for group: ${key.groupId} and topic: ${key.topic}") *> + cleanupOldEntries(map) + } + } yield ()) + .repeat(Schedule.spaced(10.minutes)) + .fork + .unit + } +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala new file mode 100644 index 0000000000..9180fc4d62 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala @@ -0,0 +1,136 @@ +package org.hyperledger.identus.shared.messaging.kafka + +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.common.header.Headers +import org.hyperledger.identus.shared.messaging.* +import zio.{Duration, RIO, Task, URIO, URLayer, ZIO, ZLayer} +import zio.kafka.consumer.{ + Consumer as ZKConsumer, + ConsumerSettings as ZKConsumerSettings, + Subscription as ZKSubscription +} +import zio.kafka.producer.{Producer as ZKProducer, ProducerSettings as ZKProducerSettings} +import zio.kafka.serde.{Deserializer as ZKDeserializer, Serializer as ZKSerializer} + +class ZKafkaMessagingServiceImpl( + bootstrapServers: List[String], + autoCreateTopics: Boolean, + maxPollRecords: Int, + maxPollInterval: Duration, + pollTimeout: Duration, + rebalanceSafeCommits: Boolean +) extends MessagingService { + override def makeConsumer[K, V](groupId: String)(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Consumer[K, V]] = + ZIO.succeed( + new ZKafkaConsumerImpl[K, V]( + bootstrapServers, + groupId, + kSerde, + vSerde, + autoCreateTopics, + maxPollRecords, + maxPollInterval, + pollTimeout, + rebalanceSafeCommits + ) + ) + + override def makeProducer[K, V]()(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Producer[K, V]] = + ZIO.succeed(new ZKafkaProducerImpl[K, V](bootstrapServers, kSerde, vSerde)) +} + +object ZKafkaMessagingServiceImpl { + val layer: URLayer[MessagingServiceConfig, MessagingService] = + ZLayer.fromZIO { + for { + config <- ZIO.service[MessagingServiceConfig] + kafkaConfig <- config.kafka match + case Some(cfg) => ZIO.succeed(cfg) + case None => ZIO.dieMessage("Kafka config is undefined") + } yield new ZKafkaMessagingServiceImpl( + kafkaConfig.bootstrapServers.split(',').toList, + kafkaConfig.consumers.autoCreateTopics, + kafkaConfig.consumers.maxPollRecords, + kafkaConfig.consumers.maxPollInterval, + kafkaConfig.consumers.pollTimeout, + kafkaConfig.consumers.rebalanceSafeCommits + ) + } +} + +class ZKafkaConsumerImpl[K, V]( + bootstrapServers: List[String], + groupId: String, + kSerde: Serde[K], + vSerde: Serde[V], + autoCreateTopics: Boolean, + maxPollRecords: Int, + maxPollInterval: Duration, + pollTimeout: Duration, + rebalanceSafeCommits: Boolean +) extends Consumer[K, V] { + private val zkConsumer = ZLayer.scoped( + ZKConsumer.make( + ZKConsumerSettings(bootstrapServers) + .withProperty(ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG, autoCreateTopics.toString) + .withGroupId(groupId) + // 'max.poll.records' default is 500. This is a Kafka property. + .withMaxPollRecords(maxPollRecords) + // 'max.poll.interval.ms' default is 5 minutes. This is a Kafka property. + .withMaxPollInterval(maxPollInterval) // Should be max.poll.records x 'max processing time per record' + // 'pollTimeout' default is 50 millis. This is a ZIO Kafka property. + .withPollTimeout(pollTimeout) + // .withOffsetRetrieval(OffsetRetrieval.Auto(AutoOffsetStrategy.Earliest)) + .withRebalanceSafeCommits(rebalanceSafeCommits) + // .withMaxRebalanceDuration(30.seconds) + ) + ) + + private val zkKeyDeserializer = new ZKDeserializer[Any, K] { + override def deserialize(topic: String, headers: Headers, data: Array[Byte]): RIO[Any, K] = + ZIO.succeed(kSerde.deserialize(data)) + } + + private val zkValueDeserializer = new ZKDeserializer[Any, V] { + override def deserialize(topic: String, headers: Headers, data: Array[Byte]): RIO[Any, V] = + ZIO.succeed(vSerde.deserialize(data)) + } + + override def consume[HR](topic: String, topics: String*)(handler: Message[K, V] => URIO[HR, Unit]): RIO[HR, Unit] = + ZKConsumer + .plainStream(ZKSubscription.topics(topic, topics*), zkKeyDeserializer, zkValueDeserializer) + .provideSomeLayer(zkConsumer) + .mapZIO(record => + handler(Message(record.key, record.value, record.offset.offset, record.timestamp)).as(record.offset) + ) + .aggregateAsync(ZKConsumer.offsetBatches) + .mapZIO(_.commit) + .runDrain +} + +class ZKafkaProducerImpl[K, V](bootstrapServers: List[String], kSerde: Serde[K], vSerde: Serde[V]) + extends Producer[K, V] { + private val zkProducer = ZLayer.scoped( + ZKProducer.make( + ZKProducerSettings(bootstrapServers) + ) + ) + + private val zkKeySerializer = new ZKSerializer[Any, K] { + override def serialize(topic: String, headers: Headers, value: K): RIO[Any, Array[Byte]] = + ZIO.succeed(kSerde.serialize(value)) + } + + private val zkValueSerializer = new ZKSerializer[Any, V] { + override def serialize(topic: String, headers: Headers, value: V): RIO[Any, Array[Byte]] = + ZIO.succeed(vSerde.serialize(value)) + } + + override def produce(topic: String, key: K, value: V): Task[Unit] = + ZKProducer + .produce(topic, key, value, zkKeySerializer, zkValueSerializer) + .tap(metadata => ZIO.logInfo(s"Message produced: ${metadata.offset()}")) + .map(_ => ()) + .provideSome(zkProducer) + +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/models/Failure.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/models/Failure.scala index c2ea5dc53f..b41611d07e 100644 --- a/shared/core/src/main/scala/org/hyperledger/identus/shared/models/Failure.scala +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/models/Failure.scala @@ -38,6 +38,7 @@ object StatusCode { val Unauthorized: StatusCode = StatusCode(401) val Forbidden: StatusCode = StatusCode(403) val NotFound: StatusCode = StatusCode(404) + val Conflict: StatusCode = StatusCode(409) val UnprocessableContent: StatusCode = StatusCode(422) val InternalServerError: StatusCode = StatusCode(500) diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/models/PrismEnvelope.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/models/PrismEnvelope.scala new file mode 100644 index 0000000000..c02df829e3 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/models/PrismEnvelope.scala @@ -0,0 +1,16 @@ +package org.hyperledger.identus.shared.models +import zio.json.* + +trait PrismEnvelope { + val resource: String + val url: String +} + +case class PrismEnvelopeData(resource: String, url: String) extends PrismEnvelope +object PrismEnvelopeData { + given encoder: JsonEncoder[PrismEnvelopeData] = + DeriveJsonEncoder.gen[PrismEnvelopeData] + + given decoder: JsonDecoder[PrismEnvelopeData] = + DeriveJsonDecoder.gen[PrismEnvelopeData] +} diff --git a/tests/integration-tests/README.md b/tests/integration-tests/README.md index 2fef57bf71..4b0e925bcd 100644 --- a/tests/integration-tests/README.md +++ b/tests/integration-tests/README.md @@ -116,10 +116,6 @@ The configuration files are divided into the following sections: * `agents`: contains the configuration for the agents (ICA) that will be started. By default, all agents will be destroyed after the test run is finished. * `roles`: contains the configuration for the roles (Issuer, Holder, Verifier, Admin). A role can be assigned to one or more agents that we set in `agents` section or already running locally or in the cloud. -> You could keep services and agents running for debugging purposes -> by specifying `keep_running = true` for the service or agent -> in the configuration file and setting `TESTCONTAINERS_RYUK_DISABLED` variable to `true`. - Please, check [test/resources/configs/basic.conf](./src/test/resources/configs/basic.conf) for a quick example of a basic configuration. You could explore the `configs` directory for more complex examples. diff --git a/tests/integration-tests/build.gradle.kts b/tests/integration-tests/build.gradle.kts index 07fcfb53a3..7a653b4398 100644 --- a/tests/integration-tests/build.gradle.kts +++ b/tests/integration-tests/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { testImplementation("io.ktor:ktor-server-netty:2.3.0") testImplementation("io.ktor:ktor-client-apache:2.3.0") // RestAPI client - testImplementation("org.hyperledger.identus:cloud-agent-client-kotlin:1.39.0-e077cdd") + testImplementation("org.hyperledger.identus:cloud-agent-client-kotlin:1.39.1-bbcedb1") // Test helpers library testImplementation("io.iohk.atala:atala-automation:0.4.0") // Hoplite for configuration @@ -48,7 +48,7 @@ dependencies { testImplementation("io.iohk.atala.prism.apollo:apollo-jvm:1.3.4") // OID4VCI testImplementation("org.htmlunit:htmlunit:4.3.0") - testImplementation("eu.europa.ec.eudi:eudi-lib-jvm-openid4vci-kt:0.3.2") + testImplementation("eu.europa.ec.eudi:eudi-lib-jvm-openid4vci-kt:0.4.1") } serenity { diff --git a/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt b/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt index 4ea1481a90..e2116d6793 100644 --- a/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt +++ b/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt @@ -15,8 +15,7 @@ import net.serenitybdd.screenplay.Ability import net.serenitybdd.screenplay.Actor import net.serenitybdd.screenplay.HasTeardown import net.serenitybdd.screenplay.Question -import org.hyperledger.identus.client.models.Connection -import org.hyperledger.identus.client.models.IssueCredentialRecord +import org.hyperledger.identus.client.models.* import java.net.URL import java.time.OffsetDateTime @@ -26,7 +25,12 @@ open class ListenToEvents( ) : Ability, HasTeardown { private val server: ApplicationEngine - private val gson = GsonBuilder().registerTypeAdapter(OffsetDateTime::class.java, CustomGsonObjectMapperFactory.OffsetDateTimeTypeAdapter()).create() + private val gson = GsonBuilder() + .registerTypeAdapter( + OffsetDateTime::class.java, + CustomGsonObjectMapperFactory.OffsetDateTimeTypeAdapter(), + ) + .create() var connectionEvents: MutableList = mutableListOf() var credentialEvents: MutableList = mutableListOf() @@ -40,10 +44,34 @@ open class ListenToEvents( val eventString = call.receiveText() val event = gson.fromJson(eventString, Event::class.java) when (event.type) { - TestConstants.EVENT_TYPE_CONNECTION_UPDATED -> connectionEvents.add(gson.fromJson(eventString, ConnectionEvent::class.java)) - TestConstants.EVENT_TYPE_ISSUE_CREDENTIAL_RECORD_UPDATED -> credentialEvents.add(gson.fromJson(eventString, CredentialEvent::class.java)) - TestConstants.EVENT_TYPE_PRESENTATION_UPDATED -> presentationEvents.add(gson.fromJson(eventString, PresentationEvent::class.java)) - TestConstants.EVENT_TYPE_DID_STATUS_UPDATED -> didEvents.add(gson.fromJson(eventString, DidEvent::class.java)) + TestConstants.EVENT_TYPE_CONNECTION_UPDATED -> connectionEvents.add( + gson.fromJson( + eventString, + ConnectionEvent::class.java, + ), + ) + + TestConstants.EVENT_TYPE_ISSUE_CREDENTIAL_RECORD_UPDATED -> credentialEvents.add( + gson.fromJson( + eventString, + CredentialEvent::class.java, + ), + ) + + TestConstants.EVENT_TYPE_PRESENTATION_UPDATED -> presentationEvents.add( + gson.fromJson( + eventString, + PresentationEvent::class.java, + ), + ) + + TestConstants.EVENT_TYPE_DID_STATUS_UPDATED -> didEvents.add( + gson.fromJson( + eventString, + DidEvent::class.java, + ), + ) + else -> { throw IllegalArgumentException("ERROR: unknown event type ${event.type}") } diff --git a/tests/integration-tests/src/test/kotlin/config/services/Agent.kt b/tests/integration-tests/src/test/kotlin/config/services/Agent.kt index f4d7cc0962..a4e97a603d 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Agent.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Agent.kt @@ -16,7 +16,6 @@ data class Agent( @ConfigAlias("prism_node") val prismNode: VerifiableDataRegistry?, val keycloak: Keycloak?, val vault: Vault?, - @ConfigAlias("keep_running") override val keepRunning: Boolean = false, ) : ServiceBase() { override val logServices = listOf("identus-cloud-agent") diff --git a/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt b/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt index d73a8814b5..86027fe3ad 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt @@ -17,7 +17,6 @@ data class Keycloak( val realm: String = "atala-demo", @ConfigAlias("client_id") val clientId: String = "cloud-agent", @ConfigAlias("client_secret") val clientSecret: String = "cloud-agent-secret", - @ConfigAlias("keep_running") override val keepRunning: Boolean = false, @ConfigAlias("compose_file") val keycloakComposeFile: String = "src/test/resources/containers/keycloak.yml", @ConfigAlias("logger_name") val loggerName: String = "keycloak", @ConfigAlias("extra_envs") val extraEnvs: Map = emptyMap(), diff --git a/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt b/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt index 8c598b61d6..9c2483084a 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt @@ -15,7 +15,6 @@ abstract class ServiceBase : Startable { } abstract val container: ComposeContainer - abstract val keepRunning: Boolean open val logServices: List = emptyList() private val logWriters: MutableList = mutableListOf() @@ -41,8 +40,6 @@ abstract class ServiceBase : Startable { logWriters.forEach { it.close() } - if (!keepRunning) { - container.stop() - } + container.stop() } } diff --git a/tests/integration-tests/src/test/kotlin/config/services/Vault.kt b/tests/integration-tests/src/test/kotlin/config/services/Vault.kt index 85f1a02b27..a14a44620b 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Vault.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Vault.kt @@ -14,7 +14,6 @@ import java.io.File data class Vault( @ConfigAlias("http_port") val httpPort: Int, @ConfigAlias("vault_auth_type") val authType: VaultAuthType = VaultAuthType.APP_ROLE, - @ConfigAlias("keep_running") override val keepRunning: Boolean = false, ) : ServiceBase() { private val logger = Logger.get() override val logServices: List = listOf("vault") diff --git a/tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt b/tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt index 2997f567cc..f4fbcdba66 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt @@ -8,7 +8,6 @@ import java.io.File data class VerifiableDataRegistry( @ConfigAlias("http_port") val httpPort: Int, val version: String, - @ConfigAlias("keep_running") override val keepRunning: Boolean = false, ) : ServiceBase() { override val logServices: List = listOf("prism-node") private val vdrComposeFile = "src/test/resources/containers/vdr.yml" diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt index b5026a7163..a25abc6ade 100644 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt @@ -35,7 +35,7 @@ class JwtCredentialSteps { } val credentialOfferRequest = CreateIssueCredentialRecordRequest( - schemaId = schemaId, + schemaId = schemaId?.let { listOf(it) }, claims = claims, issuingDID = did, connectionId = issuer.recall("connection-with-${holder.name}").connectionId, diff --git a/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt index b9589a8754..7b91024a18 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt @@ -1,5 +1,6 @@ package steps.did +import com.google.gson.JsonPrimitive import interactions.Get import interactions.Post import interactions.body @@ -148,7 +149,7 @@ class ManageDidSteps { CreateManagedDidRequestDocumentTemplate( publicKeys = listOf(ManagedDIDKeyTemplate("auth-1", purpose, curve)), services = listOf( - Service("https://foo.bar.com", listOf("LinkedDomains"), Json("https://foo.bar.com/")), + Service("https://foo.bar.com", listOf("LinkedDomains"), JsonPrimitive("https://foo.bar.com/")), ), ), ) diff --git a/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt index e9e590d319..0bbfa21380 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt @@ -1,5 +1,7 @@ package steps.did +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive import interactions.Get import interactions.Post import interactions.body @@ -51,7 +53,7 @@ class UpdateDidSteps { addService = Service( id = serviceId, type = listOf("LinkedDomains"), - serviceEndpoint = Json("https://service.com/"), + serviceEndpoint = JsonPrimitive("https://service.com/") as JsonElement, ), ) actor.remember("newServiceId", serviceId) @@ -77,7 +79,7 @@ class UpdateDidSteps { updateService = UpdateManagedDIDServiceAction( id = serviceId, type = listOf("LinkedDomains"), - serviceEndpoint = Json(newServiceUrl), + serviceEndpoint = JsonPrimitive(newServiceUrl) as JsonElement, ), ) @@ -124,7 +126,8 @@ class UpdateDidSteps { ) val didKey = "${actor.recall("shortFormDid")}#$newDidKeyId" val didDocument = SerenityRest.lastResponse().get().didDocument!! - val verificationMethodNotPresent = didDocument.verificationMethod!!.map { it.id }.none { it == didKey } + val verificationMethodNotPresent = + didDocument.verificationMethod!!.map { it.id }.none { it == didKey } verificationMethodNotPresent && when (purpose) { Purpose.ASSERTION_METHOD -> didDocument.assertionMethod!!.none { it == didKey } @@ -189,7 +192,16 @@ class UpdateDidSteps { Get.resource("/dids/${actor.recall("shortFormDid")}"), ) val service = SerenityRest.lastResponse().get().didDocument!!.service!! - service.any { it.serviceEndpoint.value.contains(serviceUrl) } + service.any { serviceEntry -> + val serviceEndpoint = serviceEntry.serviceEndpoint!! + if (serviceEndpoint.isJsonPrimitive) { + serviceEndpoint.asString.contains(serviceUrl) + } else if (serviceEndpoint.isJsonArray) { + serviceEndpoint.asJsonArray.any { it.asString.contains(serviceUrl) } + } else { + false + } + } }, equalTo(true), ), diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt index 218f233004..6a2e906361 100644 --- a/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt @@ -5,6 +5,7 @@ import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.JWK import eu.europa.ec.eudi.openid4vci.* import interactions.Post +import interactions.body import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get @@ -40,16 +41,13 @@ class IssueCredentialSteps { issuer.recall("longFormDid") } issuer.attemptsTo( - Post.to("/oid4vci/issuers/${credentialIssuer.id}/credential-offers") - .with { - it.body( - CredentialOfferRequest( - credentialConfigurationId = configurationId, - issuingDID = did, - claims = claims, - ), - ) - }, + Post.to("/oid4vci/issuers/${credentialIssuer.id}/credential-offers").body( + CredentialOfferRequest( + credentialConfigurationId = configurationId, + issuingDID = did, + claims = claims, + ), + ), Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_CREATED), ) val offerUri = SerenityRest.lastResponse().get().credentialOffer @@ -99,7 +97,7 @@ class IssueCredentialSteps { val issuer = holder.recall("eudiIssuer") val authorizedRequest = holder.recall("eudiAuthorizedRequest") val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialOffer.credentialConfigurationIdentifiers.first(), null) - val submissionOutcome = with(issuer) { + val authRequestAndsubmissionOutcome = with(issuer) { when (authorizedRequest) { is AuthorizedRequest.NoProofRequired -> throw Exception("Not supported yet") is AuthorizedRequest.ProofRequired -> runBlocking { @@ -110,7 +108,7 @@ class IssueCredentialSteps { } }.getOrThrow() } - holder.remember("eudiSubmissionOutcome", submissionOutcome) + holder.remember("eudiSubmissionOutcome", authRequestAndsubmissionOutcome.second) } @Then("{actor} sees credential issued successfully from CredentialEndpoint") diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt index 470babb703..8a53c311f9 100644 --- a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt @@ -1,20 +1,37 @@ package steps.oid4vci +import com.google.gson.JsonObject import common.CredentialSchema -import interactions.* -import io.cucumber.java.en.* +import interactions.Delete +import interactions.Get +import interactions.Post +import interactions.body +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus -import org.hyperledger.identus.client.models.* +import org.apache.http.HttpStatus.SC_OK +import org.hyperledger.identus.client.models.CreateCredentialConfigurationRequest +import org.hyperledger.identus.client.models.CredentialFormat +import org.hyperledger.identus.client.models.CredentialIssuer +import org.hyperledger.identus.client.models.IssuerMetadata +import java.util.UUID class ManageCredentialConfigSteps { @Given("{actor} has {string} credential configuration created from {}") fun issuerHasExistingCredentialConfig(issuer: Actor, configurationId: String, schema: CredentialSchema) { ManageIssuerSteps().issuerHasExistingCredentialIssuer(issuer) - issuerCreateCredentialConfiguration(issuer, schema, configurationId) + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Get("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations/$configurationId"), + ) + if (SerenityRest.lastResponse().statusCode != SC_OK) { + issuerCreateCredentialConfiguration(issuer, schema, configurationId) + } } @When("{actor} uses {} to create a credential configuration {string}") @@ -22,17 +39,15 @@ class ManageCredentialConfigSteps { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") val schemaGuid = issuer.recall(schema.name) val baseUrl = issuer.recall("baseUrl") + issuer.attemptsTo( - Post.to("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations") - .with { - it.body( - CreateCredentialConfigurationRequest( - configurationId = configurationId, - format = CredentialFormat.JWT_VC_JSON, - schemaId = "$baseUrl/schema-registry/schemas/$schemaGuid/schema", - ), - ) - }, + Post.to("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations").body( + CreateCredentialConfigurationRequest( + configurationId = configurationId, + format = CredentialFormat.JWT_VC_JSON, + schemaId = "$baseUrl/schema-registry/schemas/$schemaGuid/schema", + ), + ), Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_CREATED), ) } @@ -42,7 +57,75 @@ class ManageCredentialConfigSteps { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") issuer.attemptsTo( Delete("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations/$configurationId"), - Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @When("{actor} deletes a non existent {} credential configuration") + fun issuerDeletesANonExistentCredentialConfiguration(issuer: Actor, configurationId: String) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Delete("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations/$configurationId"), + ) + } + + @When("{actor} creates a new credential configuration request") + fun issuerCreatesANewConfigurationRequest(issuer: Actor) { + val credentialConfiguration = JsonObject() + issuer.remember("credentialConfiguration", credentialConfiguration) + } + + @When("{actor} uses {} issuer id for credential configuration") + fun issuerUsesIssuerId(issuer: Actor, issuerId: String) { + if (issuerId == "existing") { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.remember("credentialConfigurationId", credentialIssuer.id) + } else if (issuerId == "wrong") { + issuer.remember("credentialConfigurationId", UUID.randomUUID().toString()) + } + } + + @When("{actor} adds '{}' configuration id for credential configuration request") + fun issuerAddsConfigurationIdToCredentialConfigurationRequest(issuer: Actor, configurationId: String) { + val credentialIssuer = issuer.recall("credentialConfiguration") + val configurationIdProperty = if (configurationId == "null") { + null + } else { + configurationId + } + credentialIssuer.addProperty("configurationId", configurationIdProperty) + } + + @When("{actor} adds '{}' format for credential configuration request") + fun issuerAddsFormatToCredentialConfigurationRequest(issuer: Actor, format: String) { + val credentialIssuer = issuer.recall("credentialConfiguration") + val formatProperty = if (format == "null") { + null + } else { + format + } + credentialIssuer.addProperty("format", formatProperty) + } + + @When("{actor} adds '{}' schemaId for credential configuration request") + fun issuerAddsSchemaIdToCredentialConfigurationRequest(issuer: Actor, schema: String) { + val credentialIssuer = issuer.recall("credentialConfiguration") + val schemaIdProperty = if (schema == "null") { + null + } else { + val baseUrl = issuer.recall("baseUrl") + val schemaGuid = issuer.recall(schema) + "$baseUrl/schema-registry/schemas/$schemaGuid/schema" + } + credentialIssuer.addProperty("schemaId", schemaIdProperty) + } + + @When("{actor} sends the create a credential configuration request") + fun issuerSendsTheCredentialConfigurationRequest(issuer: Actor) { + val credentialConfiguration = issuer.recall("credentialConfiguration") + val credentialIssuerId = issuer.recall("credentialConfigurationId").toString() + issuer.attemptsTo( + Post.to("/oid4vci/issuers/$credentialIssuerId/credential-configurations").body(credentialConfiguration), ) } @@ -51,7 +134,7 @@ class ManageCredentialConfigSteps { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") issuer.attemptsTo( Get("/oid4vci/issuers/${credentialIssuer.id}/.well-known/openid-credential-issuer"), - Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) val metadata = SerenityRest.lastResponse().get() val credConfig = metadata.credentialConfigurationsSupported[configurationId]!! @@ -65,11 +148,19 @@ class ManageCredentialConfigSteps { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") issuer.attemptsTo( Get("/oid4vci/issuers/${credentialIssuer.id}/.well-known/openid-credential-issuer"), - Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) val metadata = SerenityRest.lastResponse().get() issuer.attemptsTo( Ensure.that(metadata.credentialConfigurationsSupported.keys).doesNotContain(configurationId), ) } + + @Then("{actor} should see that create credential configuration has failed with '{}' status code and '{}' detail") + fun issuerShouldSeeCredentialConfigurationRequestHasFailed(issuer: Actor, statusCode: Int, errorDetail: String) { + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(statusCode), + Ensure.that(SerenityRest.lastResponse().body.asString()).contains(errorDetail), + ) + } } diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt index 6fd2f2a3b1..e78408078f 100644 --- a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt @@ -1,7 +1,15 @@ package steps.oid4vci -import interactions.* -import io.cucumber.java.en.* +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import interactions.Delete +import interactions.Get +import interactions.Patch +import interactions.Post +import interactions.body +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure import net.serenitybdd.rest.SerenityRest @@ -9,16 +17,26 @@ import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus import org.apache.http.HttpStatus.SC_CREATED import org.apache.http.HttpStatus.SC_OK -import org.hyperledger.identus.client.models.* +import org.hyperledger.identus.client.models.AuthorizationServer +import org.hyperledger.identus.client.models.CreateCredentialIssuerRequest +import org.hyperledger.identus.client.models.CredentialIssuer +import org.hyperledger.identus.client.models.CredentialIssuerPage +import org.hyperledger.identus.client.models.IssuerMetadata +import org.hyperledger.identus.client.models.PatchAuthorizationServer +import org.hyperledger.identus.client.models.PatchCredentialIssuerRequest class ManageIssuerSteps { - private val UPDATE_AUTH_SERVER_URL = "http://example.com" - private val UPDATE_AUTH_SERVER_CLIENT_ID = "foo" - private val UPDATE_AUTH_SERVER_CLIENT_SECRET = "bar" + companion object { + private const val UPDATE_AUTH_SERVER_URL = "http://example.com" + private const val UPDATE_AUTH_SERVER_CLIENT_ID = "foo" + private const val UPDATE_AUTH_SERVER_CLIENT_SECRET = "bar" + } @Given("{actor} has an existing oid4vci issuer") fun issuerHasExistingCredentialIssuer(issuer: Actor) { - issuerCreateCredentialIssuer(issuer) + if (!issuer.recallAll().containsKey("oid4vciCredentialIssuer")) { + issuerCreateCredentialIssuer(issuer) + } } @When("{actor} creates an oid4vci issuer") @@ -50,7 +68,7 @@ class ManageIssuerSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) val matchedIssuers = SerenityRest.lastResponse().get().contents!! - .filter { it.id == credentialIssuer.id } + .filter { it.id.toString() == credentialIssuer.id } issuer.attemptsTo( Ensure.that(matchedIssuers).hasSize(1), ) @@ -69,19 +87,29 @@ class ManageIssuerSteps { fun issuerUpdateCredentialIssuer(issuer: Actor) { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") issuer.attemptsTo( - Patch.to("/oid4vci/issuers/${credentialIssuer.id}") - .with { - it.body( - PatchCredentialIssuerRequest( - authorizationServer = PatchAuthorizationServer( - url = UPDATE_AUTH_SERVER_URL, - clientId = UPDATE_AUTH_SERVER_CLIENT_ID, - clientSecret = UPDATE_AUTH_SERVER_CLIENT_SECRET, - ), - ), - ) - }, - Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + Patch.to("/oid4vci/issuers/${credentialIssuer.id}").body( + PatchCredentialIssuerRequest( + authorizationServer = PatchAuthorizationServer( + url = UPDATE_AUTH_SERVER_URL, + clientId = UPDATE_AUTH_SERVER_CLIENT_ID, + clientSecret = UPDATE_AUTH_SERVER_CLIENT_SECRET, + ), + ), + ), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @When("{actor} tries to update the oid4vci issuer '{}' property using '{}' value") + fun issuerTriesToUpdateTheOID4VCIIssuer(issuer: Actor, property: String, value: String) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + val body = JsonObject() + val propertyValue = if (value == "null") { null } else { value } + body.addProperty(property, propertyValue) + + val gson = GsonBuilder().serializeNulls().create() + issuer.attemptsTo( + Patch.to("/oid4vci/issuers/${credentialIssuer.id}").body(gson.toJson(body)), ) } @@ -94,6 +122,60 @@ class ManageIssuerSteps { ) } + @When("{actor} tries to create oid4vci issuer with '{}', '{}', '{}' and '{}'") + fun issuerTriesToCreateOIDCIssuer( + issuer: Actor, + id: String, + url: String, + clientId: String, + clientSecret: String, + ) { + val idProperty = if (id == "null") { + null + } else { + id + } + val urlProperty = if (url == "null") { + null + } else { + url + } + val clientIdProperty = if (clientId == "null") { + null + } else { + clientId + } + val clientSecretProperty = if (clientSecret == "null") { + null + } else { + clientSecret + } + + val body = JsonObject() + val authorizationServer = JsonObject() + + body.addProperty("id", idProperty) + body.add("authorizationServer", authorizationServer) + + authorizationServer.addProperty("url", urlProperty) + authorizationServer.addProperty("clientId", clientIdProperty) + authorizationServer.addProperty("clientSecret", clientSecretProperty) + + val gson = GsonBuilder().serializeNulls().create() + issuer.attemptsTo( + Post.to("/oid4vci/issuers").body(gson.toJson(body)), + ) + } + + @Then("{actor} should see the oid4vci '{}' http status response with '{}' detail") + fun issuerShouldSeeTheOIDC4VCIError(issuer: Actor, httpStatus: Int, errorDetail: String) { + SerenityRest.lastResponse().body.prettyPrint() + issuer.attemptsTo( + Ensure.that(SerenityRest.lastResponse().statusCode).isEqualTo(httpStatus), + Ensure.that(SerenityRest.lastResponse().body.asString()).contains(errorDetail), + ) + } + @Then("{actor} sees the oid4vci issuer updated with new values") fun issuerSeesUpdatedCredentialIssuer(issuer: Actor) { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") @@ -102,7 +184,7 @@ class ManageIssuerSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), ) val updatedIssuer = SerenityRest.lastResponse().get().contents!! - .find { it.id == credentialIssuer.id }!! + .find { it.id.toString() == credentialIssuer.id }!! issuer.attemptsTo( Ensure.that(updatedIssuer.authorizationServerUrl).isEqualTo(UPDATE_AUTH_SERVER_URL), ) @@ -129,7 +211,7 @@ class ManageIssuerSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), ) val matchedIssuers = SerenityRest.lastResponse().get().contents!! - .filter { it.id == credentialIssuer.id } + .filter { it.id.toString() == credentialIssuer.id } issuer.attemptsTo( Ensure.that(matchedIssuers).isEmpty(), ) @@ -143,4 +225,11 @@ class ManageIssuerSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_NOT_FOUND), ) } + + @Then("{actor} should see the update oid4vci issuer returned '{}' http status") + fun issuerShouldSeeTheUpdateOID4VCIIssuerReturnedHttpStatus(issuer: Actor, statusCode: Int) { + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(statusCode), + ) + } } diff --git a/tests/integration-tests/src/test/resources/containers/agent.yml b/tests/integration-tests/src/test/resources/containers/agent.yml index 5d3048eea3..74bf287ada 100644 --- a/tests/integration-tests/src/test/resources/containers/agent.yml +++ b/tests/integration-tests/src/test/resources/containers/agent.yml @@ -42,6 +42,8 @@ services: REST_SERVICE_URL: POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: API_KEY_ENABLED: + STATUS_LIST_SYNC_TRIGGER_RECURRENCE_DELAY: 5 seconds + DID_STATE_SYNC_TRIGGER_RECURRENCE_DELAY: 5 seconds # Secret storage configuration SECRET_STORAGE_BACKEND: VAULT_ADDR: "http://host.docker.internal:${VAULT_HTTP_PORT}" @@ -52,9 +54,13 @@ services: KEYCLOAK_CLIENT_ID: KEYCLOAK_CLIENT_SECRET: KEYCLOAK_UMA_AUTO_UPGRADE_RPT: true # no configurable at the moment + # Kafka Messaging Service + DEFAULT_KAFKA_ENABLED: true depends_on: postgres: condition: service_healthy + init-kafka: + condition: service_healthy ports: - "${AGENT_DIDCOMM_PORT}:${AGENT_DIDCOMM_PORT}" - "${AGENT_HTTP_PORT}:${AGENT_HTTP_PORT}" @@ -72,3 +78,91 @@ services: # Extra hosts for Linux networking extra_hosts: - "host.docker.internal:host-gateway" + zookeeper: + image: confluentinc/cp-zookeeper:latest + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + # ports: + # - 22181:2181 + + kafka: + image: confluentinc/cp-kafka:latest + depends_on: + - zookeeper + # ports: + # - 29092:29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: false + healthcheck: + test: + [ + "CMD", + "kafka-topics", + "--list", + "--bootstrap-server", + "localhost:9092", + ] + interval: 5s + timeout: 10s + retries: 5 + + init-kafka: + image: confluentinc/cp-kafka:latest + depends_on: + kafka: + condition: service_healthy + entrypoint: ["/bin/sh", "-c"] + command: | + " + # blocks until kafka is reachable + kafka-topics --bootstrap-server kafka:9092 --list + echo -e 'Creating kafka topics' + + # Connect + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-DLQ --replication-factor 1 --partitions 1 + + # Issue + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-DLQ --replication-factor 1 --partitions 1 + + # Present + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-DLQ --replication-factor 1 --partitions 1 + + # DID Publication State Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-did-state --replication-factor 1 --partitions 5 + + # Status List Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-status-list --replication-factor 1 --partitions 5 + + tail -f /dev/null + " + healthcheck: + test: + [ + "CMD-SHELL", + "kafka-topics --bootstrap-server kafka:9092 --list | grep -q 'sync-status-list'", + ] + interval: 5s + timeout: 10s + retries: 5 diff --git a/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature b/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature index 2f30658ad8..53d010c2c5 100644 --- a/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature +++ b/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature @@ -1,20 +1,20 @@ @oid4vci Feature: Issue JWT Credentials using OID4VCI authorization code flow -Background: + Background: Given Issuer has a published DID for JWT And Issuer has published STUDENT_SCHEMA schema And Issuer has an existing oid4vci issuer And Issuer has "StudentProfile" credential configuration created from STUDENT_SCHEMA -Scenario: Issuing credential with published PRISM DID + Scenario: Issuing credential with published PRISM DID When Issuer creates an offer using "StudentProfile" configuration with "short" form DID And Holder receives oid4vci offer from Issuer And Holder resolves oid4vci issuer metadata and login via front-end channel And Holder presents the access token with JWT proof on CredentialEndpoint Then Holder sees credential issued successfully from CredentialEndpoint -Scenario: Issuing credential with unpublished PRISM DID + Scenario: Issuing credential with unpublished PRISM DID When Issuer creates an offer using "StudentProfile" configuration with "long" form DID And Holder receives oid4vci offer from Issuer And Holder resolves oid4vci issuer metadata and login via front-end channel diff --git a/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature b/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature index 3253069abe..cacdeb6cdb 100644 --- a/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature +++ b/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature @@ -1,16 +1,39 @@ @oid4vci Feature: Manage OID4VCI credential configuration -Background: + Background: Given Issuer has a published DID for JWT And Issuer has published STUDENT_SCHEMA schema And Issuer has an existing oid4vci issuer -Scenario: Successfully create credential configuration - When Issuer uses STUDENT_SCHEMA to create a credential configuration "StudentProfile" + Scenario: Successfully create credential configuration + Given Issuer has "StudentProfile" credential configuration created from STUDENT_SCHEMA Then Issuer sees the "StudentProfile" configuration on IssuerMetadata endpoint -Scenario: Successfully delete credential configuration + Scenario: Successfully delete credential configuration Given Issuer has "StudentProfile" credential configuration created from STUDENT_SCHEMA When Issuer deletes "StudentProfile" credential configuration Then Issuer cannot see the "StudentProfile" configuration on IssuerMetadata endpoint + + Scenario Outline: Create configuration with expect code + When Issuer creates a new credential configuration request + And Issuer uses issuer id for credential configuration + And Issuer adds '' configuration id for credential configuration request + And Issuer adds '' format for credential configuration request + And Issuer adds '' schemaId for credential configuration request + And Issuer sends the create a credential configuration request + Then Issuer should see that create credential configuration has failed with '' status code and '' detail + Examples: + | issuerId | configurationId | format | schemaId | httpStatus | errorDetail | description | + | wrong | StudentProfile | jwt_vc_json | STUDENT_SCHEMA | 404 | There is no credential issue | wrong issuer id | + | existing | null | jwt_vc_json | STUDENT_SCHEMA | 400 | configurationId | null configuration id | + | existing | StudentProfile | null | STUDENT_SCHEMA | 400 | format | null format | + | existing | StudentProfile | wrong-format | STUDENT_SCHEMA | 400 | format | wrong format | + | existing | StudentProfile | jwt_vc_json | null | 400 | schemaId | null schema | + | existing | StudentProfile | jwt_vc_json | malformed-schema | 400 | | malformed schema | + | existing | StudentProfile | jwt_vc_json | STUDENT_SCHEMA | 201 | | right values | + | existing | StudentProfile | jwt_vc_json | STUDENT_SCHEMA | 409 | Duplicated credential | duplicated configuration id | + + Scenario: Delete non existent credential configuration + When Issuer deletes a non existent "NonExistentProfile" credential configuration + Then Issuer should see that create credential configuration has failed with '404' status code and 'There is no credential configuration' detail diff --git a/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature b/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature index d2b6bd4aa6..6259934824 100644 --- a/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature +++ b/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature @@ -1,19 +1,40 @@ @oid4vci Feature: Manage OID4VCI credential issuer -Scenario: Successfully create credential issuer + Scenario: Successfully create credential issuer When Issuer creates an oid4vci issuer Then Issuer sees the oid4vci issuer exists on the agent And Issuer sees the oid4vci issuer on IssuerMetadata endpoint -Scenario: Successfully update credential issuer + Scenario: Successfully update credential issuer Given Issuer has an existing oid4vci issuer When Issuer updates the oid4vci issuer Then Issuer sees the oid4vci issuer updated with new values And Issuer sees the oid4vci IssuerMetadata endpoint updated with new values -Scenario: Successfully delete credential issuer + Scenario: Successfully delete credential issuer Given Issuer has an existing oid4vci issuer When Issuer deletes the oid4vci issuer Then Issuer cannot see the oid4vci issuer on the agent And Issuer cannot see the oid4vci IssuerMetadata endpoint + + Scenario Outline: Create issuer with expect response + When Issuer tries to create oid4vci issuer with '', '', '' and '' + Then Issuer should see the oid4vci '' http status response with '' detail + Examples: + | id | url | clientId | clientSecret | httpStatus | errorDetail | description | + | null | null | null | null | 400 | authorizationServer.url | null values | + | null | malformed | id | secret | 400 | Relative URL 'malformed' is not | malformed url | + | null | http://example.com | id | null | 400 | authorizationServer.clientSecret | null client secret | + | null | http://example.com | null | secret | 400 | authorizationServer.clientId | null client id | + | null | null | id | secret | 400 | authorizationServer.url | null url | + | 4048ef76-749d-4296-8c6c-07c8a20733a0 | http://example.com | id | secret | 201 | | right values | + | 4048ef76-749d-4296-8c6c-07c8a20733a0 | http://example.com | id | secret | 500 | | duplicated id | + + Scenario Outline: Update issuer with expect response + Given Issuer has an existing oid4vci issuer + When Issuer tries to update the oid4vci issuer '' property using '' value + Then Issuer should see the oid4vci '' http status response with '' detail + Examples: + | property | value | httpStatus | errorDetail | description | + | url | malformed | 404 | | Invalid URL | diff --git a/tests/performance-tests/agent-performance-tests-k6/.env b/tests/performance-tests/agent-performance-tests-k6/.env index 5d67c0931e..24d8296cbe 100644 --- a/tests/performance-tests/agent-performance-tests-k6/.env +++ b/tests/performance-tests/agent-performance-tests-k6/.env @@ -1,3 +1,3 @@ -AGENT_VERSION=1.39.0-SNAPSHOT -PRISM_NODE_VERSION=2.3.0 +AGENT_VERSION=1.39.1-SNAPSHOT +PRISM_NODE_VERSION=2.5.0 VAULT_DEV_ROOT_TOKEN_ID=root diff --git a/tests/performance-tests/agent-performance-tests-k6/package.json b/tests/performance-tests/agent-performance-tests-k6/package.json index 3286629382..65d5b5f020 100644 --- a/tests/performance-tests/agent-performance-tests-k6/package.json +++ b/tests/performance-tests/agent-performance-tests-k6/package.json @@ -26,7 +26,7 @@ "webpack": "webpack" }, "dependencies": { - "@hyperledger/identus-cloud-agent-client-ts": "^1.39.0-e077cdd", + "@hyperledger/identus-cloud-agent-client-ts": "^1.39.1-bbcedb1", "uuid": "^9.0.0" } } diff --git a/tests/performance-tests/agent-performance-tests-k6/yarn.lock b/tests/performance-tests/agent-performance-tests-k6/yarn.lock index e743505b63..736d3f9bc0 100644 --- a/tests/performance-tests/agent-performance-tests-k6/yarn.lock +++ b/tests/performance-tests/agent-performance-tests-k6/yarn.lock @@ -993,10 +993,10 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@hyperledger/identus-cloud-agent-client-ts@^1.39.0-e077cdd": - version "1.39.0-e077cdd" - resolved "https://npm.pkg.github.com/download/@hyperledger/identus-cloud-agent-client-ts/1.39.0-e077cdd/06803b9bd2fa7d63805f83df22250882e84c94dd#06803b9bd2fa7d63805f83df22250882e84c94dd" - integrity sha512-3FSz2WlrykyF5LqnrI+wcbrY33i8CeyBNQmEYZ9fp84JL3qhWvnF92dBIS9qpgAQY/qIg5vHvL7RIRNrAxQfOw== +"@hyperledger/identus-cloud-agent-client-ts@^1.39.1-bbcedb1": + version "1.39.1-bbcedb1" + resolved "https://npm.pkg.github.com/download/@hyperledger/identus-cloud-agent-client-ts/1.39.1-bbcedb1/88aaeabfc4d2d8949e21014c2a5c9297ed055d42#88aaeabfc4d2d8949e21014c2a5c9297ed055d42" + integrity sha512-FjYV4HN5H/LD/v6dOw/vMaqU3f8v1IKzEMtUfj9qmLHVmr1FwxkZWSj6wE27I+sY/0sGAPCF/rPbvO27UWdtYQ== dependencies: es6-promise "^4.2.4" url-parse "^1.4.3"