diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b4fb5ea65d..dd72d47379 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,27 +1 @@ -# Castor: -/castor/ @patlo-iog @yshyn-iohk - -# Connect: -/connect/ @bvoiturier @FabioPinheiro @mineme0110 - -# Pollux: -/pollux/ @patlo-iog @CryptoKnightIOG @mineme0110 - -# Cloud Agent: -/cloud-agent/ @bvoiturier @yshyn-iohk @patlo-iog - -# CI pipelines: -/.github/ @mineme0110 @patlo-iog - -# PRISM Node: -/prism-node/ @shotexa - -# Shared: -/shared/ @patlo-iog @FabioPinheiro @mineme0110 @yshyn-iohk @bvoiturier @shotexa @CryptoKnightIOG - -# E2E tests: -/tests/ @todorkoleviohk @amagyar-iohk @yshyn-iohk @patlo-iog @mineme0110 - -# Docs: -/docs/ @bvoiturier @yshyn-iohk -*.md @petevielhaber +* @hyperledger/identus-maintainers diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000000..2872000dd8 --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ +-Dquill.macro.log=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 0907a99844..9a3be86fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# [1.38.0](https://github.com/hyperledger/identus-cloud-agent/compare/cloud-agent-v1.37.0...cloud-agent-v1.38.0) (2024-07-15) + + +### Bug Fixes + +* Move InMemory classes to the test moduels ([#1240](https://github.com/hyperledger/identus-cloud-agent/issues/1240)) ([823057a](https://github.com/hyperledger/identus-cloud-agent/commit/823057adaa6127eca80dba4df123f07098d34f65)) +* move mocks into the test modules ([#1236](https://github.com/hyperledger/identus-cloud-agent/issues/1236)) ([df83026](https://github.com/hyperledger/identus-cloud-agent/commit/df83026704980e071f7aa158634da20fbc2527c3)) +* use Put and Get for DID in doobie statement ([#1250](https://github.com/hyperledger/identus-cloud-agent/issues/1250)) ([fc1cf51](https://github.com/hyperledger/identus-cloud-agent/commit/fc1cf5157f5503143c23da54c8ea6fe78a776640)) +* Wallet Management Error Handling ([#1248](https://github.com/hyperledger/identus-cloud-agent/issues/1248)) ([cfd5101](https://github.com/hyperledger/identus-cloud-agent/commit/cfd5101f18276b9f59830c47c0d7fa64b30662db)) + + +### Features + +* upgrade docusaurus and semantic-release packages ([de53f1d](https://github.com/hyperledger/identus-cloud-agent/commit/de53f1db15a25e4d66cba1b191fc6e591b42284b)) + # [1.37.0](https://github.com/hyperledger/identus-cloud-agent/compare/cloud-agent-v1.36.1...cloud-agent-v1.37.0) (2024-07-01) diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index f25d5d9208..be74257280 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -422,34 +422,34 @@ MIT | [The MIT License (MIT)](https://opensource.org/licenses/MIT) | [com.dimafe Public Domain | [Public Domain, per Creative Commons CC0](http://creativecommons.org/publicdomain/zero/1.0/) | [org.hdrhistogram # HdrHistogram # 2.1.12](http://hdrhistogram.github.io/HdrHistogram/) | Public Domain | [Public Domain, per Creative Commons CC0](http://creativecommons.org/publicdomain/zero/1.0/) | [org.latencyutils # LatencyUtils # 2.0.3](http://latencyutils.github.io/LatencyUtils/) | none specified | []() | [net.jcip # jcip-annotations # 1.0](http://jcip.net/) | -none specified | []() | [org.hyperledger # castor-core_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # cloud-agent-wallet-api_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # connect-core_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # connect-sql-doobie_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # event-notification_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-agent-core_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-agent-didcommx_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-data-models_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-connection_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-coordinate-mediation_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-invitation_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-issue-credential_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-mailbox_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-outofband-login_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-present-proof_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-report-problem_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-revocation-notification_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-routing-2-0_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-protocol-trust-ping_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-resolver_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # mercury-verifiable-credentials_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # pollux-anoncreds_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # pollux-core_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # pollux-sd-jwt_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # pollux-sql-doobie_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # pollux-vc-jwt_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # prism-node-client_3 # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # shared # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # shared-crypto # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | -none specified | []() | [org.hyperledger # shared-test # 1.36.1-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # castor-core_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # cloud-agent-wallet-api_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # connect-core_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # connect-sql-doobie_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # event-notification_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-agent-core_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-agent-didcommx_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-data-models_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-connection_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-coordinate-mediation_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-invitation_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-issue-credential_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-mailbox_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-outofband-login_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-present-proof_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-report-problem_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-revocation-notification_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-routing-2-0_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-protocol-trust-ping_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-resolver_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # mercury-verifiable-credentials_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # pollux-anoncreds_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # pollux-core_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # pollux-sd-jwt_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # pollux-sql-doobie_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # pollux-vc-jwt_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # prism-node-client_3 # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # shared # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # shared-crypto # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | +none specified | []() | [org.hyperledger # shared-test # 1.37.0-SNAPSHOT](https://github.com/hyperledger/identus-cloud-agent) | diff --git a/build.sbt b/build.sbt index ddf3a5750c..48824dfe14 100644 --- a/build.sbt +++ b/build.sbt @@ -32,7 +32,6 @@ inThisBuild( "-feature", "-deprecation", "-unchecked", - "-Dquill.macro.log=false", // disable quill macro logs "-Wunused:all", "-Wconf:any:warning", // TODO: change unused imports to errors, Wconf configuration string is different from scala 2, figure out how! // TODO "-feature", diff --git a/cloud-agent/service/api/http/cloud-agent-openapi-spec.yaml b/cloud-agent/service/api/http/cloud-agent-openapi-spec.yaml index 588b1873af..fd0c0735af 100644 --- a/cloud-agent/service/api/http/cloud-agent-openapi-spec.yaml +++ b/cloud-agent/service/api/http/cloud-agent-openapi-spec.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Identus Cloud Agent API Reference - version: 1.37.0 + version: 1.38.0 description: |2 The Identus Cloud Agent API facilitates the integration and management of self-sovereign identity capabilities within applications. @@ -5441,7 +5441,7 @@ components: type: string description: The date and time when the issue credential record was created. format: date-time - example: '2024-07-01T08:50:32.358710422Z' + example: '2024-07-15T12:05:35.865441004Z' updatedAt: type: string description: The date and time when the issue credential record was last @@ -6213,7 +6213,7 @@ components: type: string description: Issuance timestamp of status list credential format: date-time - example: '2024-07-01T08:50:32.396776613Z' + example: '2024-07-15T12:05:35.908034143Z' credentialSubject: $ref: '#/components/schemas/CredentialSubject' proof: diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/Modules.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/Modules.scala index 6f0dc8996f..492fb55b7c 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/Modules.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/Modules.scala @@ -38,7 +38,7 @@ import org.hyperledger.identus.iam.authentication.oidc.{ KeycloakConfig, KeycloakEntity } -import org.hyperledger.identus.iam.authorization.core.PermissionManagement +import org.hyperledger.identus.iam.authorization.core.PermissionManagementService import org.hyperledger.identus.iam.authorization.keycloak.admin.KeycloakPermissionManagementService import org.hyperledger.identus.pollux.vc.jwt.{DidResolver as JwtDidResolver, PrismDidResolver} import org.hyperledger.identus.shared.crypto.Apollo @@ -103,7 +103,7 @@ object AppModule { ) val keycloakAuthenticatorLayer: RLayer[ - AppConfig & WalletManagementService & Client & PermissionManagement.Service[KeycloakEntity], + AppConfig & WalletManagementService & Client & PermissionManagementService[KeycloakEntity], KeycloakAuthenticator ] = ZLayer.fromZIO { @@ -113,7 +113,7 @@ object AppModule { if (!isEnabled) KeycloakAuthenticatorImpl.disabled else ZLayer.makeSome[ - AppConfig & WalletManagementService & Client & PermissionManagement.Service[KeycloakEntity], + AppConfig & WalletManagementService & Client & PermissionManagementService[KeycloakEntity], KeycloakAuthenticator ]( KeycloakConfig.layer, @@ -125,14 +125,14 @@ object AppModule { }.flatten val keycloakPermissionManagementLayer - : RLayer[AppConfig & WalletManagementService & Client, PermissionManagement.Service[KeycloakEntity]] = { + : RLayer[AppConfig & WalletManagementService & Client, PermissionManagementService[KeycloakEntity]] = { ZLayer.fromZIO { ZIO .serviceWith[AppConfig](_.agent.authentication.keycloak.enabled) .map { isEnabled => if (!isEnabled) KeycloakPermissionManagementService.disabled else - ZLayer.makeSome[AppConfig & WalletManagementService & Client, PermissionManagement.Service[KeycloakEntity]]( + ZLayer.makeSome[AppConfig & WalletManagementService & Client, PermissionManagementService[KeycloakEntity]]( KeycloakClientImpl.authzClientLayer, KeycloakClientImpl.layer, KeycloakConfig.layer, diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobError.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobError.scala index 3ac9e438b9..e1e5539c3a 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobError.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobError.scala @@ -1,7 +1,7 @@ package org.hyperledger.identus.agent.server.jobs import org.hyperledger.identus.mercury.HttpResponse -import org.hyperledger.identus.shared.models._ +import org.hyperledger.identus.shared.models.* sealed trait BackgroundJobError( override val statusCode: org.hyperledger.identus.shared.models.StatusCode, 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 5ed92b4441..c16c4143c6 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 @@ -6,8 +6,10 @@ 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 -import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.RecordIdNotFound +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 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 15fc1b22d7..c2d321ecd3 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 @@ -55,7 +55,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { private type MESSAGING_RESOURCES = DidOps & DIDResolver & HttpClient val presentProofExchanges: ZIO[RESOURCES, Throwable, Unit] = { - val presentProofDidComExchange = for { + for { presentationService <- ZIO.service[PresentationService] config <- ZIO.service[AppConfig] records <- presentationService @@ -72,7 +72,6 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { .foreachPar(records)(performPresentProofExchange) .withParallelism(config.pollux.presentationBgJobProcessingParallelism) } yield () - presentProofDidComExchange } private def counterMetric(key: String) = Metric @@ -81,16 +80,19 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { private def performPresentProofExchange(record: PresentationRecord): URIO[RESOURCES, Unit] = aux(record) - .tapError({ - (error: PresentationError | DIDSecretStorageError | BackgroundJobError | CredentialServiceError | - CastorDIDResolutionError | GetManagedDIDError | Failure) => - ZIO.logErrorCause( - s"Present Proof - Error processing record: ${record.id}", - Cause.fail(error) - ) - }) - .catchAll(e => ZIO.logErrorCause(s"Present Proof - Error processing record: ${record.id} ", Cause.fail(e))) - .catchAllDefect(d => ZIO.logErrorCause(s"Present Proof - Defect processing record: ${record.id}", Cause.fail(d))) + .catchAll { + case ex: Failure => + ZIO + .service[PresentationService] + .flatMap(_.reportProcessingFailure(record.id, Some(ex))) + .catchAll(ex => + ZIO.logErrorCause(s"PresentBackgroundJobs - Fail to recover from ${record.id}", Cause.fail(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] = { import org.hyperledger.identus.pollux.core.model.PresentationRecord.ProtocolState.* diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala index 5c416a4d19..6f3426ecb4 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala @@ -3,8 +3,8 @@ package org.hyperledger.identus.iam.authentication.oidc import org.hyperledger.identus.agent.walletapi.model.EntityRole import org.hyperledger.identus.iam.authentication.AuthenticationError import org.hyperledger.identus.iam.authentication.AuthenticationError.AuthenticationMethodNotEnabled -import org.hyperledger.identus.iam.authorization.core.PermissionManagement -import org.hyperledger.identus.iam.authorization.core.PermissionManagement.Error.PermissionNotAvailable +import org.hyperledger.identus.iam.authorization.core.PermissionManagementService +import org.hyperledger.identus.iam.authorization.core.PermissionManagementServiceError.PermissionNotAvailable import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletAdministrationContext} import zio.* @@ -13,7 +13,7 @@ import java.util.UUID class KeycloakAuthenticatorImpl( client: KeycloakClient, keycloakConfig: KeycloakConfig, - keycloakPermissionService: PermissionManagement.Service[KeycloakEntity], + keycloakPermissionService: PermissionManagementService[KeycloakEntity], ) extends KeycloakAuthenticator { override def isEnabled: Boolean = keycloakConfig.enabled @@ -48,7 +48,7 @@ class KeycloakAuthenticatorImpl( .listWalletPermissions(entity) .mapError { case PermissionNotAvailable(_, msg) => AuthenticationError.InvalidCredentials(msg) - case e => AuthenticationError.UnexpectedError(e.message) + case e => AuthenticationError.UnexpectedError(e.userFacingMessage) } .flatMap { case head +: Nil => ZIO.succeed(head) @@ -68,7 +68,7 @@ class KeycloakAuthenticatorImpl( .listWalletPermissions(entity) .provide(ZLayer.succeed(WalletAdministrationContext.Admin())) .mapBoth( - e => AuthenticationError.UnexpectedError(e.message), + e => AuthenticationError.UnexpectedError(e.userFacingMessage), wallets => WalletAdministrationContext.SelfService(wallets) ) @@ -90,7 +90,7 @@ class KeycloakAuthenticatorImpl( object KeycloakAuthenticatorImpl { val layer: RLayer[ - KeycloakClient & KeycloakConfig & PermissionManagement.Service[KeycloakEntity], + KeycloakClient & KeycloakConfig & PermissionManagementService[KeycloakEntity], KeycloakAuthenticator ] = ZLayer.fromFunction(KeycloakAuthenticatorImpl(_, _, _)) diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/DefaultPermissionManagementService.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/DefaultPermissionManagementService.scala index 53a33371f7..18121922b1 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/DefaultPermissionManagementService.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/DefaultPermissionManagementService.scala @@ -2,31 +2,38 @@ package org.hyperledger.identus.iam.authorization import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, Entity} import org.hyperledger.identus.iam.authentication.oidc.KeycloakEntity -import org.hyperledger.identus.iam.authorization.core.PermissionManagement -import org.hyperledger.identus.iam.authorization.core.PermissionManagement.Error +import org.hyperledger.identus.iam.authorization.core.{PermissionManagementService, PermissionManagementServiceError} import org.hyperledger.identus.shared.models.{WalletAdministrationContext, WalletId} import zio.* class DefaultPermissionManagementService( - entityPermission: PermissionManagement.Service[Entity], - keycloakPermission: PermissionManagement.Service[KeycloakEntity] -) extends PermissionManagement.Service[BaseEntity] { + entityPermission: PermissionManagementService[Entity], + keycloakPermission: PermissionManagementService[KeycloakEntity] +) extends PermissionManagementService[BaseEntity] { - def grantWalletToUser(walletId: WalletId, entity: BaseEntity): ZIO[WalletAdministrationContext, Error, Unit] = { + def grantWalletToUser( + walletId: WalletId, + entity: BaseEntity + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Unit] = { entity match { case entity: Entity => entityPermission.grantWalletToUser(walletId, entity) case kcEntity: KeycloakEntity => keycloakPermission.grantWalletToUser(walletId, kcEntity) } } - def revokeWalletFromUser(walletId: WalletId, entity: BaseEntity): ZIO[WalletAdministrationContext, Error, Unit] = { + def revokeWalletFromUser( + walletId: WalletId, + entity: BaseEntity + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Unit] = { entity match { case entity: Entity => entityPermission.revokeWalletFromUser(walletId, entity) case kcEntity: KeycloakEntity => keycloakPermission.revokeWalletFromUser(walletId, kcEntity) } } - def listWalletPermissions(entity: BaseEntity): ZIO[WalletAdministrationContext, Error, Seq[WalletId]] = { + def listWalletPermissions( + entity: BaseEntity + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Seq[WalletId]] = { entity match { case entity: Entity => entityPermission.listWalletPermissions(entity) case kcEntity: KeycloakEntity => keycloakPermission.listWalletPermissions(kcEntity) @@ -37,8 +44,8 @@ class DefaultPermissionManagementService( object DefaultPermissionManagementService { def layer: URLayer[ - PermissionManagement.Service[KeycloakEntity] & PermissionManagement.Service[Entity], - PermissionManagement.Service[BaseEntity] + PermissionManagementService[KeycloakEntity] & PermissionManagementService[Entity], + PermissionManagementService[BaseEntity] ] = ZLayer.fromFunction(DefaultPermissionManagementService(_, _)) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/EntityPermissionManagementService.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/EntityPermissionManagementService.scala index 6b6f5ebba8..a27b8fd84c 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/EntityPermissionManagementService.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/EntityPermissionManagementService.scala @@ -2,38 +2,45 @@ package org.hyperledger.identus.iam.authorization.core import org.hyperledger.identus.agent.walletapi.model.Entity import org.hyperledger.identus.agent.walletapi.service.EntityService -import org.hyperledger.identus.iam.authorization.core.PermissionManagement.Error -import org.hyperledger.identus.iam.authorization.core.PermissionManagement.Error.{ServiceError, WalletNotFoundById} +import org.hyperledger.identus.iam.authorization.core.PermissionManagementServiceError.* import org.hyperledger.identus.shared.models.{WalletAdministrationContext, WalletId} import zio.* import scala.language.implicitConversions -class EntityPermissionManagementService(entityService: EntityService) extends PermissionManagement.Service[Entity] { +class EntityPermissionManagementService(entityService: EntityService) extends PermissionManagementService[Entity] { - override def grantWalletToUser(walletId: WalletId, entity: Entity): ZIO[WalletAdministrationContext, Error, Unit] = { + override def grantWalletToUser( + walletId: WalletId, + entity: Entity + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Unit] = { for { _ <- ZIO .serviceWith[WalletAdministrationContext](_.isAuthorized(walletId)) - .filterOrFail(identity)(Error.WalletNotFoundById(walletId)) + .filterOrFail(identity)(WalletNotFoundById(walletId)) _ <- entityService.assignWallet(entity.id, walletId.toUUID).orDieAsUnmanagedFailure } yield () } - override def revokeWalletFromUser(walletId: WalletId, entity: Entity): ZIO[WalletAdministrationContext, Error, Unit] = - ZIO.fail(Error.ServiceError(s"Revoking wallet permission for an Entity is not yet supported.")) + override def revokeWalletFromUser( + walletId: WalletId, + entity: Entity + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Unit] = + ZIO.fail(ServiceError(s"Revoking wallet permission for an Entity is not yet supported.")) - override def listWalletPermissions(entity: Entity): ZIO[WalletAdministrationContext, Error, Seq[WalletId]] = { + override def listWalletPermissions( + entity: Entity + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Seq[WalletId]] = { val walletId = WalletId.fromUUID(entity.walletId) ZIO .serviceWith[WalletAdministrationContext](_.isAuthorized(walletId)) - .filterOrFail(identity)(Error.WalletNotFoundById(walletId)) + .filterOrFail(identity)(WalletNotFoundById(walletId)) .as(Seq(walletId)) } } object EntityPermissionManagementService { - val layer: URLayer[EntityService, PermissionManagement.Service[Entity]] = + val layer: URLayer[EntityService, PermissionManagementService[Entity]] = ZLayer.fromFunction(EntityPermissionManagementService(_)) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/PermissionManagement.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/PermissionManagement.scala index ef10b8bbca..8b13789179 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/PermissionManagement.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/PermissionManagement.scala @@ -1,39 +1 @@ -package org.hyperledger.identus.iam.authorization.core -import org.hyperledger.identus.agent.walletapi.model.BaseEntity -import org.hyperledger.identus.shared.models.{WalletAdministrationContext, WalletId} -import zio.* - -import java.util.UUID - -object PermissionManagement { - trait Service[E <: BaseEntity] { - def grantWalletToUser(walletId: WalletId, entity: E): ZIO[WalletAdministrationContext, Error, Unit] - def revokeWalletFromUser(walletId: WalletId, entity: E): ZIO[WalletAdministrationContext, Error, Unit] - def listWalletPermissions(entity: E): ZIO[WalletAdministrationContext, Error, Seq[WalletId]] - } - - sealed trait Error(val message: String) - - object Error { - case class UserNotFoundById(userId: UUID, cause: Option[Throwable] = None) - extends Error(s"User $userId is not found" + cause.map(t => s" Cause: ${t.getMessage}")) - case class WalletNotFoundByUserId(userId: UUID) extends Error(s"Wallet for user $userId is not found") - - case class WalletNotFoundById(walletId: WalletId) extends Error(s"Wallet not found by ${walletId.toUUID}") - - case class WalletResourceNotFoundById(walletId: WalletId) - extends Error(s"Wallet resource not found by ${walletId.toUUID}") - - case class PermissionNotFoundById(userId: UUID, walletId: WalletId, walletResourceId: String) - extends Error( - s"Permission not found by userId: $userId, walletId: ${walletId.toUUID}, walletResourceId: $walletResourceId" - ) - - case class PermissionNotAvailable(userId: UUID, cause: String) extends Error(cause) - - case class UnexpectedError(cause: Throwable) extends Error(cause.getMessage) - - case class ServiceError(cause: String) extends Error(cause) - } -} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/PermissionManagementService.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/PermissionManagementService.scala new file mode 100644 index 0000000000..ce309c1561 --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/PermissionManagementService.scala @@ -0,0 +1,19 @@ +package org.hyperledger.identus.iam.authorization.core + +import org.hyperledger.identus.agent.walletapi.model.BaseEntity +import org.hyperledger.identus.shared.models.{WalletAdministrationContext, WalletId} +import zio.* + +trait PermissionManagementService[E <: BaseEntity] { + def grantWalletToUser( + walletId: WalletId, + entity: E + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Unit] + def revokeWalletFromUser( + walletId: WalletId, + entity: E + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Unit] + def listWalletPermissions( + entity: E + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Seq[WalletId]] +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/PermissionManagementServiceError.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/PermissionManagementServiceError.scala new file mode 100644 index 0000000000..7b8ca64b4a --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/core/PermissionManagementServiceError.scala @@ -0,0 +1,50 @@ +package org.hyperledger.identus.iam.authorization.core + +import org.hyperledger.identus.shared.models.{Failure, StatusCode, WalletId} + +import java.util.UUID + +sealed trait PermissionManagementServiceError( + val statusCode: StatusCode, + val userFacingMessage: String +) extends Failure { + override val namespace: String = "PermissionManagementServiceError" +} + +object PermissionManagementServiceError { + + case class UserNotFoundById(userId: UUID, cause: Option[Throwable] = None) + extends PermissionManagementServiceError( + StatusCode.BadRequest, + s"User $userId is not found" + cause.map(t => s" Cause: ${t.getMessage}") + ) + + case class WalletNotFoundByUserId(userId: UUID) + extends PermissionManagementServiceError( + StatusCode.BadRequest, + s"Wallet for user $userId is not found" + ) + + case class WalletNotFoundById(walletId: WalletId) + extends PermissionManagementServiceError( + StatusCode.BadRequest, + s"Wallet not found by ${walletId.toUUID}" + ) + + case class WalletResourceNotFoundById(walletId: WalletId) + extends PermissionManagementServiceError( + StatusCode.BadRequest, + s"Wallet resource not found by ${walletId.toUUID}" + ) + + case class PermissionNotFoundById(userId: UUID, walletId: WalletId, walletResourceId: String) + extends PermissionManagementServiceError( + StatusCode.BadRequest, + s"Permission not found by userId: $userId, walletId: ${walletId.toUUID}, walletResourceId: $walletResourceId" + ) + + case class PermissionNotAvailable(userId: UUID, cause: String) + extends PermissionManagementServiceError(StatusCode.BadRequest, cause) + + case class ServiceError(cause: String) extends PermissionManagementServiceError(StatusCode.InternalServerError, cause) +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala index b06586d798..d4eb6963cc 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala @@ -3,9 +3,8 @@ package org.hyperledger.identus.iam.authorization.keycloak.admin import org.hyperledger.identus.agent.walletapi.model.Wallet import org.hyperledger.identus.agent.walletapi.service.WalletManagementService import org.hyperledger.identus.iam.authentication.oidc.{KeycloakClient, KeycloakEntity} -import org.hyperledger.identus.iam.authorization.core.PermissionManagement -import org.hyperledger.identus.iam.authorization.core.PermissionManagement.Error -import org.hyperledger.identus.iam.authorization.core.PermissionManagement.Error.* +import org.hyperledger.identus.iam.authorization.core.{PermissionManagementService, PermissionManagementServiceError} +import org.hyperledger.identus.iam.authorization.core.PermissionManagementServiceError.* import org.hyperledger.identus.shared.models.{WalletAdministrationContext, WalletId} import org.keycloak.authorization.client.AuthzClient import org.keycloak.representations.idm.authorization.{ResourceRepresentation, UmaPermissionRepresentation} @@ -19,7 +18,7 @@ case class KeycloakPermissionManagementService( authzClient: AuthzClient, keycloakClient: KeycloakClient, walletManagementService: WalletManagementService -) extends PermissionManagement.Service[KeycloakEntity] { +) extends PermissionManagementService[KeycloakEntity] { private def walletResourceName(walletId: WalletId) = s"wallet-${walletId.toUUID.toString}" @@ -28,60 +27,58 @@ case class KeycloakPermissionManagementService( override def grantWalletToUser( walletId: WalletId, entity: KeycloakEntity - ): ZIO[WalletAdministrationContext, PermissionManagement.Error, Unit] = { + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Unit] = { for { _ <- walletManagementService .findWallet(walletId) .someOrFail(WalletNotFoundById(walletId)) walletResourceOpt <- findWalletResource(walletId) - .logError("Error while finding wallet resource") - .mapError(UnexpectedError.apply) walletResource <- ZIO .fromOption(walletResourceOpt) .orElse(createWalletResource(walletId)) - .logError("Error while creating wallet resource") - .mapError(UnexpectedError.apply) _ <- ZIO.log(s"Wallet resource created ${walletResource.toString}") permission <- createResourcePermission(walletResource.getId, entity.id.toString) - .mapError(UnexpectedError.apply) _ <- ZIO.log(s"Permission created with id ${permission.getId} and name ${permission.getName}") } yield () } - private def createResourcePermission(resourceId: String, userId: String): Task[UmaPermissionRepresentation] = { + private def createResourcePermission(resourceId: String, userId: String): UIO[UmaPermissionRepresentation] = { val policy = UmaPermissionRepresentation() policy.setName(policyName(userId, resourceId)) policy.setUsers(Set(userId).asJava) for { - umaPermissionRepresentation <- ZIO.attemptBlocking( - authzClient - .protection() - .policy(resourceId) - .create(policy) - ) + umaPermissionRepresentation <- ZIO + .attemptBlocking( + authzClient + .protection() + .policy(resourceId) + .create(policy) + ) + .orDie } yield umaPermissionRepresentation } - private def findWalletResource(walletId: WalletId): Task[Option[ResourceRepresentation]] = { + private def findWalletResource(walletId: WalletId): UIO[Option[ResourceRepresentation]] = { for { walletResource <- ZIO .attemptBlocking( - authzClient.protection().resource().findById(walletId.toUUID.toString()) + authzClient.protection().resource().findById(walletId.toUUID.toString) ) .asSome .catchSome { case e: RuntimeException => - if (e.getMessage().contains("Could not find resource")) ZIO.none + if (e.getMessage.contains("Could not find resource")) ZIO.none else ZIO.fail(e) } + .orDie } yield walletResource } - private def createWalletResource(walletId: WalletId): Task[ResourceRepresentation] = { + private def createWalletResource(walletId: WalletId): UIO[ResourceRepresentation] = { val walletResource = ResourceRepresentation() walletResource.setId(walletId.toUUID.toString) walletResource.setUris(Set(s"/wallets/${walletResourceName(walletId)}").asJava) @@ -90,18 +87,22 @@ case class KeycloakPermissionManagementService( for { _ <- ZIO.log(s"Creating resource for the wallet ${walletId.toUUID.toString}") - response <- ZIO.attemptBlocking( - authzClient - .protection() - .resource() - .create(walletResource) - ) - resource <- ZIO.attemptBlocking( - authzClient - .protection() - .resource() - .findById(walletResource.getId) - ) + response <- ZIO + .attemptBlocking( + authzClient + .protection() + .resource() + .create(walletResource) + ) + .orDie + resource <- ZIO + .attemptBlocking( + authzClient + .protection() + .resource() + .findById(walletResource.getId) + ) + .orDie _ <- ZIO.log(s"Resource for the wallet created id: ${resource.getId}, name ${resource.getName}") } yield resource } @@ -109,17 +110,14 @@ case class KeycloakPermissionManagementService( override def revokeWalletFromUser( walletId: WalletId, entity: KeycloakEntity - ): ZIO[WalletAdministrationContext, PermissionManagement.Error, Unit] = { + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Unit] = { val userId = entity.id for { _ <- walletManagementService .findWallet(walletId) .someOrFail(WalletNotFoundById(walletId)) - walletResource <- findWalletResource(walletId) - .logError("Error while finding wallet resource") - .mapError(UnexpectedError.apply) - .someOrFail(WalletResourceNotFoundById(walletId)) + walletResource <- findWalletResource(walletId).someOrFail(WalletNotFoundById(walletId)) permissionOpt <- ZIO .attemptBlocking( @@ -133,9 +131,8 @@ case class KeycloakPermissionManagementService( 1 ) ) + .orDie .map(_.asScala.headOption) - .logError(s"Error while finding permission by name ${policyName(userId.toString, walletResource.getId)}") - .mapError(UnexpectedError.apply) permission <- ZIO .fromOption(permissionOpt) @@ -148,8 +145,7 @@ case class KeycloakPermissionManagementService( .policy(walletResource.getId) .delete(permission.getId) ) - .logError(s"Error while deleting permission ${permission.getId}") - .mapError(UnexpectedError.apply) + .orDie _ <- ZIO.log( s"Permission ${permission.getId} deleted for user ${userId.toString} and wallet ${walletResource.getId}" @@ -157,29 +153,33 @@ case class KeycloakPermissionManagementService( } yield () } - override def listWalletPermissions(entity: KeycloakEntity): ZIO[WalletAdministrationContext, Error, Seq[WalletId]] = { + override def listWalletPermissions( + entity: KeycloakEntity + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Seq[WalletId]] = { for { token <- ZIO .fromOption(entity.accessToken) - .mapError(_ => Error.ServiceError("AccessToken is missing for listing permissions.")) - tokenIsRpt <- ZIO.fromEither(token.isRpt).mapError(Error.ServiceError(_)) + .mapError(_ => ServiceError("AccessToken is missing for listing permissions.")) + tokenIsRpt <- ZIO.fromEither(token.isRpt).mapError(ServiceError.apply) rpt <- if (tokenIsRpt) ZIO.succeed(token) else if (keycloakClient.keycloakConfig.autoUpgradeToRPT) { keycloakClient .getRpt(token) .logError("Fail to obtail RPT for wallet permissions") - .mapError(e => Error.ServiceError(e.message)) - } else ZIO.fail(Error.PermissionNotAvailable(entity.id, s"AccessToken is not RPT.")) + .mapError(e => ServiceError(e.message)) + } else ZIO.fail(PermissionNotAvailable(entity.id, s"AccessToken is not RPT.")) permittedResources <- keycloakClient .checkPermissions(rpt) .logError("Fail to list resource permissions on keycloak") - .mapError(e => Error.ServiceError(e.message)) + .mapError(e => ServiceError(e.message)) permittedWallet <- getPermittedWallet(permittedResources) } yield permittedWallet.map(_.id) } - private def getPermittedWallet(resourceIds: Seq[String]): ZIO[WalletAdministrationContext, Error, Seq[Wallet]] = { + private def getPermittedWallet( + resourceIds: Seq[String] + ): ZIO[WalletAdministrationContext, PermissionManagementServiceError, Seq[Wallet]] = { val walletIds = resourceIds.flatMap(id => Try(UUID.fromString(id)).toOption).map(WalletId.fromUUID) walletManagementService .getWallets(walletIds) @@ -189,17 +189,27 @@ case class KeycloakPermissionManagementService( object KeycloakPermissionManagementService { val layer: URLayer[ AuthzClient & KeycloakClient & WalletManagementService, - PermissionManagement.Service[KeycloakEntity] + PermissionManagementService[KeycloakEntity] ] = ZLayer.fromFunction(KeycloakPermissionManagementService(_, _, _)) - val disabled: ULayer[PermissionManagement.Service[KeycloakEntity]] = + val disabled: ULayer[PermissionManagementService[KeycloakEntity]] = ZLayer.succeed { - val notEnabledError = ZIO.fail(PermissionManagement.Error.ServiceError("Keycloak is not enabled")) - new PermissionManagement.Service[KeycloakEntity] { - override def grantWalletToUser(walletId: WalletId, entity: KeycloakEntity): IO[Error, Unit] = notEnabledError - override def revokeWalletFromUser(walletId: WalletId, entity: KeycloakEntity): IO[Error, Unit] = notEnabledError - override def listWalletPermissions(entity: KeycloakEntity): IO[Error, Seq[WalletId]] = notEnabledError + val notEnabledError = ZIO.fail(ServiceError("Keycloak is not enabled")) + new PermissionManagementService[KeycloakEntity] { + override def grantWalletToUser( + walletId: WalletId, + entity: KeycloakEntity + ): IO[PermissionManagementServiceError, Unit] = notEnabledError + + override def revokeWalletFromUser( + walletId: WalletId, + entity: KeycloakEntity + ): IO[PermissionManagementServiceError, Unit] = notEnabledError + + override def listWalletPermissions( + entity: KeycloakEntity + ): IO[PermissionManagementServiceError, Seq[WalletId]] = notEnabledError } } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/wallet/http/controller/WalletManagementController.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/wallet/http/controller/WalletManagementController.scala index 2f070e39cc..ed271ffc85 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/wallet/http/controller/WalletManagementController.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/iam/wallet/http/controller/WalletManagementController.scala @@ -6,7 +6,7 @@ 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.iam.authentication.oidc.KeycloakEntity -import org.hyperledger.identus.iam.authorization.core.PermissionManagement +import org.hyperledger.identus.iam.authorization.core.PermissionManagementService import org.hyperledger.identus.iam.wallet.http.model.{ CreateWalletRequest, CreateWalletUmaPermissionRequest, @@ -41,26 +41,11 @@ trait WalletManagementController { )(implicit rc: RequestContext): ZIO[WalletAdministrationContext, ErrorResponse, Unit] } -object WalletManagementController { - given permissionManagementErrorConversion: Conversion[PermissionManagement.Error, ErrorResponse] = { - case e: PermissionManagement.Error.PermissionNotFoundById => ErrorResponse.badRequest(detail = Some(e.message)) - case e: PermissionManagement.Error.ServiceError => ErrorResponse.internalServerError(detail = Some(e.message)) - case e: PermissionManagement.Error.UnexpectedError => ErrorResponse.internalServerError(detail = Some(e.message)) - case e: PermissionManagement.Error.UserNotFoundById => ErrorResponse.badRequest(detail = Some(e.message)) - case e: PermissionManagement.Error.WalletNotFoundById => ErrorResponse.badRequest(detail = Some(e.message)) - case e: PermissionManagement.Error.WalletNotFoundByUserId => ErrorResponse.badRequest(detail = Some(e.message)) - case e: PermissionManagement.Error.WalletResourceNotFoundById => ErrorResponse.badRequest(detail = Some(e.message)) - case e: PermissionManagement.Error.PermissionNotAvailable => ErrorResponse.badRequest(detail = Some(e.message)) - } -} - class WalletManagementControllerImpl( walletService: WalletManagementService, - permissionService: PermissionManagement.Service[BaseEntity], + permissionService: PermissionManagementService[BaseEntity], ) extends WalletManagementController { - import WalletManagementController.given - override def listWallet( paginationInput: PaginationInput )(implicit rc: RequestContext): ZIO[WalletAdministrationContext, ErrorResponse, WalletDetailPage] = { @@ -152,6 +137,6 @@ class WalletManagementControllerImpl( } object WalletManagementControllerImpl { - val layer: URLayer[WalletManagementService & PermissionManagement.Service[BaseEntity], WalletManagementController] = + val layer: URLayer[WalletManagementService & PermissionManagementService[BaseEntity], WalletManagementController] = ZLayer.fromFunction(WalletManagementControllerImpl(_, _)) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerServerEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerServerEndpoints.scala index c1fb766d0d..9a356efb14 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerServerEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerServerEndpoints.scala @@ -2,13 +2,7 @@ package org.hyperledger.identus.oid4vci import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.api.http.ErrorResponse -import org.hyperledger.identus.iam.authentication.{ - Authenticator, - Authorizer, - DefaultAuthenticator, - Oid4vciAuthenticatorFactory, - SecurityLogic -} +import org.hyperledger.identus.iam.authentication.* import org.hyperledger.identus.oid4vci.controller.CredentialIssuerController import org.hyperledger.identus.oid4vci.http.{CredentialErrorResponse, ExtendedErrorResponse, NonceResponse} import org.hyperledger.identus.LogUtils.* diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/agent/server/AgentInitializationSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/agent/server/AgentInitializationSpec.scala index 9fbd912937..33db03632e 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/agent/server/AgentInitializationSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/agent/server/AgentInitializationSpec.scala @@ -103,7 +103,7 @@ object AgentInitializationSpec extends ZIOSpecDefault, PostgresTestContainerSupp _ <- AgentInitialization.run.overrideConfig(seed = Some("0" * 128)) actualSeed <- ZIO .serviceWithZIO[WalletSecretStorage]( - _.getWalletSeed + _.findWalletSeed .provide(ZLayer.succeed(WalletAccessContext(WalletId.default))) ) } yield assert(actualSeed.get.toByteArray)(equalTo(Array.fill[Byte](64)(0))) diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/iam/authorization/core/EntityPermissionManagementSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/iam/authorization/core/EntityPermissionManagementSpec.scala index d1e1b9e1ac..6bb296c9a6 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/iam/authorization/core/EntityPermissionManagementSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/iam/authorization/core/EntityPermissionManagementSpec.scala @@ -12,7 +12,10 @@ import org.hyperledger.identus.agent.walletapi.sql.{ JdbcWalletNonSecretStorage, JdbcWalletSecretStorage } -import org.hyperledger.identus.iam.authorization.core.PermissionManagement.Error.{ServiceError, WalletNotFoundById} +import org.hyperledger.identus.iam.authorization.core.PermissionManagementServiceError.{ + ServiceError, + WalletNotFoundById +} import org.hyperledger.identus.shared.crypto.ApolloSpecHelper import org.hyperledger.identus.shared.models.{WalletAdministrationContext, WalletId} import org.hyperledger.identus.sharedtest.containers.PostgresTestContainerSupport @@ -48,7 +51,7 @@ object EntityPermissionManagementSpec extends ZIOSpecDefault, PostgresTestContai test("grant wallet access to the user") { for { entityService <- ZIO.service[EntityService] - permissionService <- ZIO.service[PermissionManagement.Service[Entity]] + permissionService <- ZIO.service[PermissionManagementService[Entity]] walletService <- ZIO.service[WalletManagementService] wallet1 <- walletService .createWallet(Wallet("test")) @@ -73,7 +76,7 @@ object EntityPermissionManagementSpec extends ZIOSpecDefault, PostgresTestContai test("revoke wallet is not support") { for { entityService <- ZIO.service[EntityService] - permissionService <- ZIO.service[PermissionManagement.Service[Entity]] + permissionService <- ZIO.service[PermissionManagementService[Entity]] walletService <- ZIO.service[WalletManagementService] wallet1 <- walletService .createWallet(Wallet("test")) @@ -94,7 +97,7 @@ object EntityPermissionManagementSpec extends ZIOSpecDefault, PostgresTestContai val walletId2 = WalletId.random for { entityService <- ZIO.service[EntityService] - permissionService <- ZIO.service[PermissionManagement.Service[Entity]] + permissionService <- ZIO.service[PermissionManagementService[Entity]] walletService <- ZIO.service[WalletManagementService] wallet1 <- walletService .createWallet(Wallet("test", walletId1)) @@ -117,7 +120,7 @@ object EntityPermissionManagementSpec extends ZIOSpecDefault, PostgresTestContai val walletId2 = WalletId.random for { entityService <- ZIO.service[EntityService] - permissionService <- ZIO.service[PermissionManagement.Service[Entity]] + permissionService <- ZIO.service[PermissionManagementService[Entity]] walletService <- ZIO.service[WalletManagementService] wallet1 <- walletService .createWallet(Wallet("test", walletId1)) diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala index c215c0494e..c4e98d6d17 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala @@ -5,8 +5,8 @@ import org.hyperledger.identus.agent.walletapi.service.{WalletManagementService, import org.hyperledger.identus.agent.walletapi.sql.{JdbcWalletNonSecretStorage, JdbcWalletSecretStorage} import org.hyperledger.identus.iam.authentication.oidc.* import org.hyperledger.identus.iam.authentication.AuthenticationError.ResourceNotPermitted -import org.hyperledger.identus.iam.authorization.core.PermissionManagement -import org.hyperledger.identus.iam.authorization.core.PermissionManagement.Error.{UnexpectedError, WalletNotFoundById} +import org.hyperledger.identus.iam.authorization.core.PermissionManagementService +import org.hyperledger.identus.iam.authorization.core.PermissionManagementServiceError.WalletNotFoundById import org.hyperledger.identus.shared.crypto.ApolloSpecHelper import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletAdministrationContext, WalletId} import org.hyperledger.identus.sharedtest.containers.{ @@ -72,7 +72,7 @@ object KeycloakPermissionManagementServiceSpec user <- createUser(username = username, password = password) entity = KeycloakEntity(id = UUID.fromString(user.getId)) - permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + permissionService <- ZIO.service[PermissionManagementService[KeycloakEntity]] _ <- permissionService.grantWalletToUser(wallet.id, entity) token <- client.getAccessToken(username, password).map(_.access_token) @@ -95,7 +95,7 @@ object KeycloakPermissionManagementServiceSpec user <- createUser(username = username, password = password) entity = KeycloakEntity(id = UUID.fromString(user.getId)) - permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + permissionService <- ZIO.service[PermissionManagementService[KeycloakEntity]] _ <- permissionService.grantWalletToUser(wallet.id, entity) token <- client.getAccessToken(username, password).map(_.access_token) @@ -116,7 +116,7 @@ object KeycloakPermissionManagementServiceSpec private val failureCasesSuite = suite("Failure Cases Suite")( test("grant wallet access to the user with invalid wallet id") { for { - permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + permissionService <- ZIO.service[PermissionManagementService[KeycloakEntity]] entity = KeycloakEntity(id = UUID.randomUUID()) exit <- permissionService.grantWalletToUser(WalletId.random, entity).exit } yield assert(exit)(fails(isSubtype[WalletNotFoundById](anything))) @@ -128,9 +128,9 @@ object KeycloakPermissionManagementServiceSpec walletService <- ZIO.service[WalletManagementService] wallet <- walletService.createWallet(Wallet("test_1")) entity = KeycloakEntity(id = UUID.randomUUID()) - permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + permissionService <- ZIO.service[PermissionManagementService[KeycloakEntity]] exit <- permissionService.grantWalletToUser(wallet.id, entity).exit - } yield assert(exit)(fails(isSubtype[UnexpectedError](anything))) + } yield assert(exit)(dies(hasMessage(equalTo(s"Error creating policy for resource [${wallet.id}]")))) } ).provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) @@ -152,7 +152,7 @@ object KeycloakPermissionManagementServiceSpec user <- createUser(username = username, password = password) entity = KeycloakEntity(id = UUID.fromString(user.getId)) - permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + permissionService <- ZIO.service[PermissionManagementService[KeycloakEntity]] _ <- permissionService .grantWalletToUser(wallet.id, entity) .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.SelfService(Seq(walletId)))) @@ -179,7 +179,7 @@ object KeycloakPermissionManagementServiceSpec user <- createUser(username = username, password = password) entity = KeycloakEntity(id = UUID.fromString(user.getId)) - permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + permissionService <- ZIO.service[PermissionManagementService[KeycloakEntity]] _ <- permissionService .grantWalletToUser(wallet.id, entity) .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) @@ -216,7 +216,7 @@ object KeycloakPermissionManagementServiceSpec user <- createUser(username = username, password = password) entity = KeycloakEntity(id = UUID.fromString(user.getId)) - permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + permissionService <- ZIO.service[PermissionManagementService[KeycloakEntity]] exit <- permissionService .grantWalletToUser(WalletId.random, entity) .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.SelfService(Seq(walletId)))) @@ -239,7 +239,7 @@ object KeycloakPermissionManagementServiceSpec user <- createUser(username = username, password = password) entity = KeycloakEntity(id = UUID.fromString(user.getId)) - permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + permissionService <- ZIO.service[PermissionManagementService[KeycloakEntity]] _ <- permissionService .grantWalletToUser(wallet.id, entity) .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) 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 eba646e4b8..2de767e43e 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 @@ -1,7 +1,6 @@ package org.hyperledger.identus.oid4vci.domain import com.nimbusds.jose.* -import org.bouncycastle.util.encoders.Hex import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} import org.hyperledger.identus.agent.walletapi.storage.{DIDNonSecretStorage, MockDIDNonSecretStorage} @@ -95,7 +94,6 @@ object OIDCCredentialIssuerServiceSpec val longFormDid = PrismDID.buildLongFormFromOperation(holderOp) val keyIndex = holderDidData.publicKeys.find(_.purpose == VerificationRelationship.AssertionMethod).get.id val kid = longFormDid.toString + "#" + keyIndex - val encodedKey = Hex.toHexString(holderKp.privateKey.getEncoded) makeJwtProof(kid, nonce, aud, iat, holderKp.privateKey) } diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/model/error/DIDSecretStorageError.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/model/error/DIDSecretStorageError.scala index 85a305541e..9112f841c0 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/model/error/DIDSecretStorageError.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/model/error/DIDSecretStorageError.scala @@ -1,7 +1,7 @@ package org.hyperledger.identus.agent.walletapi.model.error import org.hyperledger.identus.mercury.model.DidId -import org.hyperledger.identus.shared.models._ +import org.hyperledger.identus.shared.models.* sealed trait DIDSecretStorageError( override val statusCode: StatusCode, 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 4138581bc0..1c2f239b55 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 @@ -29,7 +29,7 @@ private[walletapi] class DIDCreateHandler( val operationFactory = OperationFactory(apollo) for { walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) - seed <- walletSecretStorage.getWalletSeed + seed <- walletSecretStorage.findWalletSeed .someOrElseZIO(ZIO.dieMessage(s"Wallet seed for wallet $walletId does not exist")) didIndex <- nonSecretStorage .getMaxDIDIndex() diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDUpdateHandler.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDUpdateHandler.scala index 73d5d2eb57..de517075dd 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDUpdateHandler.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDUpdateHandler.scala @@ -40,7 +40,7 @@ private[walletapi] class DIDUpdateHandler( val did = state.createOperation.did for { walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) - seed <- walletSecretStorage.getWalletSeed + seed <- walletSecretStorage.findWalletSeed .someOrElseZIO(ZIO.dieMessage(s"Wallet seed for wallet $walletId does not exist")) keyCounter <- nonSecretStorage .getHdKeyCounter(did) 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 c2e3f44611..4bc6154035 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 @@ -429,7 +429,7 @@ class JdbcDIDNonSecretStorage(xa: Transactor[ContextAwareTask], xb: Transactor[T | wallet_id | FROM public.prism_did_wallet_state | WHERE - | did = ${prismDid.toString} + | did = ${prismDid} """.stripMargin .query[WalletId] .option diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcWalletSecretStorage.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcWalletSecretStorage.scala index 3325880268..824b64b73d 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcWalletSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcWalletSecretStorage.scala @@ -36,7 +36,7 @@ class JdbcWalletSecretStorage(xa: Transactor[ContextAwareTask]) extends WalletSe } yield () } - override def getWalletSeed: URIO[WalletAccessContext, Option[WalletSeed]] = { + override def findWalletSeed: URIO[WalletAccessContext, Option[WalletSeed]] = { val cxnIO = sql""" | SELECT seed diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/WalletSecretStorage.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/WalletSecretStorage.scala index f3da8a4684..b8c6f179d9 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/WalletSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/WalletSecretStorage.scala @@ -6,5 +6,5 @@ import zio.* trait WalletSecretStorage { def setWalletSeed(seed: WalletSeed): URIO[WalletAccessContext, Unit] - def getWalletSeed: URIO[WalletAccessContext, Option[WalletSeed]] + def findWalletSeed: URIO[WalletAccessContext, Option[WalletSeed]] } diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/KeyResolver.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/KeyResolver.scala index 038752f898..81a5fa8035 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/KeyResolver.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/KeyResolver.scala @@ -29,7 +29,7 @@ class KeyResolver( } private def deriveHdKey(path: ManagedDIDHdKeyPath): RIO[WalletAccessContext, Option[Secp256k1KeyPair]] = - walletSecretStorage.getWalletSeed.flatMap { + walletSecretStorage.findWalletSeed.flatMap { case None => ZIO.none case Some(seed) => apollo.secp256k1.deriveKeyPair(seed.toByteArray)(path.derivationPath*).asSome } diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/vault/VaultWalletSecretStorage.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/vault/VaultWalletSecretStorage.scala index 6c688e3145..f9ef9cd2b2 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/vault/VaultWalletSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/vault/VaultWalletSecretStorage.scala @@ -20,7 +20,7 @@ class VaultWalletSecretStorage(vaultKV: VaultKVClient) extends WalletSecretStora } yield () } - override def getWalletSeed: URIO[WalletAccessContext, Option[WalletSeed]] = { + override def findWalletSeed: URIO[WalletAccessContext, Option[WalletSeed]] = { for { walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) path = walletSeedPath(walletId) diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/memory/GenericSecretStorageInMemory.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/memory/GenericSecretStorageInMemory.scala similarity index 100% rename from cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/memory/GenericSecretStorageInMemory.scala rename to cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/memory/GenericSecretStorageInMemory.scala diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/memory/WalletSecretStorageInMemory.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/memory/WalletSecretStorageInMemory.scala similarity index 92% rename from cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/memory/WalletSecretStorageInMemory.scala rename to cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/memory/WalletSecretStorageInMemory.scala index 27ea027dee..91fed64a93 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/memory/WalletSecretStorageInMemory.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/memory/WalletSecretStorageInMemory.scala @@ -14,7 +14,7 @@ class WalletSecretStorageInMemory(storeRef: Ref[Map[WalletId, WalletSeed]]) exte } yield () } - override def getWalletSeed: URIO[WalletAccessContext, Option[WalletSeed]] = { + override def findWalletSeed: URIO[WalletAccessContext, Option[WalletSeed]] = { for { walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) seed <- storeRef.get.map(_.get(walletId)) diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/WalletManagementServiceSpec.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/WalletManagementServiceSpec.scala index 6be6eef494..be4d5e174c 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/WalletManagementServiceSpec.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/WalletManagementServiceSpec.scala @@ -88,7 +88,7 @@ object WalletManagementServiceSpec secretStorage <- ZIO.service[WalletSecretStorage] createdWallet <- svc.createWallet(Wallet("wallet-1")) listedWallets <- svc.listWallets().map(_._1) - seed <- secretStorage.getWalletSeed.provide(ZLayer.succeed(WalletAccessContext(createdWallet.id))) + seed <- secretStorage.findWalletSeed.provide(ZLayer.succeed(WalletAccessContext(createdWallet.id))) } yield assert(listedWallets)(hasSameElements(Seq(createdWallet))) && assert(seed)(isSome) }, @@ -99,7 +99,7 @@ object WalletManagementServiceSpec createdWallets <- ZIO.foreach(1 to 10)(i => svc.createWallet(Wallet(s"wallet-$i"))) listedWallets <- svc.listWallets().map(_._1) seeds <- ZIO.foreach(listedWallets) { wallet => - secretStorage.getWalletSeed.provide(ZLayer.succeed(WalletAccessContext(wallet.id))) + secretStorage.findWalletSeed.provide(ZLayer.succeed(WalletAccessContext(wallet.id))) } } yield assert(createdWallets)(hasSameElements(listedWallets)) && assert(seeds)(forall(isSome)) @@ -111,7 +111,7 @@ object WalletManagementServiceSpec seed1 = WalletSeed.fromByteArray(Array.fill[Byte](64)(0)).toOption.get createdWallet <- svc.createWallet(Wallet("wallet-1"), Some(seed1)) listedWallets <- svc.listWallets().map(_._1) - seed2 <- secretStorage.getWalletSeed.provide(ZLayer.succeed(WalletAccessContext(createdWallet.id))) + seed2 <- secretStorage.findWalletSeed.provide(ZLayer.succeed(WalletAccessContext(createdWallet.id))) } yield assert(listedWallets)(hasSameElements(Seq(createdWallet))) && assert(seed2)(isSome(equalTo(seed1))) }, @@ -123,7 +123,7 @@ object WalletManagementServiceSpec createdWallets <- ZIO.foreach(seeds1) { seed => svc.createWallet(Wallet("test-wallet"), Some(seed)) } listedWallets <- svc.listWallets().map(_._1) seeds2 <- ZIO.foreach(listedWallets) { wallet => - secretStorage.getWalletSeed.provide(ZLayer.succeed(WalletAccessContext(wallet.id))) + secretStorage.findWalletSeed.provide(ZLayer.succeed(WalletAccessContext(wallet.id))) } } yield assert(createdWallets)(hasSameElements(listedWallets)) && assert(seeds2.flatten)(hasSameElements(seeds1)) diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/WalletSecretStorageSpec.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/WalletSecretStorageSpec.scala index d9b87b2f34..c201d7036b 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/WalletSecretStorageSpec.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/WalletSecretStorageSpec.scala @@ -46,9 +46,9 @@ object WalletSecretStorageSpec extends ZIOSpecDefault, PostgresTestContainerSupp .map(_.id) walletAccessCtx = ZLayer.succeed(WalletAccessContext(walletId)) seed = WalletSeed.fromByteArray(Array.fill[Byte](64)(0)).toOption.get - seedBefore <- storage.getWalletSeed.provide(walletAccessCtx) + seedBefore <- storage.findWalletSeed.provide(walletAccessCtx) _ <- storage.setWalletSeed(seed).provide(walletAccessCtx) - seedAfter <- storage.getWalletSeed.provide(walletAccessCtx) + seedAfter <- storage.findWalletSeed.provide(walletAccessCtx) } yield assert(seedBefore)(isNone) && assert(seedAfter)(isSome(equalTo(seed))) }, @@ -69,7 +69,7 @@ object WalletSecretStorageSpec extends ZIOSpecDefault, PostgresTestContainerSupp seeds <- ZIO .foreach(wallets) { wallet => val walletAccessCtx = ZLayer.succeed(WalletAccessContext(wallet.id)) - storage.getWalletSeed.provideSomeLayer(walletAccessCtx) + storage.findWalletSeed.provideSomeLayer(walletAccessCtx) } .map(_.flatten) } yield assert(seeds.size)(equalTo(10)) && assert(seeds)(isDistinct) diff --git a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepository.scala b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepository.scala index 9ad70b3160..9d25fc133a 100644 --- a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepository.scala +++ b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepository.scala @@ -3,8 +3,7 @@ package org.hyperledger.identus.connect.core.repository import org.hyperledger.identus.connect.core.model.ConnectionRecord import org.hyperledger.identus.connect.core.model.ConnectionRecord.ProtocolState import org.hyperledger.identus.mercury.protocol.connection.* -import org.hyperledger.identus.shared.models.Failure -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.models.{Failure, WalletAccessContext} import zio.{UIO, URIO} import java.util.UUID diff --git a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepositoryInMemory.scala b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepositoryInMemory.scala similarity index 100% rename from connect/core/src/main/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepositoryInMemory.scala rename to connect/core/src/test/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepositoryInMemory.scala diff --git a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepositorySpecSuite.scala b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepositorySpecSuite.scala index ac7b1a6f6c..cb795b00b2 100644 --- a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepositorySpecSuite.scala +++ b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/repository/ConnectionRepositorySpecSuite.scala @@ -6,7 +6,6 @@ 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.models.* -import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.{Cause, Exit, ZIO, ZLayer} import zio.test.* import zio.Exit.Failure diff --git a/docs/docusaurus/credentials/issue.md b/docs/docusaurus/credentials/issue.md index 086178a18e..b392d310c2 100644 --- a/docs/docusaurus/credentials/issue.md +++ b/docs/docusaurus/credentials/issue.md @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# Issue credentials +# Issue credentials (DIDComm) In the Identus Platform, the [Issue Credentials Protocol](/docs/concepts/glossary#issue-credentials-protocol) allows you to create, retrieve, and manage issued [verifiable credentials (VCs)](/docs/concepts/glossary#verifiable-credentials) between a VC issuer and a VC holder. @@ -183,7 +183,7 @@ The `issuingDID` and `connectionId` properties come from completing the pre-requ ::: - 📌 **Note:** Claims can also include the `exp` Expiration Time attribute, which is part of JWT claims. `exp` attribute is disclosable if specified and can have a value in epoch time (in seconds), indicating when the SDJWT credential expires for more details - +[RFC5719](https://datatracker.ietf.org/doc/html/rfc7519#page-9) Once the request initiates, a new credential record for the issuer gets created with a unique ID. The state of this record is now `OfferPending`. @@ -352,7 +352,7 @@ curl -X POST "http://localhost:8090/cloud-agent/issue-credentials/records/$holde } ``` 2. `keyId`: This is optional field but must be specified to choose which key bounds to the verifiable credential. - For more information on key-binding, . + For more information on key-binding, [ietf-oauth-selective-disclosure-jwt](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt). Currently, we only support the EdDSA algorithm and curve Ed25519. The specified keyId should be of type Ed25519. The purpose of the keyId should be authentication. diff --git a/docs/docusaurus/credentials/oid4vci.md b/docs/docusaurus/credentials/oid4vci.md new file mode 100644 index 0000000000..a84b7c7dfe --- /dev/null +++ b/docs/docusaurus/credentials/oid4vci.md @@ -0,0 +1,74 @@ +# Issue credentials (OID4VCI) + +[OID4VCI](/docs/concepts/glossary#oid4vci) (OpenID for Verifiable Credential Issuance) is a protocol that extends OAuth2 to issue credentials. +It involves a Credential Issuer server and an Authorization server working together, +using the authorization and token endpoints on the Authorization Server to grant holders access to credentials on the Credential Issuer server. +These servers may or may not be the same, depending on the implementation. + +The Identus Cloud Agent can act as a Credential Issuer server and integrate with any Authorization Server that follows the integration contract. The contract for the Authorization Server in the OID4VCI flow can be found [here](https://github.com/hyperledger/identus-cloud-agent/blob/main/docs/general/authserver-oid4vci-contract.md). + +## Example: OID4VCI Authorization Code Issuance + +Example is available [here](https://github.com/hyperledger/identus-cloud-agent/tree/main/examples/st-oid4vci). + +Following the instructions, the example demonstrates a single-tenant agent setup using an external Keycloak as the Issuer Authorization Server. The demo application walks through the authorization code issuance flow step-by-step. + +#### 1. Launching Local Example Stack + +```bash +docker-compose up +``` + +After running the `docker-compose up` command, all the containers should be running and initialized with the necessary configurations. The following logs should appear indicating that the stack is ready to execute the flow + +``` + _ _ _ _ _ + | |_| |_| |_ _ __| | | ___ + | ' \ _| _| '_ \_ _(_-< + |_||_\__|\__| .__/ |_|/__/ + |_| + 2024-07-16_11:51:01.301 INFO o.h.b.s.BlazeServerBuilder@L424:[ZScheduler-Worker-5] {} - http4s v0.23.23 on blaze v0.23.15 started at http://0.0.0.0:8085/ + +``` + +#### 2. Building the demo application + +```bash +docker build -t identus-oid4vci-demo:latest ./demo +``` + +#### 3. Running the demo application + +```bash +docker run --network -it identus-oid4vci-demo:latest +``` +The parameter `NETWORK_NAME` should be the same as the network name in docker-compose. +This name can be discovered by running the `docker network ls` command. + +The demo application acts as both issuer and Holder in the same script. +See the source code for detailed steps on how to implement this flow. +The demo application will interactively prompt the next step in the issuance flow. +Keep continuing until this log appears asking the user to log in using the browser. + +``` +############################## + +Open this link in the browser to login + +http://localhost:9980/realms/students/protocol/openid-connect/auth?redirect_uri=..... + +############################## + +wating for authorization redirect ... +``` + +Open this URL in the browser. Enter `alice` for the username and `1234` for the password. + +After a successful login, this log should appear indicating the demo application has received the credentials. + +``` +::::: Credential Received ::::: +{ + "credential": "eyJ0eXAiOiJKV1QiLC...SK1vJK-fx6zjXw" +} +``` diff --git a/docs/docusaurus/schemas/credential-schema.md b/docs/docusaurus/schemas/credential-schema.md index 77c826038b..56aff05830 100644 --- a/docs/docusaurus/schemas/credential-schema.md +++ b/docs/docusaurus/schemas/credential-schema.md @@ -168,7 +168,7 @@ DID of the identity which authored the credential schema. ### schema (JSON Schema) A valid [JSON-SCHEMA](https://json-schema.org/) where the credential schema semantic gets defined. -JSON Schema must be composed according to schema. +JSON Schema must be composed according to the [Metaschema](https://json-schema.org/draft/2020-12/schema) schema. **Example:** ```json diff --git a/docs/docusaurus/secrets/seed-generation.md b/docs/docusaurus/secrets/seed-generation.md index 5d9e805819..38bb865224 100644 --- a/docs/docusaurus/secrets/seed-generation.md +++ b/docs/docusaurus/secrets/seed-generation.md @@ -52,5 +52,5 @@ users can write down their mnemonic phrase, making it more convenient to keep tr By using BIP39, users have options to choose a mnemonic phrase length as well as a passphrase. There are many tools for generating a BIP39 seed including but not limited to: -- (use the BIP39 seed field which provides a 128-chars hex string) +- [BIP39](https://iancoleman.io/bip39/) (use the BIP39 seed field which provides a 128-chars hex string) - [BIP39 - implementations section](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#other-implementations) also provides a list of implementations diff --git a/docs/docusaurus/sidebars.js b/docs/docusaurus/sidebars.js index 7e1712fa68..b746ea93f5 100644 --- a/docs/docusaurus/sidebars.js +++ b/docs/docusaurus/sidebars.js @@ -20,8 +20,9 @@ const sidebars = { }, items: [ 'credentials/issue', + 'credentials/oid4vci', 'credentials/present-proof', - 'credentials/revocation', + 'credentials/revocation' ] }, { @@ -96,7 +97,7 @@ const sidebars = { 'multitenancy/tenant-onboarding-ext-iam', 'multitenancy/tenant-onboarding-self-service', 'multitenancy/tenant-migration', - 'multitenancy/admin-authz-ext-iam', + 'multitenancy/admin-authz-ext-iam' ] } ] diff --git a/docs/general/authserver-oid4vci-contract.md b/docs/general/authserver-oid4vci-contract.md new file mode 100644 index 0000000000..3260389333 --- /dev/null +++ b/docs/general/authserver-oid4vci-contract.md @@ -0,0 +1,72 @@ +# OID4VCI Authorization Server contract + +Identus Cloud Agent supports OID4VCI while allowing users to plug-in their authorization server. +The agent provides a credential endpoint, while the authorization server handles the authorization and token endpoints. +This flexibility enables integration with any existing authorization server potentially containing the holder user base. +The issuance flow implementation requires the authorization server to adhere to the contract, ensuring the agent can manage the issuance session and coordinate the process. + +The Identus platform also provides the [Keycloak plugin reference implementation](https://github.com/hyperledger/identus-keycloak-plugins) showcasing the agent capability. +However, the authorization server is not limited to only Keycloak. + +## Contract for Authorization Code issuance flow + +The sequence diagram is largely based on [OID4VCI spec](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-authorization-code-flow) +with slight modification to the __TokenEndpoint__. + +```mermaid +sequenceDiagram + participant Holder + participant Issuer + participant AuthServer + participant CloudAgent + + Issuer ->>+ CloudAgent: Create CredentialOffer + CloudAgent ->>- Issuer: CredentialOffer
(issuer_state) + Issuer -->> Holder: Present offer
(issuer_state) + Holder ->>+ CloudAgent: Discover Metadata + CloudAgent ->>- Holder: IssuerMetadata + Holder ->>+ AuthServer: Discover Metadata + AuthServer ->>- Holder: AuthServerMetadata + Holder ->>+ AuthServer: AuthorizationRequest
(issuer_state) + AuthServer ->>- Holder: AuthorizationResponse
(code) + Holder ->>+ AuthServer: TokenRequest
(code) + AuthServer ->>+ CloudAgent: NonceRequest
(issuer_state) + CloudAgent ->>- AuthServer: NonceResponse
(c_nonce) + AuthServer ->>- Holder: TokenResponse
(c_nonce) + Holder ->>+ CloudAgent: CredentialRequest
(proof) + CloudAgent ->>- Holder: CredentialResponse +``` + + +### Authorization Endpoint + +1. Authorization `scope` MUST be configured in the Authorization Server to the same value as in Credential Issuer Metadata +2. The endpoint MUST accept the parameter `issuer_state` in the [__AuthorizationRequest__](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-5.1.3-2.3) and recall this value in the subsequent call on the __TokenEndpoint__ + +### Token Endpoint + +1. When the holder makes a __TokenRequest__ to the __TokenEndpoint__, the __AuthorizationServer__ MUST recall the `issuer_state` parameter and make an HTTP call to the `/oid4vci/nonces` endpoint in the Cloud Agent using the following format. + +__NonceRequest__ + +``` +POST /oid4vci/nonces +Authorization: Bearer + +{ + "issuerState": "" +} +``` +Where `JWT_TOKEN` is a valid token issued by the __AuthorizationServer__. + +__NonceResponse__ + +``` +{ + "nonce": "", + "nonceExpiresIn": +} +``` + +2. The [__TokenResponse__](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-successful-token-response) must include `c_nonce` and `c_nonce_expires_in` parameter in the __TokenResponse__ + diff --git a/examples/.nickel/caddy.ncl b/examples/.nickel/caddy.ncl index 1f3ccf94b7..153ce9c879 100644 --- a/examples/.nickel/caddy.ncl +++ b/examples/.nickel/caddy.ncl @@ -25,7 +25,7 @@ in handle_path /didcomm* { reverse_proxy %{args.agent.host}:%{std.to_string args.agent.didcommPort} } - handle_path /prism-agent* { + handle_path /cloud-agent* { reverse_proxy %{args.agent.host}:%{std.to_string args.agent.restPort} } handle_path /keycloak* { diff --git a/examples/.nickel/root.ncl b/examples/.nickel/root.ncl index 1276ba261b..5fa174acc2 100644 --- a/examples/.nickel/root.ncl +++ b/examples/.nickel/root.ncl @@ -21,7 +21,7 @@ in name = "issuer", port = 9980, realm = "students", - extraEnvs = { IDENTUS_URL = "http://caddy-issuer:8080/prism-agent" } + extraEnvs = { IDENTUS_URL = "http://caddy-issuer:8080/cloud-agent" } } ), diff --git a/examples/.nickel/stack.ncl b/examples/.nickel/stack.ncl index a7e713a090..323ec5d6ce 100644 --- a/examples/.nickel/stack.ncl +++ b/examples/.nickel/stack.ncl @@ -159,7 +159,7 @@ in agentDb = makeSharedDbConfig "agent", node = { host = "node" }, didcommServiceUrl = "http://%{hosts.caddy}:%{std.to_string args.port}/didcomm", - restServiceUrl = "http://%{hosts.caddy}:%{std.to_string args.port}/prism-agent", + restServiceUrl = "http://%{hosts.caddy}:%{std.to_string args.port}/cloud-agent", apikeyEnabled = args.apikeyEnabled, } & ( @@ -200,12 +200,12 @@ in version = V.hurl, hurlDir = "../.shared/hurl/simple_realm", variables = { - HURL_KEYCLOAK_BASE_URL = "http://%{hosts.keycloak}:8080", - HURL_KEYCLOAK_ADMIN_USER = "admin", - HURL_KEYCLOAK_ADMIN_PASSWORD = "admin", - HURL_KEYCLOAK_REALM = "identus", - HURL_KEYCLOAK_CLIENT_ID = "agent", - HURL_KEYCLOAK_CLIENT_SECRET = "agent-secret", + HURL_keycloak_base_url = "http://%{hosts.keycloak}:8080", + HURL_keycloak_admin_user = "admin", + HURL_keycloak_admin_password = "admin", + HURL_keycloak_realm = "identus", + HURL_keycloak_client_id = "agent", + HURL_keycloak_client_secret = "agent-secret", } }, } diff --git a/examples/.nickel/versions.ncl b/examples/.nickel/versions.ncl index 5f6ee3f616..7ff18ae327 100644 --- a/examples/.nickel/versions.ncl +++ b/examples/.nickel/versions.ncl @@ -1,8 +1,8 @@ { # identus - agent = "1.36.1-SNAPSHOT", + agent = "1.38.0", node = "2.4.0", - identusKeycloak = "0.1.0", + identusKeycloak = "0.2.0", # 3rd party caddy = "2.7.6-alpine", mockServer = "5.15.0", diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000..670b327f54 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,74 @@ +# How to run examples + +## Prerequisites + +- docker-compose version >= `2.23.1` + +## Running examples + +Most of the examples should follow the same pattern. +Simply go to each example directory and spin up the docker-compose of each example. + +```bash +cd +docker-compose up +``` + +If some example requires a different command, it should be provided in its own local README. + +Once finished, `docker-compose down --volumes` can be used to clean docker volumes to avoid unexpected behavior for the next run. + +## Examples + +| example | description | +|---------------------|---------------------------------------------------------------------------| +| `st` | single-tenant configuration without external services (except database) | +| `st-multi` | 3 instances of single-tenant configuration | +| `st-vault` | single-tenant with Vault for secret storage | +| `st-oid4vci` | single-tenant agent with Keycloak as external Issuer Authorization Server | +| `mt` | multi-tenant configuration using built-in IAM | +| `mt-keycloak` | multi-tenant configuration using Keycloak for IAM | +| `mt-keycloak-vault` | multi-tenant configuration using Keycloak and Vault | + +## Testing examples + +Some example directories may contain a sub-directory called `hurl`. +Hurl is a CLI tool for testing HTTP requests and can be installed according to [this documentation](https://hurl.dev/docs/installation.html). +If the example contains a sub-directory named `hurl`, the example can be tested against HTTP calls with the following commands. + +```bash +cd ./hurl +hurl --variables-file ./local *.hurl --test +``` + +# Contributing + +All of the docker-compose files in examples are generated using [Nickel](https://nickel-lang.org/). +They are defined in a shared `.nickel` directory and generated using the `build.sh` script. + +## Prerequisites + +- [Nickel](https://nickel-lang.org/) version >= `1.5` installed + +## Generate example compose files + +To generate the docker-compose config for all examples, run + +```bash +cd .nickel +./build.sh +``` + +## Updating example compose files + +To update the configuration, simply edit the `*.ncl` config in the `.nickel` directory and regenerate the docker-compose files. + +## Adding new examples + +To add a new example with docker-compose file, simply create a new configuration key in the `root.ncl` and add a new entry in the `build.sh` script. +You may need to create the target example directory if it does not already exist. + +## Example with bootstrapping script + +If any example requires initialize steps, it should be made part of the docker-compose `depends_on` construct. +Ideally, infrastructure bootstrapping should be automatic (database, IAM), but not necessarily application bootstrapping (tenant onboarding). diff --git a/examples/mt-keycloak-vault/README.md b/examples/mt-keycloak-vault/README.md new file mode 100644 index 0000000000..207927dfcd --- /dev/null +++ b/examples/mt-keycloak-vault/README.md @@ -0,0 +1,16 @@ +## Configuration + +| Exposed Service | Description | +|---------------------------------|--------------------------| +| `localhost:8080/cloudagent` | Multi-tenant Cloud Agent | +| `localhost:8080/keycloak/admin` | Keycloak | +| `localhost:8200` | Vault | + +__Keycloak__ + +- Admin user `admin` +- Admin password `admin` + +__Vault__ + +- Root token `admin` diff --git a/examples/mt-keycloak-vault/compose.yaml b/examples/mt-keycloak-vault/compose.yaml index daf6939b36..6a313e1e19 100644 --- a/examples/mt-keycloak-vault/compose.yaml +++ b/examples/mt-keycloak-vault/compose.yaml @@ -5,7 +5,7 @@ configs: handle_path /didcomm* { reverse_proxy agent-default:8090 } - handle_path /prism-agent* { + handle_path /cloud-agent* { reverse_proxy agent-default:8085 } handle_path /keycloak* { @@ -46,14 +46,14 @@ services: POLLUX_DB_PASSWORD: postgres POLLUX_DB_PORT: '5432' POLLUX_DB_USER: postgres - POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-default:8080/prism-agent + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-default:8080/cloud-agent PRISM_NODE_HOST: node PRISM_NODE_PORT: '50053' - REST_SERVICE_URL: http://caddy-default:8080/prism-agent + REST_SERVICE_URL: http://caddy-default:8080/cloud-agent SECRET_STORAGE_BACKEND: vault VAULT_ADDR: http://vault-default:8200 VAULT_TOKEN: admin - image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.38.0 restart: always caddy-default: configs: @@ -103,12 +103,12 @@ services: - /hurl/*.hurl - --test environment: - HURL_KEYCLOAK_ADMIN_PASSWORD: admin - HURL_KEYCLOAK_ADMIN_USER: admin - HURL_KEYCLOAK_BASE_URL: http://keycloak-default:8080 - HURL_KEYCLOAK_CLIENT_ID: agent - HURL_KEYCLOAK_CLIENT_SECRET: agent-secret - HURL_KEYCLOAK_REALM: identus + HURL_keycloak_admin_password: admin + HURL_keycloak_admin_user: admin + HURL_keycloak_base_url: http://keycloak-default:8080 + HURL_keycloak_client_id: agent + HURL_keycloak_client_secret: agent-secret + HURL_keycloak_realm: identus image: ghcr.io/orange-opensource/hurl:4.2.0 volumes: - ../.shared/hurl/simple_realm:/hurl diff --git a/examples/mt-keycloak-vault/tests/01_create_users.hurl b/examples/mt-keycloak-vault/hurl/01_create_users.hurl similarity index 95% rename from examples/mt-keycloak-vault/tests/01_create_users.hurl rename to examples/mt-keycloak-vault/hurl/01_create_users.hurl index 9a412b9668..0e5d13854a 100644 --- a/examples/mt-keycloak-vault/tests/01_create_users.hurl +++ b/examples/mt-keycloak-vault/hurl/01_create_users.hurl @@ -66,7 +66,7 @@ HTTP 200 issuer_access_token: jsonpath "$.access_token" # Create Issuer wallet -POST {{ agent_url }}/prism-agent/wallets +POST {{ agent_url }}/cloud-agent/wallets Authorization: Bearer {{ issuer_access_token }} { "name": "issuer-wallet" @@ -85,7 +85,7 @@ HTTP 200 holder_access_token: jsonpath "$.access_token" # Create Holder wallet -POST {{ agent_url }}/prism-agent/wallets +POST {{ agent_url }}/cloud-agent/wallets Authorization: Bearer {{ holder_access_token }} { "name": "holder-wallet" @@ -104,7 +104,7 @@ HTTP 200 verifier_access_token: jsonpath "$.access_token" # Create Verifier wallet -POST {{ agent_url }}/prism-agent/wallets +POST {{ agent_url }}/cloud-agent/wallets Authorization: Bearer {{ verifier_access_token }} { "name": "verifier-wallet" diff --git a/examples/mt-keycloak-vault/tests/02_jwt_flow.hurl b/examples/mt-keycloak-vault/hurl/02_jwt_flow.hurl similarity index 83% rename from examples/mt-keycloak-vault/tests/02_jwt_flow.hurl rename to examples/mt-keycloak-vault/hurl/02_jwt_flow.hurl index c1ceb033b4..6e0c8edcab 100644 --- a/examples/mt-keycloak-vault/tests/02_jwt_flow.hurl +++ b/examples/mt-keycloak-vault/hurl/02_jwt_flow.hurl @@ -39,7 +39,7 @@ verifier_access_token: jsonpath "$.access_token" # Prerequisites ############################## # Issuer create DID -POST {{ agent_url }}/prism-agent/did-registrar/dids +POST {{ agent_url }}/cloud-agent/did-registrar/dids Authorization: Bearer {{ issuer_access_token }} { "documentTemplate": { @@ -54,10 +54,10 @@ Authorization: Bearer {{ issuer_access_token }} } HTTP 201 [Captures] -issuer_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" +issuer_did: jsonpath "$.longFormDid" # regex "(did:prism:[a-z0-9]+):.+$" # Holder create DID -POST {{ agent_url }}/prism-agent/did-registrar/dids +POST {{ agent_url }}/cloud-agent/did-registrar/dids Authorization: Bearer {{ holder_access_token }} { "documentTemplate": { @@ -78,7 +78,7 @@ holder_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" # Issuance Connection ############################## # Inviter create connection -POST {{ agent_url }}/prism-agent/connections +POST {{ agent_url }}/cloud-agent/connections Authorization: Bearer {{ issuer_access_token }} { "label": "My Connection" @@ -89,7 +89,7 @@ raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" issuer_connection_id: jsonpath "$.connectionId" # Invitee accept connection -POST {{ agent_url }}/prism-agent/connection-invitations +POST {{ agent_url }}/cloud-agent/connection-invitations Authorization: Bearer {{ holder_access_token }} { "invitation": "{{ raw_invitation }}" @@ -99,7 +99,7 @@ HTTP 200 holder_connection_id: jsonpath "$.connectionId" # Wait for inviter connection status -GET {{ agent_url }}/prism-agent/connections/{{ issuer_connection_id }} +GET {{ agent_url }}/cloud-agent/connections/{{ issuer_connection_id }} Authorization: Bearer {{ issuer_access_token }} [Options] retry: -1 @@ -108,7 +108,7 @@ HTTP 200 jsonpath "$.state" == "ConnectionResponseSent" # Wait for invitee connection status -GET {{ agent_url }}/prism-agent/connections/{{ holder_connection_id }} +GET {{ agent_url }}/cloud-agent/connections/{{ holder_connection_id }} Authorization: Bearer {{ holder_access_token }} [Options] retry: -1 @@ -120,7 +120,7 @@ jsonpath "$.state" == "ConnectionResponseReceived" # Issuance ############################## # Issuer create credential offer -POST {{ agent_url }}/prism-agent/issue-credentials/credential-offers +POST {{ agent_url }}/cloud-agent/issue-credentials/credential-offers Authorization: Bearer {{ issuer_access_token }} { "claims": { @@ -138,7 +138,7 @@ issuer_cred_record_id: jsonpath "$.recordId" didcomm_issuing_thid: jsonpath "$.thid" # Holder wait for OfferReceived state -GET {{ agent_url }}/prism-agent/issue-credentials/records +GET {{ agent_url }}/cloud-agent/issue-credentials/records Authorization: Bearer {{ holder_access_token }} [QueryStringParams] thid: {{ didcomm_issuing_thid }} @@ -151,7 +151,7 @@ jsonpath "$.contents[0].protocolState" == "OfferReceived" holder_cred_record_id: jsonpath "$.contents[0].recordId" # Holder accept a credential-offer -POST {{ agent_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }}/accept-offer +POST {{ agent_url }}/cloud-agent/issue-credentials/records/{{ holder_cred_record_id }}/accept-offer Authorization: Bearer {{ holder_access_token }} { "subjectId": "{{ holder_did }}" @@ -159,7 +159,7 @@ Authorization: Bearer {{ holder_access_token }} HTTP 200 # Holder wait for CredentialReceived state -GET {{ agent_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }} +GET {{ agent_url }}/cloud-agent/issue-credentials/records/{{ holder_cred_record_id }} Authorization: Bearer {{ holder_access_token }} [Options] retry: -1 @@ -168,7 +168,7 @@ HTTP 200 jsonpath "$.protocolState" == "CredentialReceived" # Issuer wait for CredentialSent state -GET {{ agent_url }}/prism-agent/issue-credentials/records/{{ issuer_cred_record_id }} +GET {{ agent_url }}/cloud-agent/issue-credentials/records/{{ issuer_cred_record_id }} Authorization: Bearer {{ issuer_access_token }} [Options] retry: -1 @@ -180,7 +180,7 @@ jsonpath "$.protocolState" == "CredentialSent" # Presentation Connection ############################## # Inviter create connection -POST {{ agent_url }}/prism-agent/connections +POST {{ agent_url }}/cloud-agent/connections Authorization: Bearer {{ verifier_access_token }} { "label": "My Connection" @@ -191,7 +191,7 @@ raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" verifier_connection_id: jsonpath "$.connectionId" # Invitee accept connection -POST {{ agent_url }}/prism-agent/connection-invitations +POST {{ agent_url }}/cloud-agent/connection-invitations Authorization: Bearer {{ holder_access_token }} { "invitation": "{{ raw_invitation }}" @@ -201,7 +201,7 @@ HTTP 200 holder_connection_id: jsonpath "$.connectionId" # Wait for inviter connection status -GET {{ agent_url }}/prism-agent/connections/{{ verifier_connection_id }} +GET {{ agent_url }}/cloud-agent/connections/{{ verifier_connection_id }} Authorization: Bearer {{ verifier_access_token }} [Options] retry: -1 @@ -210,7 +210,7 @@ HTTP 200 jsonpath "$.state" == "ConnectionResponseSent" # Wait for invitee connection status -GET {{ agent_url }}/prism-agent/connections/{{ holder_connection_id }} +GET {{ agent_url }}/cloud-agent/connections/{{ holder_connection_id }} Authorization: Bearer {{ holder_access_token }} [Options] retry: -1 @@ -222,7 +222,7 @@ jsonpath "$.state" == "ConnectionResponseReceived" # Presentation ############################## # Verifier create presentation request -POST {{ agent_url }}/prism-agent/present-proof/presentations +POST {{ agent_url }}/cloud-agent/present-proof/presentations Authorization: Bearer {{ verifier_access_token }} { "connectionId": "{{ verifier_connection_id }}", @@ -238,7 +238,7 @@ verifier_presentation_id: jsonpath "$.presentationId" didcomm_presentation_thid: jsonpath "$.thid" # Holder wait for RequestReceived state -GET {{ agent_url }}/prism-agent/present-proof/presentations +GET {{ agent_url }}/cloud-agent/present-proof/presentations Authorization: Bearer {{ holder_access_token }} [QueryStringParams] thid: {{ didcomm_presentation_thid }} @@ -251,7 +251,7 @@ jsonpath "$.contents[0].status" == "RequestReceived" holder_presentation_id: jsonpath "$.contents[0].presentationId" # Holder accept presentation request -PATCH {{ agent_url }}/prism-agent/present-proof/presentations/{{ holder_presentation_id }} +PATCH {{ agent_url }}/cloud-agent/present-proof/presentations/{{ holder_presentation_id }} Authorization: Bearer {{ holder_access_token }} { "action": "request-accept", @@ -260,7 +260,7 @@ Authorization: Bearer {{ holder_access_token }} HTTP 200 # Holder wait for PresentationSent state -GET {{ agent_url }}/prism-agent/present-proof/presentations +GET {{ agent_url }}/cloud-agent/present-proof/presentations Authorization: Bearer {{ holder_access_token }} [QueryStringParams] thid: {{ didcomm_presentation_thid }} @@ -271,7 +271,7 @@ HTTP 200 jsonpath "$.contents[0].status" == "PresentationSent" # Verfiier wait for PresentationVerified state -GET {{ agent_url }}/prism-agent/present-proof/presentations +GET {{ agent_url }}/cloud-agent/present-proof/presentations Authorization: Bearer {{ verifier_access_token }} [QueryStringParams] thid: {{ didcomm_presentation_thid }} diff --git a/examples/mt-keycloak-vault/tests/local b/examples/mt-keycloak-vault/hurl/local similarity index 100% rename from examples/mt-keycloak-vault/tests/local rename to examples/mt-keycloak-vault/hurl/local diff --git a/examples/mt-keycloak/README.md b/examples/mt-keycloak/README.md new file mode 100644 index 0000000000..373b8a4bfa --- /dev/null +++ b/examples/mt-keycloak/README.md @@ -0,0 +1,11 @@ +## Configuration + +| Exposed Service | Description | +|---------------------------------|--------------------------| +| `localhost:8080/cloud-agent` | Multi-tenant Cloud Agent | +| `localhost:8080/keycloak/admin` | Keycloak | + +__Keycloak__ + +- Admin user `admin` +- Admin password `admin` diff --git a/examples/mt-keycloak/compose.yaml b/examples/mt-keycloak/compose.yaml index 3ebc37d1ad..ea2b55a150 100644 --- a/examples/mt-keycloak/compose.yaml +++ b/examples/mt-keycloak/compose.yaml @@ -5,7 +5,7 @@ configs: handle_path /didcomm* { reverse_proxy agent-default:8090 } - handle_path /prism-agent* { + handle_path /cloud-agent* { reverse_proxy agent-default:8085 } handle_path /keycloak* { @@ -46,12 +46,12 @@ services: POLLUX_DB_PASSWORD: postgres POLLUX_DB_PORT: '5432' POLLUX_DB_USER: postgres - POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-default:8080/prism-agent + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-default:8080/cloud-agent PRISM_NODE_HOST: node PRISM_NODE_PORT: '50053' - REST_SERVICE_URL: http://caddy-default:8080/prism-agent + REST_SERVICE_URL: http://caddy-default:8080/cloud-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.38.0 restart: always caddy-default: configs: @@ -101,12 +101,12 @@ services: - /hurl/*.hurl - --test environment: - HURL_KEYCLOAK_ADMIN_PASSWORD: admin - HURL_KEYCLOAK_ADMIN_USER: admin - HURL_KEYCLOAK_BASE_URL: http://keycloak-default:8080 - HURL_KEYCLOAK_CLIENT_ID: agent - HURL_KEYCLOAK_CLIENT_SECRET: agent-secret - HURL_KEYCLOAK_REALM: identus + HURL_keycloak_admin_password: admin + HURL_keycloak_admin_user: admin + HURL_keycloak_base_url: http://keycloak-default:8080 + HURL_keycloak_client_id: agent + HURL_keycloak_client_secret: agent-secret + HURL_keycloak_realm: identus image: ghcr.io/orange-opensource/hurl:4.2.0 volumes: - ../.shared/hurl/simple_realm:/hurl diff --git a/examples/mt-keycloak/tests/01_create_users.hurl b/examples/mt-keycloak/hurl/01_create_users.hurl similarity index 95% rename from examples/mt-keycloak/tests/01_create_users.hurl rename to examples/mt-keycloak/hurl/01_create_users.hurl index 9a412b9668..0e5d13854a 100644 --- a/examples/mt-keycloak/tests/01_create_users.hurl +++ b/examples/mt-keycloak/hurl/01_create_users.hurl @@ -66,7 +66,7 @@ HTTP 200 issuer_access_token: jsonpath "$.access_token" # Create Issuer wallet -POST {{ agent_url }}/prism-agent/wallets +POST {{ agent_url }}/cloud-agent/wallets Authorization: Bearer {{ issuer_access_token }} { "name": "issuer-wallet" @@ -85,7 +85,7 @@ HTTP 200 holder_access_token: jsonpath "$.access_token" # Create Holder wallet -POST {{ agent_url }}/prism-agent/wallets +POST {{ agent_url }}/cloud-agent/wallets Authorization: Bearer {{ holder_access_token }} { "name": "holder-wallet" @@ -104,7 +104,7 @@ HTTP 200 verifier_access_token: jsonpath "$.access_token" # Create Verifier wallet -POST {{ agent_url }}/prism-agent/wallets +POST {{ agent_url }}/cloud-agent/wallets Authorization: Bearer {{ verifier_access_token }} { "name": "verifier-wallet" diff --git a/examples/mt-keycloak/tests/02_jwt_flow.hurl b/examples/mt-keycloak/hurl/02_jwt_flow.hurl similarity index 83% rename from examples/mt-keycloak/tests/02_jwt_flow.hurl rename to examples/mt-keycloak/hurl/02_jwt_flow.hurl index 2169d2f777..41999eeb87 100644 --- a/examples/mt-keycloak/tests/02_jwt_flow.hurl +++ b/examples/mt-keycloak/hurl/02_jwt_flow.hurl @@ -39,7 +39,7 @@ verifier_access_token: jsonpath "$.access_token" # Prerequisites ############################## # Issuer create DID -POST {{ agent_url }}/prism-agent/did-registrar/dids +POST {{ agent_url }}/cloud-agent/did-registrar/dids Authorization: Bearer {{ issuer_access_token }} { "documentTemplate": { @@ -57,12 +57,12 @@ HTTP 201 issuer_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" # Issuer publish DID -POST {{ agent_url }}/prism-agent/did-registrar/dids/{{ issuer_did }}/publications +POST {{ agent_url }}/cloud-agent/did-registrar/dids/{{ issuer_did }}/publications Authorization: Bearer {{ issuer_access_token }} HTTP 202 # Issuer wait for DID to be published -GET {{ agent_url }}/prism-agent/did-registrar/dids/{{ issuer_did }} +GET {{ agent_url }}/cloud-agent/did-registrar/dids/{{ issuer_did }} Authorization: Bearer {{ issuer_access_token }} [Options] retry: -1 @@ -71,7 +71,7 @@ HTTP 200 jsonpath "$.status" == "PUBLISHED" # Holder create DID -POST {{ agent_url }}/prism-agent/did-registrar/dids +POST {{ agent_url }}/cloud-agent/did-registrar/dids Authorization: Bearer {{ holder_access_token }} { "documentTemplate": { @@ -92,7 +92,7 @@ holder_did: jsonpath "$.longFormDid" # Issuance Connection ############################## # Inviter create connection -POST {{ agent_url }}/prism-agent/connections +POST {{ agent_url }}/cloud-agent/connections Authorization: Bearer {{ issuer_access_token }} { "label": "My Connection" @@ -103,7 +103,7 @@ raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" issuer_connection_id: jsonpath "$.connectionId" # Invitee accept connection -POST {{ agent_url }}/prism-agent/connection-invitations +POST {{ agent_url }}/cloud-agent/connection-invitations Authorization: Bearer {{ holder_access_token }} { "invitation": "{{ raw_invitation }}" @@ -113,7 +113,7 @@ HTTP 200 holder_connection_id: jsonpath "$.connectionId" # Wait for inviter connection status -GET {{ agent_url }}/prism-agent/connections/{{ issuer_connection_id }} +GET {{ agent_url }}/cloud-agent/connections/{{ issuer_connection_id }} Authorization: Bearer {{ issuer_access_token }} [Options] retry: -1 @@ -122,7 +122,7 @@ HTTP 200 jsonpath "$.state" == "ConnectionResponseSent" # Wait for invitee connection status -GET {{ agent_url }}/prism-agent/connections/{{ holder_connection_id }} +GET {{ agent_url }}/cloud-agent/connections/{{ holder_connection_id }} Authorization: Bearer {{ holder_access_token }} [Options] retry: -1 @@ -134,7 +134,7 @@ jsonpath "$.state" == "ConnectionResponseReceived" # Issuance ############################## # Issuer create credential offer -POST {{ agent_url }}/prism-agent/issue-credentials/credential-offers +POST {{ agent_url }}/cloud-agent/issue-credentials/credential-offers Authorization: Bearer {{ issuer_access_token }} { "claims": { @@ -152,7 +152,7 @@ issuer_cred_record_id: jsonpath "$.recordId" didcomm_issuing_thid: jsonpath "$.thid" # Holder wait for OfferReceived state -GET {{ agent_url }}/prism-agent/issue-credentials/records +GET {{ agent_url }}/cloud-agent/issue-credentials/records Authorization: Bearer {{ holder_access_token }} [QueryStringParams] thid: {{ didcomm_issuing_thid }} @@ -165,7 +165,7 @@ jsonpath "$.contents[0].protocolState" == "OfferReceived" holder_cred_record_id: jsonpath "$.contents[0].recordId" # Holder accept a credential-offer -POST {{ agent_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }}/accept-offer +POST {{ agent_url }}/cloud-agent/issue-credentials/records/{{ holder_cred_record_id }}/accept-offer Authorization: Bearer {{ holder_access_token }} { "subjectId": "{{ holder_did }}" @@ -173,7 +173,7 @@ Authorization: Bearer {{ holder_access_token }} HTTP 200 # Holder wait for CredentialReceived state -GET {{ agent_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }} +GET {{ agent_url }}/cloud-agent/issue-credentials/records/{{ holder_cred_record_id }} Authorization: Bearer {{ holder_access_token }} [Options] retry: -1 @@ -182,7 +182,7 @@ HTTP 200 jsonpath "$.protocolState" == "CredentialReceived" # Issuer wait for CredentialSent state -GET {{ agent_url }}/prism-agent/issue-credentials/records/{{ issuer_cred_record_id }} +GET {{ agent_url }}/cloud-agent/issue-credentials/records/{{ issuer_cred_record_id }} Authorization: Bearer {{ issuer_access_token }} [Options] retry: -1 @@ -194,7 +194,7 @@ jsonpath "$.protocolState" == "CredentialSent" # Presentation Connection ############################## # Inviter create connection -POST {{ agent_url }}/prism-agent/connections +POST {{ agent_url }}/cloud-agent/connections Authorization: Bearer {{ verifier_access_token }} { "label": "My Connection" @@ -205,7 +205,7 @@ raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" verifier_connection_id: jsonpath "$.connectionId" # Invitee accept connection -POST {{ agent_url }}/prism-agent/connection-invitations +POST {{ agent_url }}/cloud-agent/connection-invitations Authorization: Bearer {{ holder_access_token }} { "invitation": "{{ raw_invitation }}" @@ -215,7 +215,7 @@ HTTP 200 holder_connection_id: jsonpath "$.connectionId" # Wait for inviter connection status -GET {{ agent_url }}/prism-agent/connections/{{ verifier_connection_id }} +GET {{ agent_url }}/cloud-agent/connections/{{ verifier_connection_id }} Authorization: Bearer {{ verifier_access_token }} [Options] retry: -1 @@ -224,7 +224,7 @@ HTTP 200 jsonpath "$.state" == "ConnectionResponseSent" # Wait for invitee connection status -GET {{ agent_url }}/prism-agent/connections/{{ holder_connection_id }} +GET {{ agent_url }}/cloud-agent/connections/{{ holder_connection_id }} Authorization: Bearer {{ holder_access_token }} [Options] retry: -1 @@ -236,7 +236,7 @@ jsonpath "$.state" == "ConnectionResponseReceived" # Presentation ############################## # Verifier create presentation request -POST {{ agent_url }}/prism-agent/present-proof/presentations +POST {{ agent_url }}/cloud-agent/present-proof/presentations Authorization: Bearer {{ verifier_access_token }} { "connectionId": "{{ verifier_connection_id }}", @@ -252,7 +252,7 @@ verifier_presentation_id: jsonpath "$.presentationId" didcomm_presentation_thid: jsonpath "$.thid" # Holder wait for RequestReceived state -GET {{ agent_url }}/prism-agent/present-proof/presentations +GET {{ agent_url }}/cloud-agent/present-proof/presentations Authorization: Bearer {{ holder_access_token }} [QueryStringParams] thid: {{ didcomm_presentation_thid }} @@ -265,7 +265,7 @@ jsonpath "$.contents[0].status" == "RequestReceived" holder_presentation_id: jsonpath "$.contents[0].presentationId" # Holder accept presentation request -PATCH {{ agent_url }}/prism-agent/present-proof/presentations/{{ holder_presentation_id }} +PATCH {{ agent_url }}/cloud-agent/present-proof/presentations/{{ holder_presentation_id }} Authorization: Bearer {{ holder_access_token }} { "action": "request-accept", @@ -274,7 +274,7 @@ Authorization: Bearer {{ holder_access_token }} HTTP 200 # Holder wait for PresentationSent state -GET {{ agent_url }}/prism-agent/present-proof/presentations +GET {{ agent_url }}/cloud-agent/present-proof/presentations Authorization: Bearer {{ holder_access_token }} [QueryStringParams] thid: {{ didcomm_presentation_thid }} @@ -285,7 +285,7 @@ HTTP 200 jsonpath "$.contents[0].status" == "PresentationSent" # Verfiier wait for PresentationVerified state -GET {{ agent_url }}/prism-agent/present-proof/presentations +GET {{ agent_url }}/cloud-agent/present-proof/presentations Authorization: Bearer {{ verifier_access_token }} [QueryStringParams] thid: {{ didcomm_presentation_thid }} diff --git a/examples/mt-keycloak/tests/local b/examples/mt-keycloak/hurl/local similarity index 100% rename from examples/mt-keycloak/tests/local rename to examples/mt-keycloak/hurl/local diff --git a/examples/mt/README.md b/examples/mt/README.md new file mode 100644 index 0000000000..030af3b4bd --- /dev/null +++ b/examples/mt/README.md @@ -0,0 +1,5 @@ +## Configuration + +| Exposed Service | Description | +|------------------------------|--------------------------| +| `localhost:8080/cloud-agent` | Multi-tenant Cloud Agent | diff --git a/examples/mt/compose.yaml b/examples/mt/compose.yaml index 84e4f7a3d1..0b355b8049 100644 --- a/examples/mt/compose.yaml +++ b/examples/mt/compose.yaml @@ -5,7 +5,7 @@ configs: handle_path /didcomm* { reverse_proxy agent-default:8090 } - handle_path /prism-agent* { + handle_path /cloud-agent* { reverse_proxy agent-default:8085 } handle_path /keycloak* { @@ -39,12 +39,12 @@ services: POLLUX_DB_PASSWORD: postgres POLLUX_DB_PORT: '5432' POLLUX_DB_USER: postgres - POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-default:8080/prism-agent + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-default:8080/cloud-agent PRISM_NODE_HOST: node PRISM_NODE_PORT: '50053' - REST_SERVICE_URL: http://caddy-default:8080/prism-agent + REST_SERVICE_URL: http://caddy-default:8080/cloud-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.38.0 restart: always caddy-default: configs: diff --git a/examples/st-multi/README.me b/examples/st-multi/README.me new file mode 100644 index 0000000000..da8c69f181 --- /dev/null +++ b/examples/st-multi/README.me @@ -0,0 +1,7 @@ +## Configuration + +|Exposed Service |Description | +|----------------------------|--------------------------------------| +|`localhost:8080/cloud-agent`|Single-tenant Cloud Agent#1 (issuer) | +|`localhost:8081/cloud-agent`|Single-tenant Cloud Agent#2 (holder) | +|`localhost:8082/cloud-agent`|Single-tenant Cloud Agent#3 (verifier)| diff --git a/examples/st-multi/compose.yaml b/examples/st-multi/compose.yaml index 34650ad413..88771d576a 100644 --- a/examples/st-multi/compose.yaml +++ b/examples/st-multi/compose.yaml @@ -5,7 +5,7 @@ configs: handle_path /didcomm* { reverse_proxy agent-holder:8090 } - handle_path /prism-agent* { + handle_path /cloud-agent* { reverse_proxy agent-holder:8085 } handle_path /keycloak* { @@ -21,7 +21,7 @@ configs: handle_path /didcomm* { reverse_proxy agent-issuer:8090 } - handle_path /prism-agent* { + handle_path /cloud-agent* { reverse_proxy agent-issuer:8085 } handle_path /keycloak* { @@ -37,7 +37,7 @@ configs: handle_path /didcomm* { reverse_proxy agent-verifier:8090 } - handle_path /prism-agent* { + handle_path /cloud-agent* { reverse_proxy agent-verifier:8085 } handle_path /keycloak* { @@ -71,12 +71,12 @@ services: POLLUX_DB_PASSWORD: postgres POLLUX_DB_PORT: '5432' POLLUX_DB_USER: postgres - POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-holder:8081/prism-agent + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-holder:8081/cloud-agent PRISM_NODE_HOST: node PRISM_NODE_PORT: '50053' - REST_SERVICE_URL: http://caddy-holder:8081/prism-agent + REST_SERVICE_URL: http://caddy-holder:8081/cloud-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.38.0 restart: always agent-issuer: depends_on: @@ -101,12 +101,12 @@ services: POLLUX_DB_PASSWORD: postgres POLLUX_DB_PORT: '5432' POLLUX_DB_USER: postgres - POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/prism-agent + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/cloud-agent PRISM_NODE_HOST: node PRISM_NODE_PORT: '50053' - REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent + REST_SERVICE_URL: http://caddy-issuer:8080/cloud-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.38.0 restart: always agent-verifier: depends_on: @@ -131,12 +131,12 @@ services: POLLUX_DB_PASSWORD: postgres POLLUX_DB_PORT: '5432' POLLUX_DB_USER: postgres - POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-verifier:8082/prism-agent + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-verifier:8082/cloud-agent PRISM_NODE_HOST: node PRISM_NODE_PORT: '50053' - REST_SERVICE_URL: http://caddy-verifier:8082/prism-agent + REST_SERVICE_URL: http://caddy-verifier:8082/cloud-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.38.0 restart: always caddy-holder: configs: diff --git a/examples/st-multi/tests/01_jwt_flow.hurl b/examples/st-multi/hurl/01_jwt_flow.hurl similarity index 77% rename from examples/st-multi/tests/01_jwt_flow.hurl rename to examples/st-multi/hurl/01_jwt_flow.hurl index 94bc540275..4752a1adb1 100644 --- a/examples/st-multi/tests/01_jwt_flow.hurl +++ b/examples/st-multi/hurl/01_jwt_flow.hurl @@ -2,7 +2,7 @@ # Prerequisites ############################## # Issuer create DID -POST {{ issuer_url }}/prism-agent/did-registrar/dids +POST {{ issuer_url }}/cloud-agent/did-registrar/dids { "documentTemplate": { "publicKeys": [ @@ -19,11 +19,11 @@ HTTP 201 issuer_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" # Issuer publish DID -POST {{ issuer_url }}/prism-agent/did-registrar/dids/{{ issuer_did }}/publications +POST {{ issuer_url }}/cloud-agent/did-registrar/dids/{{ issuer_did }}/publications HTTP 202 # Issuer wait for DID to be published -GET {{ issuer_url }}/prism-agent/did-registrar/dids/{{ issuer_did }} +GET {{ issuer_url }}/cloud-agent/did-registrar/dids/{{ issuer_did }} [Options] retry: -1 HTTP 200 @@ -31,7 +31,7 @@ HTTP 200 jsonpath "$.status" == "PUBLISHED" # Holder create DID -POST {{ holder_url }}/prism-agent/did-registrar/dids +POST {{ holder_url }}/cloud-agent/did-registrar/dids { "documentTemplate": { "publicKeys": [ @@ -51,7 +51,7 @@ holder_did: jsonpath "$.longFormDid" # Issuance Connection ############################## # Inviter create connection -POST {{ issuer_url }}/prism-agent/connections +POST {{ issuer_url }}/cloud-agent/connections { "label": "My Connection" } @@ -61,7 +61,7 @@ raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" issuer_connection_id: jsonpath "$.connectionId" # Invitee accept connection -POST {{ holder_url }}/prism-agent/connection-invitations +POST {{ holder_url }}/cloud-agent/connection-invitations { "invitation": "{{ raw_invitation }}" } @@ -70,7 +70,7 @@ HTTP 200 holder_connection_id: jsonpath "$.connectionId" # Wait for inviter connection status -GET {{ issuer_url }}/prism-agent/connections/{{ issuer_connection_id }} +GET {{ issuer_url }}/cloud-agent/connections/{{ issuer_connection_id }} [Options] retry: -1 HTTP 200 @@ -78,7 +78,7 @@ HTTP 200 jsonpath "$.state" == "ConnectionResponseSent" # Wait for invitee connection status -GET {{ holder_url }}/prism-agent/connections/{{ holder_connection_id }} +GET {{ holder_url }}/cloud-agent/connections/{{ holder_connection_id }} [Options] retry: -1 HTTP 200 @@ -89,7 +89,7 @@ jsonpath "$.state" == "ConnectionResponseReceived" # Issuance ############################## # Issuer create credential offer -POST {{ issuer_url }}/prism-agent/issue-credentials/credential-offers +POST {{ issuer_url }}/cloud-agent/issue-credentials/credential-offers { "claims": { "emailAddress": "alice@wonderland.com", @@ -106,7 +106,7 @@ issuer_cred_record_id: jsonpath "$.recordId" didcomm_issuing_thid: jsonpath "$.thid" # Holder wait for OfferReceived state -GET {{ holder_url }}/prism-agent/issue-credentials/records +GET {{ holder_url }}/cloud-agent/issue-credentials/records [QueryStringParams] thid: {{ didcomm_issuing_thid }} [Options] @@ -118,14 +118,14 @@ jsonpath "$.contents[0].protocolState" == "OfferReceived" holder_cred_record_id: jsonpath "$.contents[0].recordId" # Holder accept a credential-offer -POST {{ holder_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }}/accept-offer +POST {{ holder_url }}/cloud-agent/issue-credentials/records/{{ holder_cred_record_id }}/accept-offer { "subjectId": "{{ holder_did }}" } HTTP 200 # Holder wait for CredentialReceived state -GET {{ holder_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }} +GET {{ holder_url }}/cloud-agent/issue-credentials/records/{{ holder_cred_record_id }} [Options] retry: -1 HTTP 200 @@ -133,7 +133,7 @@ HTTP 200 jsonpath "$.protocolState" == "CredentialReceived" # Issuer wait for CredentialSent state -GET {{ issuer_url }}/prism-agent/issue-credentials/records/{{ issuer_cred_record_id }} +GET {{ issuer_url }}/cloud-agent/issue-credentials/records/{{ issuer_cred_record_id }} [Options] retry: -1 HTTP 200 @@ -144,7 +144,7 @@ jsonpath "$.protocolState" == "CredentialSent" # Presentation Connection ############################## # Inviter create connection -POST {{ verifier_url }}/prism-agent/connections +POST {{ verifier_url }}/cloud-agent/connections { "label": "My Connection" } @@ -154,7 +154,7 @@ raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" verifier_connection_id: jsonpath "$.connectionId" # Invitee accept connection -POST {{ holder_url }}/prism-agent/connection-invitations +POST {{ holder_url }}/cloud-agent/connection-invitations { "invitation": "{{ raw_invitation }}" } @@ -163,7 +163,7 @@ HTTP 200 holder_connection_id: jsonpath "$.connectionId" # Wait for inviter connection status -GET {{ verifier_url }}/prism-agent/connections/{{ verifier_connection_id }} +GET {{ verifier_url }}/cloud-agent/connections/{{ verifier_connection_id }} [Options] retry: -1 HTTP 200 @@ -171,7 +171,7 @@ HTTP 200 jsonpath "$.state" == "ConnectionResponseSent" # Wait for invitee connection status -GET {{ holder_url }}/prism-agent/connections/{{ holder_connection_id }} +GET {{ holder_url }}/cloud-agent/connections/{{ holder_connection_id }} [Options] retry: -1 HTTP 200 @@ -182,7 +182,7 @@ jsonpath "$.state" == "ConnectionResponseReceived" # Presentation ############################## # Verifier create presentation request -POST {{ verifier_url }}/prism-agent/present-proof/presentations +POST {{ verifier_url }}/cloud-agent/present-proof/presentations { "connectionId": "{{ verifier_connection_id }}", "proofs":[], @@ -197,7 +197,7 @@ verifier_presentation_id: jsonpath "$.presentationId" didcomm_presentation_thid: jsonpath "$.thid" # Holder wait for RequestReceived state -GET {{ holder_url }}/prism-agent/present-proof/presentations +GET {{ holder_url }}/cloud-agent/present-proof/presentations [QueryStringParams] thid: {{ didcomm_presentation_thid }} [Options] @@ -209,7 +209,7 @@ jsonpath "$.contents[0].status" == "RequestReceived" holder_presentation_id: jsonpath "$.contents[0].presentationId" # Holder accept presentation request -PATCH {{ holder_url }}/prism-agent/present-proof/presentations/{{ holder_presentation_id }} +PATCH {{ holder_url }}/cloud-agent/present-proof/presentations/{{ holder_presentation_id }} { "action": "request-accept", "proofId": ["{{ holder_cred_record_id }}"] @@ -217,7 +217,7 @@ PATCH {{ holder_url }}/prism-agent/present-proof/presentations/{{ holder_present HTTP 200 # Holder wait for PresentationSent state -GET {{ holder_url }}/prism-agent/present-proof/presentations +GET {{ holder_url }}/cloud-agent/present-proof/presentations [QueryStringParams] thid: {{ didcomm_presentation_thid }} [Options] @@ -227,7 +227,7 @@ HTTP 200 jsonpath "$.contents[0].status" == "PresentationSent" # Verfiier wait for PresentationVerified state -GET {{ verifier_url }}/prism-agent/present-proof/presentations +GET {{ verifier_url }}/cloud-agent/present-proof/presentations [QueryStringParams] thid: {{ didcomm_presentation_thid }} [Options] diff --git a/examples/st-multi/tests/local b/examples/st-multi/hurl/local similarity index 100% rename from examples/st-multi/tests/local rename to examples/st-multi/hurl/local diff --git a/examples/st-oid4vci/README.md b/examples/st-oid4vci/README.md index 85b357d0a3..956da6a0db 100644 --- a/examples/st-oid4vci/README.md +++ b/examples/st-oid4vci/README.md @@ -3,24 +3,6 @@ ## Prerequisites - Docker installed v2.24.0 or later -- Python 3 with the following packages installed - - [requests](https://pypi.org/project/requests/) - - [pyjwt](https://pyjwt.readthedocs.io/en/stable/) - - [cryptography](https://cryptography.io/en/latest/) -- Virtual environment (optional) - -Example of the script to install the required packages in a virtual environment: -```shell -python -m venv {path-to-the-project-dir}/identus-cloud-agent/examples/st-oid4vci/python-env -source {path-to-the-project-dir}/identus-cloud-agent/examples/st-oid4vci/python-env/bin/activate -pip install requests pyjwt cryptography -``` - -- the latest Cloud Agent image is built and available in the local Docker registry - -```shell -sbt docker:publishLocal -``` ### 1. Spin up the agent stack with pre-configured Keycloak @@ -32,10 +14,16 @@ The Keycloak UI is available at `http://localhost:9980` and the admin username i ### 2. Run the issuance demo script +Build the demo application and run it using + ```bash -python demo.py +docker build -t identus-oid4vci-demo:latest ./demo +docker run --network -it identus-oid4vci-demo:latest ``` +where `NETWORK_NAME` is the docker-compose network name from agent stack. +By default, this value should be `st-oid4vci_default`. + - 2.1 Follow the instructions in the terminal. The holder will then be asked to log in via a browser - 2.2 Enter the username `alice` and password `1234` to log in - 2.3 Grant access for the scopes displayed on the consent UI diff --git a/examples/st-oid4vci/compose.yaml b/examples/st-oid4vci/compose.yaml index dde0a828ff..adc542a455 100644 --- a/examples/st-oid4vci/compose.yaml +++ b/examples/st-oid4vci/compose.yaml @@ -5,7 +5,7 @@ configs: handle_path /didcomm* { reverse_proxy agent-issuer:8090 } - handle_path /prism-agent* { + handle_path /cloud-agent* { reverse_proxy agent-issuer:8085 } handle_path /keycloak* { @@ -39,12 +39,12 @@ services: POLLUX_DB_PASSWORD: postgres POLLUX_DB_PORT: '5432' POLLUX_DB_USER: postgres - POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/prism-agent + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/cloud-agent PRISM_NODE_HOST: node PRISM_NODE_PORT: '50053' - REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent + REST_SERVICE_URL: http://caddy-issuer:8080/cloud-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.38.0 restart: always caddy-issuer: configs: @@ -102,10 +102,10 @@ services: - --hostname-url=http://localhost:9980 - --hostname-admin-url=http://localhost:9980 environment: - IDENTUS_URL: http://caddy-issuer:8080/prism-agent + IDENTUS_URL: http://caddy-issuer:8080/cloud-agent KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin - image: ghcr.io/hyperledger/identus-keycloak-plugins:0.1.0 + image: ghcr.io/hyperledger/identus-keycloak-plugins:0.2.0 ports: - 9980:8080 restart: always diff --git a/examples/st-oid4vci/demo/Dockerfile b/examples/st-oid4vci/demo/Dockerfile new file mode 100644 index 0000000000..b68166517b --- /dev/null +++ b/examples/st-oid4vci/demo/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12 +WORKDIR /workspace +COPY requirements.txt /workspace/requirements.txt +RUN pip install -r requirements.txt +COPY . /workspace +CMD ["python", "main.py"] diff --git a/examples/st-oid4vci/demo.py b/examples/st-oid4vci/demo/main.py similarity index 90% rename from examples/st-oid4vci/demo.py rename to examples/st-oid4vci/demo/main.py index 0dd12acf4d..b48239a161 100755 --- a/examples/st-oid4vci/demo.py +++ b/examples/st-oid4vci/demo/main.py @@ -6,14 +6,14 @@ import requests from cryptography.hazmat.primitives.asymmetric import ec -MOCKSERVER_URL = "http://localhost:7777" +MOCKSERVER_URL = "http://mockserver:1080" LOGIN_REDIRECT_URL = "http://localhost:7777/cb" -AGENT_URL = "http://localhost:8080/prism-agent" +AGENT_URL = "http://caddy-issuer:8080/cloud-agent" CREDENTIAL_ISSUER = None CREDENTIAL_ISSUER_DID = None CREDENTIAL_CONFIGURATION_ID = "UniversityDegreeCredential" -AUTHORIZATION_SERVER = "http://localhost:9980/realms/students" +AUTHORIZATION_SERVER = "http://external-keycloak-issuer:8080/realms/students" ALICE_CLIENT_ID = "alice-wallet" @@ -24,10 +24,11 @@ def prepare_mock_server(): - # reset mock server - requests.put(f"{MOCKSERVER_URL}/mockserver/reset") + """ + Prepare mock server for authorization redirect from front-end channel + """ - # mock wallet authorization callback endpoint + requests.put(f"{MOCKSERVER_URL}/mockserver/reset") requests.put( f"{MOCKSERVER_URL}/mockserver/expectation", json={ @@ -41,7 +42,16 @@ def prepare_mock_server(): def prepare_issuer(): - # prepare issuging DID + """ + Prepare an oid4vci issuer + 1. Create issuing DID + 2. Publish issuing DID + 3. Create credential schema + 4. Create oid4vci issuer + 5. Create oid4vci credential configuration from schema + """ + + # 1. Create issuing DID dids = requests.get(f"{AGENT_URL}/did-registrar/dids").json()["contents"] if len(dids) == 0: requests.post( @@ -55,6 +65,7 @@ def prepare_issuer(): ) dids = requests.get(f"{AGENT_URL}/did-registrar/dids").json()["contents"] + # 2. Publish issuing DID issuer_did = dids[0] while issuer_did["status"] != "PUBLISHED": time.sleep(2) @@ -72,7 +83,7 @@ def prepare_issuer(): global CREDENTIAL_ISSUER_DID CREDENTIAL_ISSUER_DID = canonical_did - # prepare schema + # 3. Create credential schema schema = requests.post( f"{AGENT_URL}/schema-registry/schemas", json={ @@ -97,7 +108,7 @@ def prepare_issuer(): ).json() schema_guid = schema["guid"] - # prepare issuer + # 4. Create oid4vci issuer credential_issuer = requests.post( f"{AGENT_URL}/oid4vci/issuers", json={ @@ -112,14 +123,13 @@ def prepare_issuer(): global CREDENTIAL_ISSUER CREDENTIAL_ISSUER = f"{AGENT_URL}/oid4vci/issuers/{issuer_id}" - # prepare credential configuration + # 5. Create oid4vci credential configuration from schema cred_config = requests.post( f"{CREDENTIAL_ISSUER}/credential-configurations", json={ "configurationId": CREDENTIAL_CONFIGURATION_ID, "format": "jwt_vc_json", - # TODO: align docker host URL - "schemaId": f"http://localhost:8085/schema-registry/schemas/{schema_guid}/schema", + "schemaId": f"{AGENT_URL}/schema-registry/schemas/{schema_guid}/schema", }, ).json() @@ -137,19 +147,8 @@ def issuer_create_credential_offer(claims): def holder_get_issuer_metadata(credential_issuer: str): - # FIXME: override this just to make url reachable from host - def override_host(url): - return url.replace("http://caddy-issuer:8080/prism-agent", AGENT_URL) - - metadata_url = override_host( - f"{credential_issuer}/.well-known/openid-credential-issuer" - ) + metadata_url = f"{credential_issuer}/.well-known/openid-credential-issuer" response = requests.get(metadata_url).json() - response["credential_endpoint"] = override_host(response["credential_endpoint"]) - response["credential_issuer"] = override_host(response["credential_issuer"]) - response["authorization_servers"] = [ - AUTHORIZATION_SERVER for s in response["authorization_servers"] - ] return response @@ -157,7 +156,6 @@ def holder_get_issuer_as_metadata(authorization_server: str): metadata_url = f"{authorization_server}/.well-known/openid-configuration" response = requests.get(metadata_url) metadata = response.json() - print(f"Metadata: {metadata}") return metadata @@ -293,6 +291,7 @@ def holder_get_credential(credential_endpoint: str, token_response): issuer_as_token_endpoint = issuer_as_metadata["token_endpoint"] issuer_as_authorization_endpoint = issuer_as_metadata["authorization_endpoint"] print("\n::::: Issuer Authorization Server Metadata :::::") + print(json.dumps(issuer_as_metadata, indent=2)) print(f"issuer_as_auth_endpoint: {issuer_as_authorization_endpoint}") print(f"issuer_as_token_endpoint: {issuer_as_token_endpoint}") input("\nEnter to continue ...") diff --git a/examples/st-oid4vci/demo/requirements.txt b/examples/st-oid4vci/demo/requirements.txt new file mode 100644 index 0000000000..791e544886 --- /dev/null +++ b/examples/st-oid4vci/demo/requirements.txt @@ -0,0 +1,11 @@ +certifi==2024.7.4 +cffi==1.16.0 +charset-normalizer==3.3.2 +cryptography==42.0.8 +idna==3.7 +pycparser==2.22 +PyJWT==2.8.0 +requests==2.32.3 +setuptools==70.3.0 +urllib3==2.2.2 +wheel==0.43.0 diff --git a/examples/st-vault/README.md b/examples/st-vault/README.md new file mode 100644 index 0000000000..782bea7136 --- /dev/null +++ b/examples/st-vault/README.md @@ -0,0 +1,10 @@ +## Configuration + +| Exposed Service | Description | +|------------------------------|---------------------------| +| `localhost:8080/cloud-agent` | Single-tenant Cloud Agent | +| `localhost:8200` | Vault | + +__Vault__ + +- Root token `admin` diff --git a/examples/st-vault/compose.yaml b/examples/st-vault/compose.yaml index 911a4a1f42..fdbb1909e7 100644 --- a/examples/st-vault/compose.yaml +++ b/examples/st-vault/compose.yaml @@ -5,7 +5,7 @@ configs: handle_path /didcomm* { reverse_proxy agent-issuer:8090 } - handle_path /prism-agent* { + handle_path /cloud-agent* { reverse_proxy agent-issuer:8085 } handle_path /keycloak* { @@ -39,14 +39,14 @@ services: POLLUX_DB_PASSWORD: postgres POLLUX_DB_PORT: '5432' POLLUX_DB_USER: postgres - POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/prism-agent + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/cloud-agent PRISM_NODE_HOST: node PRISM_NODE_PORT: '50053' - REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent + REST_SERVICE_URL: http://caddy-issuer:8080/cloud-agent SECRET_STORAGE_BACKEND: vault VAULT_ADDR: http://vault-issuer:8200 VAULT_TOKEN: admin - image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.38.0 restart: always caddy-issuer: configs: diff --git a/examples/st/README.md b/examples/st/README.md new file mode 100644 index 0000000000..1575a89961 --- /dev/null +++ b/examples/st/README.md @@ -0,0 +1,5 @@ +## Configuration + +| Exposed Service | Description | +|------------------------------|---------------------------| +| `localhost:8080/cloud-agent` | Single-tenant Cloud Agent | diff --git a/examples/st/compose.yaml b/examples/st/compose.yaml index 167a2a199e..0b2b1dc541 100644 --- a/examples/st/compose.yaml +++ b/examples/st/compose.yaml @@ -5,7 +5,7 @@ configs: handle_path /didcomm* { reverse_proxy agent-issuer:8090 } - handle_path /prism-agent* { + handle_path /cloud-agent* { reverse_proxy agent-issuer:8085 } handle_path /keycloak* { @@ -39,12 +39,12 @@ services: POLLUX_DB_PASSWORD: postgres POLLUX_DB_PORT: '5432' POLLUX_DB_USER: postgres - POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/prism-agent + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/cloud-agent PRISM_NODE_HOST: node PRISM_NODE_PORT: '50053' - REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent + REST_SERVICE_URL: http://caddy-issuer:8080/cloud-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.38.0 restart: always caddy-issuer: configs: diff --git a/infrastructure/charts/agent/Chart.yaml b/infrastructure/charts/agent/Chart.yaml index 15c98c9c8b..0445277f57 100644 --- a/infrastructure/charts/agent/Chart.yaml +++ b/infrastructure/charts/agent/Chart.yaml @@ -13,12 +13,12 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.37.0 +version: 1.38.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: 1.37.0 +appVersion: 1.38.0 dependencies: - name: vault version: 0.24.1 diff --git a/infrastructure/charts/cloud-agent-1.38.0.tgz b/infrastructure/charts/cloud-agent-1.38.0.tgz new file mode 100644 index 0000000000..0135b9a6e5 Binary files /dev/null and b/infrastructure/charts/cloud-agent-1.38.0.tgz differ diff --git a/infrastructure/charts/index.yaml b/infrastructure/charts/index.yaml index 71980545cb..97e5d96623 100644 --- a/infrastructure/charts/index.yaml +++ b/infrastructure/charts/index.yaml @@ -1,9 +1,27 @@ apiVersion: v1 entries: cloud-agent: + - apiVersion: v2 + appVersion: 1.38.0 + created: "2024-07-15T12:29:39.496521751Z" + dependencies: + - name: vault + repository: https://helm.releases.hashicorp.com + version: 0.24.1 + - condition: keycloak.enabled + name: keycloak + repository: https://charts.bitnami.com/bitnami + version: 17.2.0 + description: A Helm chart for deploying cloud-agent + digest: 3b1bd1dd295c3361dba8e0c4ec58994ed76a12bea5484938aa77805192fd452b + name: cloud-agent + type: application + urls: + - https://raw.githubusercontent.com/hyperledger/identus-cloud-agent/main/infrastructure/charts/cloud-agent-1.38.0.tgz + version: 1.38.0 - apiVersion: v2 appVersion: 1.37.0 - created: "2024-07-01T09:13:50.234308304Z" + created: "2024-07-15T12:29:39.487203135Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -21,7 +39,7 @@ entries: version: 1.37.0 - apiVersion: v2 appVersion: 1.36.1 - created: "2024-07-01T09:13:50.223902191Z" + created: "2024-07-15T12:29:39.476683729Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -39,7 +57,7 @@ entries: version: 1.36.1 - apiVersion: v2 appVersion: 1.36.0 - created: "2024-07-01T09:13:50.214857923Z" + created: "2024-07-15T12:29:39.4664606Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -57,7 +75,7 @@ entries: version: 1.36.0 - apiVersion: v2 appVersion: 1.35.1 - created: "2024-07-01T09:13:50.204930147Z" + created: "2024-07-15T12:29:39.456221677Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -75,7 +93,7 @@ entries: version: 1.35.1 - apiVersion: v2 appVersion: 1.35.0 - created: "2024-07-01T09:13:50.195078852Z" + created: "2024-07-15T12:29:39.445760067Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -93,7 +111,7 @@ entries: version: 1.35.0 - apiVersion: v2 appVersion: 1.34.0 - created: "2024-07-01T09:13:50.185271959Z" + created: "2024-07-15T12:29:39.435700056Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -112,7 +130,7 @@ entries: prism-agent: - apiVersion: v2 appVersion: 1.33.1 - created: "2024-07-01T09:13:50.461034123Z" + created: "2024-07-15T12:29:39.731773224Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -130,7 +148,7 @@ entries: version: 1.33.1 - apiVersion: v2 appVersion: 1.33.0 - created: "2024-07-01T09:13:50.450096134Z" + created: "2024-07-15T12:29:39.721534328Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -148,7 +166,7 @@ entries: version: 1.33.0 - apiVersion: v2 appVersion: 1.32.1 - created: "2024-07-01T09:13:50.440440387Z" + created: "2024-07-15T12:29:39.710669925Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -166,7 +184,7 @@ entries: version: 1.32.1 - apiVersion: v2 appVersion: 1.32.0 - created: "2024-07-01T09:13:50.429229489Z" + created: "2024-07-15T12:29:39.700959357Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -184,7 +202,7 @@ entries: version: 1.32.0 - apiVersion: v2 appVersion: 1.31.0 - created: "2024-07-01T09:13:50.420150878Z" + created: "2024-07-15T12:29:39.689846117Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -202,7 +220,7 @@ entries: version: 1.31.0 - apiVersion: v2 appVersion: 1.30.1 - created: "2024-07-01T09:13:50.40938063Z" + created: "2024-07-15T12:29:39.679147948Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -220,7 +238,7 @@ entries: version: 1.30.1 - apiVersion: v2 appVersion: 1.30.0 - created: "2024-07-01T09:13:50.398758055Z" + created: "2024-07-15T12:29:39.669134164Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -238,7 +256,7 @@ entries: version: 1.30.0 - apiVersion: v2 appVersion: 1.29.0 - created: "2024-07-01T09:13:50.388282635Z" + created: "2024-07-15T12:29:39.657444145Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -256,7 +274,7 @@ entries: version: 1.29.0 - apiVersion: v2 appVersion: 1.28.0 - created: "2024-07-01T09:13:50.378802886Z" + created: "2024-07-15T12:29:39.64729637Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -274,7 +292,7 @@ entries: version: 1.28.0 - apiVersion: v2 appVersion: 1.27.0 - created: "2024-07-01T09:13:50.368443349Z" + created: "2024-07-15T12:29:39.636142885Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -292,7 +310,7 @@ entries: version: 1.27.0 - apiVersion: v2 appVersion: 1.26.0 - created: "2024-07-01T09:13:50.358343839Z" + created: "2024-07-15T12:29:39.625580446Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -310,7 +328,7 @@ entries: version: 1.26.0 - apiVersion: v2 appVersion: 1.25.0 - created: "2024-07-01T09:13:50.348875871Z" + created: "2024-07-15T12:29:39.615551643Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -328,7 +346,7 @@ entries: version: 1.25.0 - apiVersion: v2 appVersion: 1.24.0 - created: "2024-07-01T09:13:50.339737128Z" + created: "2024-07-15T12:29:39.604404582Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -346,7 +364,7 @@ entries: version: 1.24.0 - apiVersion: v2 appVersion: 1.23.0 - created: "2024-07-01T09:13:50.328935996Z" + created: "2024-07-15T12:29:39.593553755Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -364,7 +382,7 @@ entries: version: 1.23.0 - apiVersion: v2 appVersion: 1.22.0 - created: "2024-07-01T09:13:50.318312167Z" + created: "2024-07-15T12:29:39.583832686Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -382,7 +400,7 @@ entries: version: 1.22.0 - apiVersion: v2 appVersion: 1.21.1 - created: "2024-07-01T09:13:50.307947402Z" + created: "2024-07-15T12:29:39.573324718Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -400,7 +418,7 @@ entries: version: 1.21.1 - apiVersion: v2 appVersion: 1.21.0 - created: "2024-07-01T09:13:50.298351541Z" + created: "2024-07-15T12:29:39.563170515Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -418,7 +436,7 @@ entries: version: 1.21.0 - apiVersion: v2 appVersion: 1.20.1 - created: "2024-07-01T09:13:50.289340676Z" + created: "2024-07-15T12:29:39.553469417Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -436,7 +454,7 @@ entries: version: 1.20.1 - apiVersion: v2 appVersion: 1.20.0 - created: "2024-07-01T09:13:50.279663615Z" + created: "2024-07-15T12:29:39.543477488Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -454,7 +472,7 @@ entries: version: 1.20.0 - apiVersion: v2 appVersion: 1.19.1 - created: "2024-07-01T09:13:50.269885862Z" + created: "2024-07-15T12:29:39.53342354Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -472,7 +490,7 @@ entries: version: 1.19.1 - apiVersion: v2 appVersion: 1.19.0 - created: "2024-07-01T09:13:50.259598085Z" + created: "2024-07-15T12:29:39.522721417Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -490,7 +508,7 @@ entries: version: 1.19.0 - apiVersion: v2 appVersion: 1.18.0 - created: "2024-07-01T09:13:50.249658531Z" + created: "2024-07-15T12:29:39.512436124Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -504,7 +522,7 @@ entries: version: 1.18.0 - apiVersion: v2 appVersion: 1.17.0 - created: "2024-07-01T09:13:50.246850237Z" + created: "2024-07-15T12:29:39.509538689Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -518,7 +536,7 @@ entries: version: 1.17.0 - apiVersion: v2 appVersion: 1.16.4 - created: "2024-07-01T09:13:50.242960911Z" + created: "2024-07-15T12:29:39.505838077Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -532,7 +550,7 @@ entries: version: 1.16.4 - apiVersion: v2 appVersion: 1.16.3 - created: "2024-07-01T09:13:50.239921005Z" + created: "2024-07-15T12:29:39.502999021Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -546,7 +564,7 @@ entries: version: 1.16.3 - apiVersion: v2 appVersion: 1.16.2 - created: "2024-07-01T09:13:50.237164297Z" + created: "2024-07-15T12:29:39.50018937Z" dependencies: - name: vault repository: https://helm.releases.hashicorp.com @@ -558,4 +576,4 @@ entries: urls: - https://raw.githubusercontent.com/hyperledger/identus-cloud-agent/main/infrastructure/charts/prism-agent-1.16.2.tgz version: 1.16.2 -generated: "2024-07-01T09:13:50.175364732Z" +generated: "2024-07-15T12:29:39.425487877Z" diff --git a/infrastructure/local/.env b/infrastructure/local/.env index d151c7359e..c2fe0a6b2a 100644 --- a/infrastructure/local/.env +++ b/infrastructure/local/.env @@ -1,3 +1,3 @@ -AGENT_VERSION=1.37.0 +AGENT_VERSION=1.38.0 PRISM_NODE_VERSION=2.3.0 VAULT_DEV_ROOT_TOKEN_ID=root diff --git a/mercury/models/src/main/scala/org/hyperledger/identus/mercury/model/error/package.scala b/mercury/models/src/main/scala/org/hyperledger/identus/mercury/model/error/package.scala index 4ea83a1357..9886e80901 100644 --- a/mercury/models/src/main/scala/org/hyperledger/identus/mercury/model/error/package.scala +++ b/mercury/models/src/main/scala/org/hyperledger/identus/mercury/model/error/package.scala @@ -1,6 +1,6 @@ package org.hyperledger.identus.mercury.model -import org.hyperledger.identus.shared.models._ +import org.hyperledger.identus.shared.models.* package object error { diff --git a/package-lock.json b/package-lock.json index b2b54b9e17..03c7c280fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "identus-cloud-agent", - "version": "1.37.0", + "version": "1.38.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "identus-cloud-agent", - "version": "1.37.0", + "version": "1.38.0", "devDependencies": { "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", diff --git a/package.json b/package.json index 84f56ed851..3b9750b32f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "identus-cloud-agent", - "version": "1.37.0", + "version": "1.38.0", "engines": { "node": ">=16.13.0" }, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepository.scala index 789a25473d..fffd057a59 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepository.scala @@ -74,5 +74,5 @@ trait PresentationRepository { def updateAfterFail( recordId: DidCommID, failReason: Option[Failure] - ): URIO[WalletAccessContext, Unit] + ): UIO[Unit] } 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 6038278a1c..7bdc865887 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 @@ -169,6 +169,6 @@ trait PresentationService { def reportProcessingFailure( recordId: DidCommID, failReason: Option[Failure] - ): ZIO[WalletAccessContext, PresentationError, Unit] + ): IO[PresentationError, Unit] } 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 ff245f5aea..d7da9a6594 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 @@ -1096,14 +1096,11 @@ private class PresentationServiceImpl( } yield result } - def reportProcessingFailure( + override def reportProcessingFailure( recordId: DidCommID, failReason: Option[Failure] - ): ZIO[WalletAccessContext, PresentationError, Unit] = - for { - _ <- getRecord(recordId) - result <- presentationRepository.updateAfterFail(recordId, failReason) - } yield result + ): IO[PresentationError, Unit] = + presentationRepository.updateAfterFail(recordId, failReason) private def getRecordFromThreadId( thid: DidCommID 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 c8558054c0..696b4dd5c3 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 @@ -281,7 +281,7 @@ class PresentationServiceNotifier( override def reportProcessingFailure( recordId: DidCommID, failReason: Option[Failure] - ): ZIO[WalletAccessContext, PresentationError, Unit] = svc.reportProcessingFailure(recordId, failReason) + ): IO[PresentationError, Unit] = svc.reportProcessingFailure(recordId, failReason) } object PresentationServiceNotifier { diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala similarity index 100% rename from pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala rename to pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala similarity index 100% rename from pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala rename to pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala similarity index 100% rename from pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala rename to pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositoryInMemory.scala similarity index 87% rename from pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositoryInMemory.scala rename to pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositoryInMemory.scala index 4f646f3bee..01d00ed4ea 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositoryInMemory.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositoryInMemory.scala @@ -30,6 +30,17 @@ class PresentationRepositoryInMemory( }(ZIO.succeed) } yield walletRef + private def anyWalletStoreRefBy( + recordId: DidCommID + ): ZIO[Any, Nothing, Option[Ref[Map[DidCommID, PresentationRecord]]]] = { + for { + refs <- walletRefs.get + // walletsNoRef <- ZIO.foreach(refs)({ case (wID, ref) => ref.get.map(r => (wID, r)) }) + tmp <- ZIO.foreach(refs)({ case (wID, ref) => ref.get.map(r => (ref, r)) }) + walletRef = tmp.find(e => e._2.keySet.contains(recordId)).map(_._1) + } yield walletRef + } + override def createPresentationRecord(record: PresentationRecord): URIO[WalletAccessContext, Unit] = { val result = for { @@ -324,35 +335,40 @@ class PresentationRepositoryInMemory( result.ensureOneAffectedRowOrDie } + // def updateAfterFailX( + // recordId: DidCommID, + // failReason: Option[Failure] + // ): URIO[WalletAccessContext, Unit] = { override def updateAfterFail( recordId: DidCommID, failReason: Option[Failure] - ): URIO[WalletAccessContext, Unit] = { - val result = - for { - storeRef <- walletStoreRef - maybeRecord <- findPresentationRecord(recordId) - count <- maybeRecord - .map(record => - for { - _ <- storeRef.update(r => - r.updated( - recordId, - record.copy( - metaRetries = math.max(0, record.metaRetries - 1), - metaNextRetry = - if (record.metaRetries - 1 <= 0) None - else Some(Instant.now().plusSeconds(60)), // TODO exponention time - metaLastFailure = failReason + ): UIO[Unit] = + anyWalletStoreRefBy(recordId).flatMap { mStoreRef => + mStoreRef match + case None => ZIO.succeed(0) + case Some(storeRef) => + for { + maybeRecord <- storeRef.get.map(store => store.get(recordId)) + count <- maybeRecord + .map(record => + for { + _ <- storeRef.update(r => + r.updated( + recordId, + record.copy( + metaRetries = math.max(0, record.metaRetries - 1), + metaNextRetry = + if (record.metaRetries - 1 <= 0) None + else Some(Instant.now().plusSeconds(60)), // TODO exponention time + metaLastFailure = failReason + ) + ) ) - ) + } yield 1 ) - } yield 1 - ) - .getOrElse(ZIO.succeed(0)) - } yield count - result.ensureOneAffectedRowOrDie - } + .getOrElse(ZIO.succeed(0)) + } yield count + }.ensureOneAffectedRowOrDie } object PresentationRepositoryInMemory { 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 2cab203276..554d6c4852 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 @@ -548,7 +548,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ .ensureOneAffectedRowOrDie } - def updateAfterFail( + override def updateAfterFail( recordId: DidCommID, failReason: Option[Failure] ): URIO[WalletAccessContext, Unit] = { 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 5d98bd1f23..57a22d2ee8 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 @@ -489,10 +489,10 @@ class JdbcPresentationRepository( .ensureOneAffectedRowOrDie } - def updateAfterFail( + override def updateAfterFail( recordId: DidCommID, failReason: Option[Failure] - ): URIO[WalletAccessContext, Unit] = { + ): UIO[Unit] = { val cxnIO = sql""" | UPDATE public.presentation_records | SET @@ -503,7 +503,7 @@ class JdbcPresentationRepository( | id = $recordId """.stripMargin.update cxnIO.run - .transactWallet(xa) + .transact(xb) .orDie .ensureOneAffectedRowOrDie } diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala index 0ee6ffebbf..8bf6ab2086 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala @@ -2,7 +2,6 @@ package org.hyperledger.identus.pollux.vc.jwt import cats.implicits.* import com.nimbusds.jose.{JWSAlgorithm, JWSHeader, JWSObject, Payload} -import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton import com.nimbusds.jose.crypto.ECDSASigner import com.nimbusds.jwt.SignedJWT import io.circe.* @@ -104,7 +103,6 @@ object EcdsaSecp256k1Signature2019ProofGenerator { } object EddsaJcs2022ProofGenerator { - private val provider = BouncyCastleProviderSingleton.getInstance private val ed25519MultiBaseHeader: Array[Byte] = Array(-19, 1) // 0xed01 private def pkToMultiKey(pk: Ed25519PublicKey): MultiKey = { diff --git a/project/build.properties b/project/build.properties index 081fdbbc76..ee4c672cd0 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version=1.10.1 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 7a7c800ac9..c2ea5dc53f 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 @@ -1,7 +1,7 @@ package org.hyperledger.identus.shared.models import zio.{URIO, ZIO} -import zio.json._ +import zio.json.* trait Failure { def namespace: String diff --git a/tests/integration-tests/build.gradle.kts b/tests/integration-tests/build.gradle.kts index 00ba4ab3cd..1f56e07876 100644 --- a/tests/integration-tests/build.gradle.kts +++ b/tests/integration-tests/build.gradle.kts @@ -45,6 +45,10 @@ dependencies { testImplementation("com.nimbusds:nimbus-jose-jwt:9.40") testImplementation("org.bouncycastle:bcprov-jdk18on:1.78.1") testImplementation("com.google.crypto.tink:tink:1.13.0") + 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") } serenity { @@ -61,6 +65,12 @@ tasks.test { finalizedBy("aggregate", "reports") testLogging.showStandardStreams = true systemProperty("cucumber.filter.tags", System.getProperty("cucumber.filter.tags")) + // Since the test runs on host and system-unter-test runs in containers, + // We need to make the test on host resolves host.docker.internal same as the containerized services, + // because some spec (e.g. OID4VCI) requires domain to be the same. + // + // The OID4VCI library does not allow mixing host.docker.internal and localhost + systemProperty("jdk.net.hosts.file", "hosts_test") } kotlin { @@ -91,6 +101,7 @@ afterEvaluate { systemProperty("PRISM_NODE_VERSION", System.getenv("PRISM_NODE_VERSION") ?: "") systemProperty("AGENT_VERSION", System.getenv("AGENT_VERSION") ?: "") systemProperty("cucumber.filter.tags", System.getProperty("cucumber.filter.tags")) + systemProperty("jdk.net.hosts.file", "hosts_test") finalizedBy("aggregate", "reports") outputs.upToDateWhen { false } } diff --git a/tests/integration-tests/hosts_test b/tests/integration-tests/hosts_test new file mode 100644 index 0000000000..26590d4c39 --- /dev/null +++ b/tests/integration-tests/hosts_test @@ -0,0 +1,2 @@ +127.0.0.1 localhost +127.0.0.1 host.docker.internal \ No newline at end of file diff --git a/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt b/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt index 43c92230ed..4ea1481a90 100644 --- a/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt +++ b/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt @@ -14,6 +14,9 @@ import models.* 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 java.net.URL import java.time.OffsetDateTime @@ -29,6 +32,7 @@ open class ListenToEvents( var credentialEvents: MutableList = mutableListOf() var presentationEvents: MutableList = mutableListOf() var didEvents: MutableList = mutableListOf() + var authCodeCallbackEvents: MutableList> = mutableListOf() private fun route(application: Application) { application.routing { @@ -46,6 +50,12 @@ open class ListenToEvents( } call.respond(HttpStatusCode.OK) } + get("/auth-cb") { + val authCode = call.parameters["code"]!! + val state = call.parameters["state"]!! + authCodeCallbackEvents.add(Pair(authCode, state)) + call.respond(HttpStatusCode.OK, "Login Successfully") + } } } @@ -57,6 +67,42 @@ open class ListenToEvents( fun with(actor: Actor): ListenToEvents { return actor.abilityTo(ListenToEvents::class.java) } + + fun presentationProofStatus(actor: Actor): Question { + return Question.about("presentation status").answeredBy { + val proofEvent = with(actor).presentationEvents.lastOrNull { + it.data.thid == actor.recall("thid") + } + proofEvent?.data?.status + } + } + + fun connectionState(actor: Actor): Question { + return Question.about("connection state").answeredBy { + val lastEvent = with(actor).connectionEvents.lastOrNull { + it.data.thid == actor.recall("connection").thid + } + lastEvent?.data?.state + } + } + + fun credentialState(actor: Actor): Question { + return Question.about("credential state").answeredBy { + val credentialEvent = ListenToEvents.with(actor).credentialEvents.lastOrNull { + it.data.thid == actor.recall("thid") + } + credentialEvent?.data?.protocolState + } + } + + fun didStatus(actor: Actor): Question { + return Question.about("did status").answeredBy { + val didEvent = ListenToEvents.with(actor).didEvents.lastOrNull { + it.data.did == actor.recall("shortFormDid") + } + didEvent?.data?.status + } + } } init { diff --git a/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt b/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt index dee1243f98..7dcc6ec6e3 100644 --- a/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt +++ b/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt @@ -3,23 +3,30 @@ package common import org.hyperledger.identus.client.models.* enum class DidPurpose { - EMPTY { - override val publicKeys = emptyList() - override val services = emptyList() + CUSTOM { + override val publicKeys = mutableListOf() + override val services = mutableListOf() + }, + SD_JWT { + override val publicKeys = mutableListOf( + ManagedDIDKeyTemplate("auth-1", Purpose.AUTHENTICATION, Curve.ED25519), + ManagedDIDKeyTemplate("assertion-1", Purpose.ASSERTION_METHOD, Curve.ED25519), + ) + override val services = mutableListOf() }, JWT { - override val publicKeys = listOf( + override val publicKeys = mutableListOf( ManagedDIDKeyTemplate("auth-1", Purpose.AUTHENTICATION, Curve.SECP256K1), ManagedDIDKeyTemplate("auth-2", Purpose.AUTHENTICATION, Curve.ED25519), ManagedDIDKeyTemplate("assertion-1", Purpose.ASSERTION_METHOD, Curve.SECP256K1), ) - override val services = emptyList() + override val services = mutableListOf() }, ANONCRED { - override val publicKeys = emptyList() - override val services = emptyList() + override val publicKeys = mutableListOf() + override val services = mutableListOf() }, ; - abstract val publicKeys: List - abstract val services: List + abstract val publicKeys: MutableList + abstract val services: MutableList } 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 fbe8c94602..d73a8814b5 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt @@ -15,17 +15,20 @@ import java.io.File data class Keycloak( @ConfigAlias("http_port") val httpPort: Int, val realm: String = "atala-demo", - @ConfigAlias("client_id") val clientId: String = "prism-agent", - @ConfigAlias("client_secret") val clientSecret: String = "prism-agent-demo-secret", + @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(), + @ConfigAlias("extra_clients") val extraClients: Map = emptyMap(), + @ConfigAlias("extra_scopes") val extraScopes: List = emptyList(), ) : ServiceBase() { private val logger = Logger.get() private val keycloakEnvConfig: Map = extraEnvs + mapOf( "KEYCLOAK_HTTP_PORT" to httpPort.toString(), ) - override val logServices: List = listOf("keycloak") + override val logServices: List = listOf(loggerName) private val keycloakClientRoles: List = AgentRole.entries.map { it.roleName } override val container: ComposeContainer = ComposeContainer(File(keycloakComposeFile)).withEnv(keycloakEnvConfig) @@ -43,8 +46,10 @@ data class Keycloak( logger.info("Setting up Keycloak") initRequestBuilder() createRealm() - createClient() + createAgentClient() + createPublicClients() createClientRoles() + createScopes() createUsers(users) } @@ -93,7 +98,7 @@ data class Keycloak( .then().statusCode(HttpStatus.SC_CREATED) } - private fun createClient() { + private fun createAgentClient() { RestAssured.given().spec(requestBuilder) .body( mapOf( @@ -108,6 +113,55 @@ data class Keycloak( .then().statusCode(HttpStatus.SC_CREATED) } + private fun createPublicClients() { + extraClients.forEach { client -> + RestAssured.given().spec(requestBuilder) + .body( + mapOf( + "id" to client.key, + "publicClient" to true, + "consentRequired" to true, + "redirectUris" to client.value.redirectUris, + ), + ) + .post("/admin/realms/$realm/clients") + .then().statusCode(HttpStatus.SC_CREATED) + } + } + + private fun createScopes() { + extraScopes.forEach { scope -> + val response = RestAssured.given().spec(requestBuilder) + .body( + mapOf( + "name" to scope, + "description" to scope, + "protocol" to "openid-connect", + "attributes" to mapOf( + "consent.screen.text" to scope, + "display.on.consent.screen" to true, + "include.in.token.scope" to true, + "gui.order" to "", + ), + ), + ) + .post("/admin/realms/$realm/client-scopes") + .thenReturn() + response.then().statusCode(HttpStatus.SC_CREATED) + val clientScopeId = response.getHeader("Location").split("/").last() + mapClientsScopeToClient(clientScopeId) + } + } + + private fun mapClientsScopeToClient(clientScopeId: String) { + extraClients.keys.forEach { client -> + RestAssured.given().spec(requestBuilder) + .put("/admin/realms/$realm/clients/$client/optional-client-scopes/$clientScopeId") + .then() + .statusCode(HttpStatus.SC_NO_CONTENT) + } + } + private fun createClientRoles() { keycloakClientRoles.forEach { roleName -> RestAssured.given().spec(requestBuilder) @@ -171,3 +225,5 @@ data class Keycloak( .then().statusCode(HttpStatus.SC_NO_CONTENT) } } + +data class KeycloakPublicClientConfig(val redirectUris: List = listOf()) diff --git a/tests/integration-tests/src/test/kotlin/eu/europa/ec/eudi/openid4vci/Types.kt b/tests/integration-tests/src/test/kotlin/eu/europa/ec/eudi/openid4vci/Types.kt new file mode 100644 index 0000000000..bae29d46a2 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/eu/europa/ec/eudi/openid4vci/Types.kt @@ -0,0 +1,27 @@ +package eu.europa.ec.eudi.openid4vci + +import java.net.URI +import java.net.URL + +/** + * https://github.com/eu-digital-identity-wallet/eudi-lib-jvm-openid4vci-kt/blob/e81802f3b90639b97e32a6fd1c06c20e5ff53f27/src/main/kotlin/eu/europa/ec/eudi/openid4vci/Types.kt#L45 + * + * This overrides the implementation in EUDI to relax the HTTPS requirement making it easier for testing purpose + */ +@JvmInline +value class HttpsUrl private constructor(val value: URL) { + + override fun toString(): String = value.toString() + + companion object { + + /** + * Parses the provided [value] as a [URI] and tries creates a new [HttpsUrl]. + */ + operator fun invoke(value: String): Result = runCatching { + val uri = URI.create(value) + // require(uri.scheme.contentEquals("https", true)) { "URL must use https protocol" } + HttpsUrl(uri.toURL()) + } + } +} diff --git a/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt b/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt index e4395e27e0..f1d3e71695 100644 --- a/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt +++ b/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt @@ -16,7 +16,6 @@ import java.io.Serializable import java.security.Provider import java.security.SecureRandom import java.time.OffsetDateTime -import java.util.Base64 import java.util.Date import kotlin.reflect.KClass @@ -146,7 +145,7 @@ class JwtCredential { } fun parseBase64(base64: String): JwtCredential { - val jwt = String(Base64.getDecoder().decode(base64)) + val jwt = Base64URL.from(base64).decodeToString() return parseJwt(jwt) } diff --git a/tests/integration-tests/src/test/kotlin/models/SdJwtClaim.kt b/tests/integration-tests/src/test/kotlin/models/SdJwtClaim.kt new file mode 100644 index 0000000000..0ce288c800 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/models/SdJwtClaim.kt @@ -0,0 +1,7 @@ +package models + +data class SdJwtClaim( + val salt: String, + val key: String, + val value: String, +) diff --git a/tests/integration-tests/src/test/kotlin/steps/Setup.kt b/tests/integration-tests/src/test/kotlin/steps/Setup.kt index 37701683fd..07038f919d 100644 --- a/tests/integration-tests/src/test/kotlin/steps/Setup.kt +++ b/tests/integration-tests/src/test/kotlin/steps/Setup.kt @@ -1,12 +1,14 @@ package steps import abilities.ListenToEvents +import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton import com.sksamuel.hoplite.ConfigException import com.sksamuel.hoplite.ConfigLoader import common.TestConstants import config.* import io.cucumber.java.AfterAll import io.cucumber.java.BeforeAll +import io.ktor.server.util.url import io.restassured.RestAssured import io.restassured.builder.RequestSpecBuilder import net.serenitybdd.screenplay.Actor @@ -16,6 +18,7 @@ import net.serenitybdd.screenplay.rest.abilities.CallAnApi import org.apache.http.HttpStatus import org.hyperledger.identus.client.models.CreateWalletRequest import org.hyperledger.identus.client.models.CreateWebhookNotification +import java.security.Security import java.util.UUID object Setup { @@ -123,6 +126,7 @@ object Setup { } if (role.webhook != null) { actor.whoCan(ListenToEvents.at(role.webhook.url, role.webhook.localPort)) + actor.remember("webhookUrl", role.webhook.url) if (role.webhook.initRequired) { registerWebhook(actor, role.webhook.url.toExternalForm()) } @@ -130,12 +134,18 @@ object Setup { actor.remember("baseUrl", role.url.toExternalForm()) } if (config.services?.keycloakOid4vci != null) { - val role = config.roles.find { it.name == "Issuer" } ?: throw ConfigException("Issuer role does not exist") - val url = role.oid4vciAuthServer ?: throw ConfigException("Issuer's oid4vci_auth_server must be provided") - val actor = cast.actorNamed(role.name) - actor.remember("OID4VCI_AUTH_SERVER_URL", url.toExternalForm()) - actor.remember("OID4VCI_AUTH_SERVER_CLIENT_ID", config.services.keycloakOid4vci.clientId) - actor.remember("OID4VCI_AUTH_SERVER_CLIENT_SECRET", config.services.keycloakOid4vci.clientSecret) + val issuerRole = config.roles.find { it.name == "Issuer" } ?: throw ConfigException("Issuer role does not exist") + val issuerActor = cast.actorNamed(issuerRole.name) + with(issuerActor) { + val url = issuerRole.oid4vciAuthServer ?: throw ConfigException("Issuer's oid4vci_auth_server must be provided") + remember("OID4VCI_AUTH_SERVER_URL", url.toExternalForm()) + remember("OID4VCI_AUTH_SERVER_CLIENT_ID", config.services.keycloakOid4vci.clientId) + remember("OID4VCI_AUTH_SERVER_CLIENT_SECRET", config.services.keycloakOid4vci.clientSecret) + } + + val holderRole = config.roles.find { it.name == "Holder" } ?: throw ConfigException("Holder role does not exist") + val holderActor = cast.actorNamed(holderRole.name) + holderActor.remember("OID4VCI_AUTH_SERVER_CLIENT_ID", "holder") } OnStage.setTheStage(cast) } @@ -189,6 +199,7 @@ object Setup { @BeforeAll fun init() { + Security.insertProviderAt(BouncyCastleProviderSingleton.getInstance(), 2) Setup.initServices() Setup.initActors() } diff --git a/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt b/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt index bc885b15ee..d5c8428b13 100644 --- a/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt @@ -12,29 +12,31 @@ import org.apache.http.HttpStatus import org.hyperledger.identus.client.models.Connection import org.hyperledger.identus.client.models.ConnectionsPage import steps.connection.ConnectionSteps -import steps.credentials.IssueCredentialsSteps +import steps.credentials.* import steps.did.PublishDidSteps import steps.schemas.CredentialSchemasSteps class CommonSteps { @Given("{actor} has a jwt issued credential from {actor}") - fun holderHasIssuedCredentialFromIssuer(holder: Actor, issuer: Actor) { + fun holderHasIssuedJwtCredentialFromIssuer(holder: Actor, issuer: Actor) { actorsHaveExistingConnection(issuer, holder) val publishDidSteps = PublishDidSteps() publishDidSteps.agentHasAnUnpublishedDID(holder, DidPurpose.JWT) publishDidSteps.agentHasAPublishedDID(issuer, DidPurpose.JWT) - val issueSteps = IssueCredentialsSteps() - issueSteps.issuerOffersACredential(issuer, holder, "short") - issueSteps.holderReceivesCredentialOffer(holder) - issueSteps.holderAcceptsCredentialOfferForJwt(holder) - issueSteps.acmeIssuesTheCredential(issuer) - issueSteps.bobHasTheCredentialIssued(holder) + val jwtCredentialSteps = JwtCredentialSteps() + val credentialSteps = CredentialSteps() + + jwtCredentialSteps.issuerOffersAJwtCredential(issuer, holder, "short") + credentialSteps.holderReceivesCredentialOffer(holder) + jwtCredentialSteps.holderAcceptsJwtCredentialOfferForJwt(holder) + credentialSteps.issuerIssuesTheCredential(issuer) + credentialSteps.holderReceivesTheIssuedCredential(holder) } @Given("{actor} has a jwt issued credential with {} schema from {actor}") - fun holderHasIssuedCredentialFromIssuerWithSchema( + fun holderHasIssuedJwtCredentialFromIssuerWithSchema( holder: Actor, schema: CredentialSchema, issuer: Actor, @@ -48,12 +50,47 @@ class CommonSteps { val schemaSteps = CredentialSchemasSteps() schemaSteps.agentHasAPublishedSchema(issuer, schema) - val issueSteps = IssueCredentialsSteps() - issueSteps.issuerOffersCredentialToHolderUsingSchema(issuer, holder, "short", schema) - issueSteps.holderReceivesCredentialOffer(holder) - issueSteps.holderAcceptsCredentialOfferForJwt(holder) - issueSteps.acmeIssuesTheCredential(issuer) - issueSteps.bobHasTheCredentialIssued(holder) + val jwtCredentialSteps = JwtCredentialSteps() + val credentialSteps = CredentialSteps() + jwtCredentialSteps.issuerOffersJwtCredentialToHolderUsingSchema(issuer, holder, "short", schema) + credentialSteps.holderReceivesCredentialOffer(holder) + jwtCredentialSteps.holderAcceptsJwtCredentialOfferForJwt(holder) + credentialSteps.issuerIssuesTheCredential(issuer) + credentialSteps.holderReceivesTheIssuedCredential(holder) + } + + @Given("{actor} has a sd-jwt issued credential from {actor}") + fun holderHasIssuedSdJwtCredentialFromIssuer(holder: Actor, issuer: Actor) { + actorsHaveExistingConnection(issuer, holder) + + val publishDidSteps = PublishDidSteps() + publishDidSteps.agentHasAnUnpublishedDID(holder, DidPurpose.SD_JWT) + publishDidSteps.agentHasAPublishedDID(issuer, DidPurpose.SD_JWT) + + val sdJwtCredentialSteps = SdJwtCredentialSteps() + val credentialSteps = CredentialSteps() + sdJwtCredentialSteps.issuerOffersSdJwtCredentialToHolder(issuer, holder) + credentialSteps.holderReceivesCredentialOffer(holder) + sdJwtCredentialSteps.holderAcceptsSdJwtCredentialOffer(holder) + credentialSteps.issuerIssuesTheCredential(issuer) + credentialSteps.holderReceivesTheIssuedCredential(holder) + } + + @Given("{actor} has a bound sd-jwt issued credential from {actor}") + fun holderHasIssuedSdJwtCredentialFromIssuerWithKeyBind(holder: Actor, issuer: Actor) { + actorsHaveExistingConnection(issuer, holder) + + val publishDidSteps = PublishDidSteps() + publishDidSteps.agentHasAnUnpublishedDID(holder, DidPurpose.SD_JWT) + publishDidSteps.agentHasAPublishedDID(issuer, DidPurpose.SD_JWT) + + val sdJwtCredentialSteps = SdJwtCredentialSteps() + val credentialSteps = CredentialSteps() + sdJwtCredentialSteps.issuerOffersSdJwtCredentialToHolder(issuer, holder) + credentialSteps.holderReceivesCredentialOffer(holder) + sdJwtCredentialSteps.holderAcceptsSdJwtCredentialOfferWithKeyBinding(holder, "auth-1") + credentialSteps.issuerIssuesTheCredential(issuer) + credentialSteps.holderReceivesTheIssuedCredential(holder) } @Given("{actor} and {actor} have an existing connection") diff --git a/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt b/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt index 886bb64b86..17df7e4bf3 100644 --- a/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt @@ -8,13 +8,16 @@ 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 io.iohk.atala.automation.utils.Wait +import io.iohk.atala.automation.serenity.interactions.PollingWait import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus.SC_CREATED import org.apache.http.HttpStatus.SC_OK import org.assertj.core.api.Assertions.assertThat +import org.hamcrest.CoreMatchers import org.hyperledger.identus.client.models.* +import org.hyperledger.identus.client.models.Connection.State.CONNECTION_RESPONSE_RECEIVED +import org.hyperledger.identus.client.models.Connection.State.CONNECTION_RESPONSE_SENT class ConnectionSteps { @@ -73,27 +76,22 @@ class ConnectionSteps { @When("{actor} receives the connection request and sends back the response") fun inviterReceivesTheConnectionRequest(inviter: Actor) { - Wait.until( - errorMessage = "Inviter connection didn't reach ${Connection.State.CONNECTION_RESPONSE_SENT} state", - ) { - val lastEvent = ListenToEvents.with(inviter).connectionEvents.lastOrNull { - it.data.thid == inviter.recall("connection").thid - } - lastEvent != null && lastEvent.data.state == Connection.State.CONNECTION_RESPONSE_SENT - } + inviter.attemptsTo( + PollingWait.until( + ListenToEvents.connectionState(inviter), + CoreMatchers.equalTo(CONNECTION_RESPONSE_SENT), + ), + ) } @When("{actor} receives the connection response") fun inviteeReceivesTheConnectionResponse(invitee: Actor) { - Wait.until( - errorMessage = "Invitee connection didn't reach ${Connection.State.CONNECTION_RESPONSE_RECEIVED} state.", - ) { - val lastEvent = ListenToEvents.with(invitee).connectionEvents.lastOrNull { - it.data.thid == invitee.recall("connection").thid - } - lastEvent != null && - lastEvent.data.state == Connection.State.CONNECTION_RESPONSE_RECEIVED - } + invitee.attemptsTo( + PollingWait.until( + ListenToEvents.connectionState(invitee), + CoreMatchers.equalTo(CONNECTION_RESPONSE_RECEIVED), + ), + ) } @Then("{actor} and {actor} have a connection") @@ -120,8 +118,8 @@ class ConnectionSteps { assertThat(inviter.recall("connection-with-${invitee.name}").theirDid) .isEqualTo(invitee.recall("connection-with-${inviter.name}").myDid) assertThat(inviter.recall("connection-with-${invitee.name}").state) - .isEqualTo(Connection.State.CONNECTION_RESPONSE_SENT) + .isEqualTo(CONNECTION_RESPONSE_SENT) assertThat(invitee.recall("connection-with-${inviter.name}").state) - .isEqualTo(Connection.State.CONNECTION_RESPONSE_RECEIVED) + .isEqualTo(CONNECTION_RESPONSE_RECEIVED) } } diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/AnoncredSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/AnoncredSteps.kt new file mode 100644 index 0000000000..2cddddb2b6 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/AnoncredSteps.kt @@ -0,0 +1,50 @@ +package steps.credentials + +import interactions.Post +import interactions.body +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.SC_CREATED +import org.apache.http.HttpStatus.SC_OK +import org.hyperledger.identus.client.models.* + +class AnoncredSteps { + + @When("{actor} accepts anoncred credential offer") + fun holderAcceptsCredentialOfferForAnoncred(holder: Actor) { + val recordId = holder.recall("recordId") + holder.attemptsTo( + Post.to("/issue-credentials/records/$recordId/accept-offer").body("{}"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @When("{actor} offers anoncred to {actor}") + fun acmeOffersAnoncredToBob(issuer: Actor, holder: Actor) { + val credentialOfferRequest = CreateIssueCredentialRecordRequest( + credentialDefinitionId = issuer.recall("anoncredsCredentialDefinition").guid, + claims = linkedMapOf( + "name" to "Bob", + "age" to "21", + "sex" to "M", + ), + issuingDID = issuer.recall("shortFormDid"), + connectionId = issuer.recall("connection-with-${holder.name}").connectionId, + validityPeriod = 3600.0, + credentialFormat = "AnonCreds", + automaticIssuance = false, + ) + + issuer.attemptsTo( + Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + + val credentialRecord = SerenityRest.lastResponse().get() + issuer.remember("thid", credentialRecord.thid) + holder.remember("thid", credentialRecord.thid) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/CredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/CredentialSteps.kt new file mode 100644 index 0000000000..7c2251ec83 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/CredentialSteps.kt @@ -0,0 +1,79 @@ +package steps.credentials + +import abilities.ListenToEvents +import interactions.Post +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import io.iohk.atala.automation.serenity.ensure.Ensure +import io.iohk.atala.automation.serenity.interactions.PollingWait +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus.* +import org.hamcrest.CoreMatchers.equalTo +import org.hyperledger.identus.client.models.* +import org.hyperledger.identus.client.models.IssueCredentialRecord.ProtocolState.* + +class CredentialSteps { + + @When("{actor} receives the credential offer") + fun holderReceivesCredentialOffer(holder: Actor) { + holder.attemptsTo( + PollingWait.until( + ListenToEvents.credentialState(holder), + equalTo(OFFER_RECEIVED), + ), + ) + val recordId = ListenToEvents.with(holder).credentialEvents.last().data.recordId + holder.remember("recordId", recordId) + } + + @When("{actor} tries to issue the credential") + fun issuerTriesToIssueTheCredential(issuer: Actor) { + issuer.attemptsTo( + PollingWait.until( + ListenToEvents.credentialState(issuer), + equalTo(REQUEST_RECEIVED), + ), + ) + + val recordId = ListenToEvents.with(issuer).credentialEvents.last().data.recordId + + issuer.attemptsTo( + Post.to("/issue-credentials/records/$recordId/issue-credential"), + ) + } + + @When("{actor} issues the credential") + fun issuerIssuesTheCredential(issuer: Actor) { + issuerTriesToIssueTheCredential(issuer) + + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + + issuer.attemptsTo( + PollingWait.until( + ListenToEvents.credentialState(issuer), + equalTo(CREDENTIAL_SENT), + ), + ) + issuer.remember("issuedCredential", ListenToEvents.with(issuer).credentialEvents.last().data) + } + + @Then("{actor} receives the issued credential") + fun holderReceivesTheIssuedCredential(holder: Actor) { + holder.attemptsTo( + PollingWait.until( + ListenToEvents.credentialState(holder), + equalTo(CREDENTIAL_RECEIVED), + ), + ) + holder.remember("issuedCredential", ListenToEvents.with(holder).credentialEvents.last().data) + } + + @Then("{actor} should see that credential issuance has failed") + fun issuerShouldSeeThatCredentialIssuanceHasFailed(issuer: Actor) { + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_UNPROCESSABLE_ENTITY), + ) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt deleted file mode 100644 index 1688e3a585..0000000000 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt +++ /dev/null @@ -1,242 +0,0 @@ -package steps.credentials - -import abilities.ListenToEvents -import common.CredentialSchema -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 -import io.iohk.atala.automation.serenity.ensure.Ensure -import io.iohk.atala.automation.utils.Wait -import models.CredentialEvent -import net.serenitybdd.rest.SerenityRest -import net.serenitybdd.screenplay.Actor -import org.apache.http.HttpStatus.* -import org.hyperledger.identus.client.models.* -import kotlin.time.Duration.Companion.seconds - -class IssueCredentialsSteps { - - private var credentialEvent: CredentialEvent? = null - - private fun sendCredentialOffer( - issuer: Actor, - holder: Actor, - didForm: String, - schemaGuid: String?, - claims: Map, - ) { - val did: String = if (didForm == "short") { - issuer.recall("shortFormDid") - } else { - issuer.recall("longFormDid") - } - - val schemaId: String? = if (schemaGuid != null) { - val baseUrl = issuer.recall("baseUrl") - "$baseUrl/schema-registry/schemas/$schemaGuid" - } else { - null - } - - val credentialOfferRequest = CreateIssueCredentialRecordRequest( - schemaId = schemaId, - claims = claims, - issuingDID = did, - connectionId = issuer.recall("connection-with-${holder.name}").connectionId, - validityPeriod = 3600.0, - credentialFormat = "JWT", - automaticIssuance = false, - ) - - issuer.attemptsTo( - Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), - ) - } - - private fun saveCredentialOffer(issuer: Actor, holder: Actor) { - val credentialRecord = SerenityRest.lastResponse().get() - - issuer.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), - ) - - issuer.remember("thid", credentialRecord.thid) - holder.remember("thid", credentialRecord.thid) - } - - @When("{actor} offers a credential to {actor} with {string} form DID") - fun issuerOffersACredential(issuer: Actor, holder: Actor, format: String) { - val claims = linkedMapOf( - "firstName" to "FirstName", - "lastName" to "LastName", - ) - sendCredentialOffer(issuer, holder, format, null, claims) - saveCredentialOffer(issuer, holder) - } - - @When("{actor} offers a credential to {actor} with {} form using {} schema") - fun issuerOffersCredentialToHolderUsingSchema( - issuer: Actor, - holder: Actor, - format: String, - schema: CredentialSchema, - ) { - val schemaGuid = issuer.recall(schema.name) - val claims = schema.claims - sendCredentialOffer(issuer, holder, format, schemaGuid, claims) - saveCredentialOffer(issuer, holder) - } - - @When("{actor} offers a credential to {actor} with {} form DID with wrong claims structure using {} schema") - fun issuerOffersCredentialToHolderWithWrongClaimStructure( - issuer: Actor, - holder: Actor, - format: String, - schema: CredentialSchema, - ) { - val schemaGuid = issuer.recall(schema.name)!! - val claims = linkedMapOf( - "name" to "Name", - "surname" to "Surname", - ) - sendCredentialOffer(issuer, holder, format, schemaGuid, claims) - } - - @When("{actor} offers anoncred to {actor}") - fun acmeOffersAnoncredToBob(issuer: Actor, holder: Actor) { - val credentialOfferRequest = CreateIssueCredentialRecordRequest( - credentialDefinitionId = issuer.recall("anoncredsCredentialDefinition").guid, - claims = linkedMapOf( - "name" to "Bob", - "age" to "21", - "sex" to "M", - ), - issuingDID = issuer.recall("shortFormDid"), - connectionId = issuer.recall("connection-with-${holder.name}").connectionId, - validityPeriod = 3600.0, - credentialFormat = "AnonCreds", - automaticIssuance = false, - ) - - issuer.attemptsTo( - Post.to("/issue-credentials/credential-offers") - .with { - it.body(credentialOfferRequest) - }, - ) - - val credentialRecord = SerenityRest.lastResponse().get() - - issuer.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), - ) - - issuer.remember("thid", credentialRecord.thid) - holder.remember("thid", credentialRecord.thid) - } - - @When("{actor} receives the credential offer") - fun holderReceivesCredentialOffer(holder: Actor) { - Wait.until( - errorMessage = "Holder was unable to receive the credential offer from Issuer! " + - "Protocol state did not achieve ${IssueCredentialRecord.ProtocolState.OFFER_RECEIVED} state.", - ) { - credentialEvent = ListenToEvents.with(holder).credentialEvents.lastOrNull { - it.data.thid == holder.recall("thid") - } - credentialEvent != null && - credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.OFFER_RECEIVED - } - - val recordId = ListenToEvents.with(holder).credentialEvents.last().data.recordId - holder.remember("recordId", recordId) - } - - @When("{actor} accepts credential offer for JWT") - fun holderAcceptsCredentialOfferForJwt(holder: Actor) { - holder.attemptsTo( - Post.to("/issue-credentials/records/${holder.recall("recordId")}/accept-offer") - .with { - it.body( - AcceptCredentialOfferRequest(holder.recall("longFormDid")), - ) - }, - ) - holder.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), - ) - } - - @When("{actor} accepts credential offer for anoncred") - fun holderAcceptsCredentialOfferForAnoncred(holder: Actor) { - holder.attemptsTo( - Post.to("/issue-credentials/records/${holder.recall("recordId")}/accept-offer") - .with { - it.body( - "{}", - ) - }, - ) - holder.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), - ) - } - - @When("{actor} issues the credential") - fun acmeIssuesTheCredential(issuer: Actor) { - Wait.until( - errorMessage = "Issuer was unable to receive the credential request from Holder! Protocol state did not achieve RequestReceived state.", - ) { - credentialEvent = ListenToEvents.with(issuer).credentialEvents.lastOrNull { - it.data.thid == issuer.recall("thid") - } - credentialEvent != null && - credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.REQUEST_RECEIVED - } - val recordId = credentialEvent!!.data.recordId - issuer.attemptsTo( - Post.to("/issue-credentials/records/$recordId/issue-credential"), - ) - issuer.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), - ) - - Wait.until( - 10.seconds, - errorMessage = "Issuer was unable to issue the credential! " + - "Protocol state did not achieve ${IssueCredentialRecord.ProtocolState.CREDENTIAL_SENT} state.", - ) { - credentialEvent = ListenToEvents.with(issuer).credentialEvents.lastOrNull { - it.data.thid == issuer.recall("thid") - } - issuer.remember("issuedCredential", credentialEvent!!.data) - - credentialEvent != null && - credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.CREDENTIAL_SENT - } - } - - @Then("{actor} receives the issued credential") - fun bobHasTheCredentialIssued(holder: Actor) { - Wait.until( - errorMessage = "Holder was unable to receive the credential from Issuer! " + - "Protocol state did not achieve ${IssueCredentialRecord.ProtocolState.CREDENTIAL_RECEIVED} state.", - ) { - credentialEvent = ListenToEvents.with(holder).credentialEvents.lastOrNull { - it.data.thid == holder.recall("thid") - } - credentialEvent != null && - credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.CREDENTIAL_RECEIVED - } - holder.remember("issuedCredential", ListenToEvents.with(holder).credentialEvents.last().data) - } - - @Then("{actor} should see that credential issuance has failed") - fun issuerShouldSeeThatCredentialIssuanceHasFailed(issuer: Actor) { - issuer.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_UNPROCESSABLE_ENTITY), - ) - } -} diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt new file mode 100644 index 0000000000..b5026a7163 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt @@ -0,0 +1,110 @@ +package steps.credentials + +import common.CredentialSchema +import interactions.Post +import interactions.body +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.SC_CREATED +import org.apache.http.HttpStatus.SC_OK +import org.hyperledger.identus.client.models.* + +class JwtCredentialSteps { + + private fun sendCredentialOffer( + issuer: Actor, + holder: Actor, + didForm: String, + schemaGuid: String?, + claims: Map, + ) { + val did: String = if (didForm == "short") { + issuer.recall("shortFormDid") + } else { + issuer.recall("longFormDid") + } + + val schemaId: String? = if (schemaGuid != null) { + val baseUrl = issuer.recall("baseUrl") + "$baseUrl/schema-registry/schemas/$schemaGuid" + } else { + null + } + + val credentialOfferRequest = CreateIssueCredentialRecordRequest( + schemaId = schemaId, + claims = claims, + issuingDID = did, + connectionId = issuer.recall("connection-with-${holder.name}").connectionId, + validityPeriod = 3600.0, + credentialFormat = "JWT", + automaticIssuance = false, + ) + + issuer.attemptsTo( + Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), + ) + } + + private fun saveCredentialOffer(issuer: Actor, holder: Actor) { + val credentialRecord = SerenityRest.lastResponse().get() + + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + + issuer.remember("thid", credentialRecord.thid) + holder.remember("thid", credentialRecord.thid) + } + + @When("{actor} offers a jwt credential to {actor} with {string} form DID") + fun issuerOffersAJwtCredential(issuer: Actor, holder: Actor, format: String) { + val claims = linkedMapOf( + "firstName" to "FirstName", + "lastName" to "LastName", + ) + sendCredentialOffer(issuer, holder, format, null, claims) + saveCredentialOffer(issuer, holder) + } + + @When("{actor} offers a jwt credential to {actor} with {} form using {} schema") + fun issuerOffersJwtCredentialToHolderUsingSchema( + issuer: Actor, + holder: Actor, + format: String, + schema: CredentialSchema, + ) { + val schemaGuid = issuer.recall(schema.name) + val claims = schema.claims + sendCredentialOffer(issuer, holder, format, schemaGuid, claims) + saveCredentialOffer(issuer, holder) + } + + @When("{actor} offers a jwt credential to {actor} with {} form DID with wrong claims structure using {} schema") + fun issuerOffersJwtCredentialToHolderWithWrongClaimStructure( + issuer: Actor, + holder: Actor, + format: String, + schema: CredentialSchema, + ) { + val schemaGuid = issuer.recall(schema.name)!! + val claims = linkedMapOf( + "name" to "Name", + "surname" to "Surname", + ) + sendCredentialOffer(issuer, holder, format, schemaGuid, claims) + } + + @When("{actor} accepts jwt credential offer") + fun holderAcceptsJwtCredentialOfferForJwt(holder: Actor) { + val recordId = holder.recall("recordId") + holder.attemptsTo( + Post.to("/issue-credentials/records/$recordId/accept-offer") + .body(AcceptCredentialOfferRequest(holder.recall("longFormDid"))), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt index ab8d16d289..3e3d539346 100644 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt @@ -6,15 +6,19 @@ import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.extensions.toJsonPath import io.iohk.atala.automation.serenity.ensure.Ensure -import io.iohk.atala.automation.utils.Wait +import io.iohk.atala.automation.serenity.interactions.PollingWait import models.JwtCredential import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor +import net.serenitybdd.screenplay.Question import org.apache.http.HttpStatus +import org.hamcrest.CoreMatchers.equalTo import org.hyperledger.identus.client.models.IssueCredentialRecord -import kotlin.time.Duration.Companion.seconds +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes class RevokeCredentialSteps { + @When("{actor} revokes the credential issued to {actor}") fun issuerRevokesCredentialsIssuedToHolder(issuer: Actor, holder: Actor) { val issuedCredential = issuer.recall("issuedCredential") @@ -50,18 +54,20 @@ class RevokeCredentialSteps { @Then("{actor} should see the credential was revoked") fun credentialShouldBeRevoked(issuer: Actor) { - Wait.until( - timeout = 60.seconds, - errorMessage = "Encoded Status List didn't change after revoking.", - ) { - val statusListId: String = issuer.recall("statusListId") - val encodedStatusList: String = issuer.recall("encodedStatusList") - issuer.attemptsTo( - Get.resource("/credential-status/$statusListId"), - ) - val actualEncodedList: String = SerenityRest.lastResponse().jsonPath().get("credentialSubject.encodedList") - actualEncodedList != encodedStatusList - } + issuer.attemptsTo( + PollingWait.with(1.minutes, 500.milliseconds).until( + Question.about("revocation status list").answeredBy { + val statusListId: String = issuer.recall("statusListId") + val encodedStatusList: String = issuer.recall("encodedStatusList") + issuer.attemptsTo( + Get.resource("/credential-status/$statusListId"), + ) + val actualEncodedList: String = SerenityRest.lastResponse().jsonPath().get("credentialSubject.encodedList") + actualEncodedList != encodedStatusList + }, + equalTo(true), + ), + ) } @Then("{actor} should see the credential is not revoked") diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/SdJwtCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/SdJwtCredentialSteps.kt new file mode 100644 index 0000000000..5bdf8bcf48 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/SdJwtCredentialSteps.kt @@ -0,0 +1,137 @@ +package steps.credentials + +import com.google.gson.Gson +import com.nimbusds.jose.util.Base64URL +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 +import io.iohk.atala.automation.serenity.ensure.Ensure +import models.JwtCredential +import models.SdJwtClaim +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus.* +import org.hyperledger.identus.client.models.* + +class SdJwtCredentialSteps { + + private val claims = linkedMapOf( + "firstName" to "Automation", + "lastName" to "Execution", + ) + + @When("{actor} offers a sd-jwt credential to {actor}") + fun issuerOffersSdJwtCredentialToHolder(issuer: Actor, holder: Actor) { + val connectionId = issuer.recall("connection-with-${holder.name}").connectionId + val did = issuer.recall("shortFormDid") + + val credentialOfferRequest = CreateIssueCredentialRecordRequest( + claims = claims, + issuingDID = did, + connectionId = connectionId, + validityPeriod = 3600.0, + credentialFormat = "SDJWT", + automaticIssuance = false, + ) + + issuer.attemptsTo( + Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + + val credentialRecord = SerenityRest.lastResponse().get() + issuer.remember("thid", credentialRecord.thid) + holder.remember("thid", credentialRecord.thid) + } + + @When("{actor} accepts credential offer for sd-jwt") + fun holderAcceptsSdJwtCredentialOffer(holder: Actor) { + holderAcceptsSdJwtCredentialOfferWithKeyBinding(holder, null) + } + + @When("{actor} accepts credential offer for sd-jwt with '{}' key binding") + fun holderAcceptsSdJwtCredentialOfferWithKeyBinding(holder: Actor, key: String?) { + val recordId = holder.recall("recordId") + val did = holder.recall("longFormDid") + val request = AcceptCredentialOfferRequest(subjectId = did, keyId = key) + holder.attemptsTo( + Post.to("/issue-credentials/records/$recordId/accept-offer").body(request), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @When("{actor} tries to offer a sd-jwt credential to {actor}") + fun issuerTriesToOfferSdJwtCredentialToHolder(issuer: Actor, holder: Actor) { + val connectionId = issuer.recall("connection-with-${holder.name}").connectionId + val did = issuer.recall("shortFormDid") + + val credentialOfferRequest = CreateIssueCredentialRecordRequest( + claims = claims, + issuingDID = did, + connectionId = connectionId, + validityPeriod = 3600.0, + credentialFormat = "SDJWT", + automaticIssuance = false, + ) + + issuer.attemptsTo( + Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), + ) + } + + @Then("{actor} checks the sd-jwt credential contents") + fun holderChecksTheSdJwtCredentialContents(holder: Actor) { + commonValidation(holder) { payload, _ -> + holder.attemptsTo( + Ensure.that(payload.containsKey("cnf")).isFalse(), + ) + } + } + + @Then("{actor} checks the sd-jwt credential contents with holder binding") + fun holderChecksTheSdJwtCredentialContentsWithHolderBinding(holder: Actor) { + commonValidation(holder) { payload, _ -> + holder.attemptsTo( + Ensure.that(payload.containsKey("cnf")).isTrue(), + ) + } + } + + @Then("{actor} should see the issuance has failed") + fun issuerShouldSeeTheIssuanceHasFailed(issuer: Actor) { + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_EXPECTATION_FAILED), + ) + } + + private fun commonValidation( + holder: Actor, + additionalChecks: (payload: Map, disclosedClaims: Map) -> Unit, + ) { + val issuedCredential = holder.recall("issuedCredential") + val jwtCredential = JwtCredential.parseBase64(issuedCredential.credential!!) + + val payload = jwtCredential.payload!!.toJSONObject() + + val disclosedClaims = jwtCredential.signature!!.toString().split("~") + .drop(1) + .dropLastWhile { it.isBlank() } + .map { Base64URL.from(it).decodeToString() } + .map { Gson().fromJson(it, Array::class.java) } + .associate { it[1] to SdJwtClaim(salt = it[0], key = it[1], value = it[2]) } + + holder.attemptsTo( + Ensure.that(payload.containsKey("_sd")).isTrue(), + Ensure.that(payload.containsKey("_sd_alg")).isTrue(), + Ensure.that(payload.containsKey("iss")).isTrue(), + Ensure.that(payload.containsKey("iat")).isTrue(), + Ensure.that(payload.containsKey("exp")).isTrue(), + Ensure.that(disclosedClaims["firstName"]!!.value).isEqualTo(claims["firstName"]!!), + Ensure.that(disclosedClaims["lastName"]!!.value).isEqualTo(claims["lastName"]!!), + ) + + additionalChecks(payload, disclosedClaims) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/did/DeactivateDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/DeactivateDidSteps.kt index 52a46869f5..5a4ab0c385 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/DeactivateDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/DeactivateDidSteps.kt @@ -1,17 +1,17 @@ package steps.did -import interactions.Get import interactions.Post import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get +import io.iohk.atala.automation.matchers.RestAssuredJsonProperty import io.iohk.atala.automation.serenity.ensure.Ensure -import io.iohk.atala.automation.utils.Wait +import io.iohk.atala.automation.serenity.interactions.PollingWait +import io.iohk.atala.automation.serenity.questions.HttpRequest import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus import org.hyperledger.identus.client.models.DIDOperationResponse -import org.hyperledger.identus.client.models.DIDResolutionResult class DeactivateDidSteps { @@ -37,9 +37,11 @@ class DeactivateDidSteps { @Then("{actor} sees that PRISM DID is successfully deactivated") fun actorSeesThatPrismDidIsSuccessfullyDeactivated(actor: Actor) { val deactivatedDid = actor.recall("deactivatedDid") - Wait.until(errorMessage = "ERROR: DID deactivate operation did not succeed on the ledger!") { - actor.attemptsTo(Get.resource("/dids/$deactivatedDid")) - SerenityRest.lastResponse().get().didDocumentMetadata.deactivated!! - } + actor.attemptsTo( + PollingWait.until( + HttpRequest.get("/dids/$deactivatedDid"), + RestAssuredJsonProperty.toBe("didDocumentMetadata.deactivated", "true"), + ), + ) } } diff --git a/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt index cd4302d194..d7c3addafb 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt @@ -8,14 +8,14 @@ import interactions.body import io.cucumber.java.en.* import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure -import io.iohk.atala.automation.utils.Wait +import io.iohk.atala.automation.serenity.interactions.PollingWait import net.serenitybdd.rest.SerenityRest 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.hamcrest.CoreMatchers.equalTo import org.hyperledger.identus.client.models.* -import kotlin.time.Duration.Companion.seconds class PublishDidSteps { @@ -46,7 +46,7 @@ class PublishDidSteps { @Given("{actor} creates unpublished DID") fun agentCreatesEmptyUnpublishedDid(actor: Actor) { - agentCreatesUnpublishedDid(actor, DidPurpose.EMPTY) + agentCreatesUnpublishedDid(actor, DidPurpose.CUSTOM) } @Given("{actor} creates unpublished DID for {}") @@ -75,6 +75,24 @@ class PublishDidSteps { actor.forget("hasPublishedDid") } + @When("{actor} prepares a custom PRISM DID") + fun actorPreparesCustomDid(actor: Actor) { + val customDid = DidPurpose.CUSTOM + actor.remember("customDid", customDid) + } + + @When("{actor} adds a '{curve}' key for '{purpose}' purpose with '{}' name to the custom PRISM DID") + fun actorAddsKeyToCustomDid(actor: Actor, curve: Curve, purpose: Purpose, name: String) { + val customDid = actor.recall("customDid") + customDid.publicKeys.add(ManagedDIDKeyTemplate(name, purpose, curve)) + } + + @When("{actor} creates the custom PRISM DID") + fun actorCreatesTheCustomPrismDid(actor: Actor) { + val customDid = actor.recall("customDid") + agentCreatesUnpublishedDid(actor, customDid) + } + @When("{actor} publishes DID to ledger") fun hePublishesDidToLedger(actor: Actor) { val shortFormDid = actor.recall("shortFormDid") @@ -87,18 +105,7 @@ class PublishDidSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_ACCEPTED), Ensure.that(didOperationResponse.scheduledOperation.didRef).isNotEmpty(), Ensure.that(didOperationResponse.scheduledOperation.id).isNotEmpty(), - ) - - Wait.until( - timeout = 30.seconds, - errorMessage = "ERROR: DID was not published to ledger!", - ) { - val didEvent = ListenToEvents.with(actor).didEvents.lastOrNull { - it.data.did == actor.recall("shortFormDid") - } - didEvent != null && didEvent.data.status == "PUBLISHED" - } - actor.attemptsTo( + PollingWait.until(ListenToEvents.didStatus(actor), equalTo("PUBLISHED")), Get.resource("/dids/${actor.recall("shortFormDid")}"), ) 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 b2a73bf072..e9e590d319 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt @@ -7,10 +7,12 @@ 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 io.iohk.atala.automation.utils.Wait +import io.iohk.atala.automation.serenity.interactions.PollingWait import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor +import net.serenitybdd.screenplay.Question import org.apache.http.HttpStatus +import org.hamcrest.CoreMatchers.equalTo import org.hyperledger.identus.client.models.* import java.util.UUID @@ -87,91 +89,111 @@ class UpdateDidSteps { fun actorSeesDidSuccessfullyUpdatedWithNewKeys(actor: Actor, purpose: Purpose) { val newDidKeyId = actor.recall("newDidKeyId") - Wait.until(errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!") { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - val didKey = "${actor.recall("shortFormDid")}#$newDidKeyId" - val didDocument = SerenityRest.lastResponse().get().didDocument!! - val foundVerificationMethod = didDocument.verificationMethod!!.map { it.id }.any { it == didKey } - - foundVerificationMethod && when (purpose) { - Purpose.ASSERTION_METHOD -> didDocument.assertionMethod!!.any { it == didKey } - Purpose.AUTHENTICATION -> didDocument.authentication!!.any { it == didKey } - Purpose.CAPABILITY_DELEGATION -> didDocument.capabilityDelegation!!.any { it == didKey } - Purpose.CAPABILITY_INVOCATION -> didDocument.capabilityInvocation!!.any { it == didKey } - Purpose.KEY_AGREEMENT -> didDocument.keyAgreement!!.any { it == didKey } - } - } + actor.attemptsTo( + PollingWait.until( + Question.about("did update").answeredBy { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val didKey = "${actor.recall("shortFormDid")}#$newDidKeyId" + val didDocument = SerenityRest.lastResponse().get().didDocument!! + val foundVerificationMethod = didDocument.verificationMethod!!.map { it.id }.any { it == didKey } + + foundVerificationMethod && when (purpose) { + Purpose.ASSERTION_METHOD -> didDocument.assertionMethod!!.any { it == didKey } + Purpose.AUTHENTICATION -> didDocument.authentication!!.any { it == didKey } + Purpose.CAPABILITY_DELEGATION -> didDocument.capabilityDelegation!!.any { it == didKey } + Purpose.CAPABILITY_INVOCATION -> didDocument.capabilityInvocation!!.any { it == didKey } + Purpose.KEY_AGREEMENT -> didDocument.keyAgreement!!.any { it == didKey } + } + }, + equalTo(true), + ), + ) } @Then("{actor} sees PRISM DID was successfully updated and keys removed with {purpose} purpose") fun actorSeesDidSuccessfullyUpdatedAndKeysRemoved(actor: Actor, purpose: Purpose) { val newDidKeyId = actor.recall("newDidKeyId") - Wait.until(errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!") { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - val didKey = "${actor.recall("shortFormDid")}#$newDidKeyId" - val didDocument = SerenityRest.lastResponse().get().didDocument!! - val verificationMethodNotPresent = didDocument.verificationMethod!!.map { it.id }.none { it == didKey } - - verificationMethodNotPresent && when (purpose) { - Purpose.ASSERTION_METHOD -> didDocument.assertionMethod!!.none { it == didKey } - Purpose.AUTHENTICATION -> didDocument.authentication!!.none { it == didKey } - Purpose.CAPABILITY_DELEGATION -> didDocument.capabilityDelegation!!.none { it == didKey } - Purpose.CAPABILITY_INVOCATION -> didDocument.capabilityInvocation!!.none { it == didKey } - Purpose.KEY_AGREEMENT -> didDocument.keyAgreement!!.none { it == didKey } - } - } + + actor.attemptsTo( + PollingWait.until( + Question.about("did update").answeredBy { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val didKey = "${actor.recall("shortFormDid")}#$newDidKeyId" + val didDocument = SerenityRest.lastResponse().get().didDocument!! + val verificationMethodNotPresent = didDocument.verificationMethod!!.map { it.id }.none { it == didKey } + + verificationMethodNotPresent && when (purpose) { + Purpose.ASSERTION_METHOD -> didDocument.assertionMethod!!.none { it == didKey } + Purpose.AUTHENTICATION -> didDocument.authentication!!.none { it == didKey } + Purpose.CAPABILITY_DELEGATION -> didDocument.capabilityDelegation!!.none { it == didKey } + Purpose.CAPABILITY_INVOCATION -> didDocument.capabilityInvocation!!.none { it == didKey } + Purpose.KEY_AGREEMENT -> didDocument.keyAgreement!!.none { it == didKey } + } + }, + equalTo(true), + ), + ) } @Then("{actor} sees that PRISM DID should have the new service") fun actorSeesDidSuccessfullyUpdatedWithNewServices(actor: Actor) { val serviceId = actor.recall("newServiceId") - Wait.until( - errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", - ) { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - val serviceIds = - SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } - serviceIds.any { - it == "${actor.recall("shortFormDid")}#$serviceId" - } - } + actor.attemptsTo( + PollingWait.until( + Question.about("did update").answeredBy { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val serviceIds = + SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } + serviceIds.any { + it == "${actor.recall("shortFormDid")}#$serviceId" + } + }, + equalTo(true), + ), + ) } @Then("{actor} sees the PRISM DID should have the service removed") fun actorSeesDidSuccessfullyUpdatedByRemovingServices(actor: Actor) { val serviceId = actor.recall("newServiceId") - Wait.until( - errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", - ) { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - val serviceIds = - SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } - serviceIds.none { - it == "${actor.recall("shortFormDid")}#$serviceId" - } - } + actor.attemptsTo( + PollingWait.until( + Question.about("did update").answeredBy { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val serviceIds = + SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } + serviceIds.none { + it == "${actor.recall("shortFormDid")}#$serviceId" + } + }, + equalTo(true), + ), + ) } @Then("{actor} sees the PRISM DID should have the service updated") fun actorSeesDidSuccessfullyUpdatedByUpdatingServices(actor: Actor) { val serviceUrl = actor.recall("newServiceUrl") - Wait.until( - errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", - ) { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - val service = SerenityRest.lastResponse().get().didDocument!!.service!! - service.any { it.serviceEndpoint.value.contains(serviceUrl) } - } + actor.attemptsTo( + PollingWait.until( + Question.about("did update").answeredBy { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val service = SerenityRest.lastResponse().get().didDocument!!.service!! + service.any { it.serviceEndpoint.value.contains(serviceUrl) } + }, + equalTo(true), + ), + ) } private fun actorSubmitsPrismDidUpdateOperation(actor: Actor, updatePrismDidAction: UpdateManagedDIDRequestAction) { diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt new file mode 100644 index 0000000000..218f233004 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt @@ -0,0 +1,172 @@ +package steps.oid4vci + +import abilities.ListenToEvents +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.jwk.JWK +import eu.europa.ec.eudi.openid4vci.* +import interactions.Post +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 kotlinx.coroutines.runBlocking +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus +import org.htmlunit.* +import org.htmlunit.html.HtmlPage +import org.htmlunit.html.HtmlPasswordInput +import org.htmlunit.html.HtmlSubmitInput +import org.htmlunit.html.HtmlTextInput +import org.hyperledger.identus.apollo.base64.base64UrlEncoded +import org.hyperledger.identus.apollo.utils.KMMECSecp256k1PrivateKey +import org.hyperledger.identus.apollo.utils.decodeHex +import org.hyperledger.identus.client.models.* +import org.hyperledger.identus.client.models.CredentialOfferRequest +import java.net.URI +import java.net.URL + +class IssueCredentialSteps { + @When("{actor} creates an offer using {string} configuration with {string} form DID") + fun issuerCreateCredentialOffer(issuer: Actor, configurationId: String, didForm: String) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + val claims = linkedMapOf( + "name" to "Alice", + "age" to 42, + ) + val did: String = if (didForm == "short") { + issuer.recall("shortFormDid") + } else { + issuer.recall("longFormDid") + } + issuer.attemptsTo( + Post.to("/oid4vci/issuers/${credentialIssuer.id}/credential-offers") + .with { + it.body( + CredentialOfferRequest( + credentialConfigurationId = configurationId, + issuingDID = did, + claims = claims, + ), + ) + }, + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_CREATED), + ) + val offerUri = SerenityRest.lastResponse().get().credentialOffer + issuer.remember("oid4vciOffer", offerUri) + } + + @When("{actor} receives oid4vci offer from {actor}") + fun holderReceivesOfferFromIssuer(holder: Actor, issuer: Actor) { + val offerUri = issuer.recall("oid4vciOffer") + holder.remember("oid4vciOffer", offerUri) + } + + @When("{actor} resolves oid4vci issuer metadata and login via front-end channel") + fun holderResolvesIssuerMetadata(holder: Actor) { + val offerUri = holder.recall("oid4vciOffer") + val credentialOffer = runBlocking { + CredentialOfferRequestResolver().resolve(offerUri).getOrThrow() + } + val redirectUrl = holder.recall("webhookUrl") + val openId4VCIConfig = OpenId4VCIConfig( + clientId = holder.recall("OID4VCI_AUTH_SERVER_CLIENT_ID"), + authFlowRedirectionURI = URI.create("$redirectUrl/auth-cb"), + keyGenerationConfig = KeyGenerationConfig.ecOnly(com.nimbusds.jose.jwk.Curve.SECP256K1), + credentialResponseEncryptionPolicy = CredentialResponseEncryptionPolicy.SUPPORTED, + parUsage = ParUsage.Never, + ) + val issuer = Issuer.make(openId4VCIConfig, credentialOffer).getOrThrow() + val authorizationRequest = runBlocking { + issuer.prepareAuthorizationRequest().getOrThrow() + } + val authResponse = keycloakLoginViaBrowser(authorizationRequest.authorizationCodeURL.value, holder) + val authorizedRequest = + with(issuer) { + runBlocking { + val authCode = AuthorizationCode(authResponse.first) + authorizationRequest.authorizeWithAuthorizationCode(authCode, authResponse.second).getOrThrow() + } + } + holder.remember("eudiCredentialOffer", credentialOffer) + holder.remember("eudiAuthorizedRequest", authorizedRequest) + holder.remember("eudiIssuer", issuer) + } + + @When("{actor} presents the access token with JWT proof on CredentialEndpoint") + fun holderPresentsTokenOnCredentialEdpoint(holder: Actor) { + val credentialOffer = holder.recall("eudiCredentialOffer") + val issuer = holder.recall("eudiIssuer") + val authorizedRequest = holder.recall("eudiAuthorizedRequest") + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialOffer.credentialConfigurationIdentifiers.first(), null) + val submissionOutcome = with(issuer) { + when (authorizedRequest) { + is AuthorizedRequest.NoProofRequired -> throw Exception("Not supported yet") + is AuthorizedRequest.ProofRequired -> runBlocking { + authorizedRequest.requestSingle( + requestPayload, + popSigner(), + ) + } + }.getOrThrow() + } + holder.remember("eudiSubmissionOutcome", submissionOutcome) + } + + @Then("{actor} sees credential issued successfully from CredentialEndpoint") + fun holderSeesCredentialIssuedSuccessfully(holder: Actor) { + val credentials = when (val submissionOutcome = holder.recall("eudiSubmissionOutcome")) { + is SubmissionOutcome.Success -> submissionOutcome.credentials + else -> throw Exception("Issuance failed. $submissionOutcome") + } + holder.attemptsTo( + Ensure.that(credentials).hasSize(1), + ) + } + + private fun popSigner(): PopSigner { + val privateKeyHex = "d93c6485e30aad4d6522313553e58d235693f7007b822676e5e1e9a667655b69" + val did = "did:prism:4a2bc09be65136f604d1564e2fced1a1cdbce9deb9b64ee396afc95fc0b01c59:CnsKeRI6CgZhdXRoLTEQBEouCglzZWNwMjU2azESIQOx16yykO2nDcmM-NeQeVipxmuaF38KasIA8gycJCHWJhI7CgdtYXN0ZXIwEAFKLgoJc2VjcDI1NmsxEiECKrfbf1_p7YT5aRJspBLct5zDyL6aicEam1Gycq5xKy0" + val kid = "$did#auth-1" + val privateKey = KMMECSecp256k1PrivateKey.secp256k1FromByteArray(privateKeyHex.decodeHex()) + val point = privateKey.getPublicKey().getCurvePoint() + val jwk = JWK.parse( + mapOf( + "kty" to "EC", + "crv" to "secp256k1", + "x" to point.x.base64UrlEncoded, + "y" to point.y.base64UrlEncoded, + "d" to privateKey.raw.base64UrlEncoded, + ), + ) + return PopSigner.jwtPopSigner( + privateKey = jwk, + algorithm = JWSAlgorithm.ES256K, + publicKey = JwtBindingKey.Did(identity = kid), + ) + } + + /** + * @return A tuple of authorization code and authorization server state + */ + private fun keycloakLoginViaBrowser(loginUrl: URL, actor: Actor): Pair { + val client = WebClient() + + // step 1 - login with username and password + val loginPage = client.getPage(loginUrl) + val loginForm = loginPage.forms.first() + loginForm.getInputByName("username").type(actor.name) + loginForm.getInputByName("password").type(actor.name) + val postLoginPage = loginForm.getInputByName("login").click() + + // If it is the first time user is logged in, Keycloak ask for consent by returning HtmlPage. + if (postLoginPage is HtmlPage) { + // step 2 - give client consent to access the scopes + val consentForm = postLoginPage.forms.first() + consentForm.getInputByName("accept").click() + } + + client.close() + return ListenToEvents.with(actor).authCodeCallbackEvents.last() + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredsPresentProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredProofSteps.kt similarity index 61% rename from tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredsPresentProofSteps.kt rename to tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredProofSteps.kt index ace6fc890e..e41618037c 100644 --- a/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredsPresentProofSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredProofSteps.kt @@ -1,26 +1,19 @@ package steps.proofs -import abilities.ListenToEvents -import interactions.Patch -import interactions.Post +import interactions.* import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure -import io.iohk.atala.automation.utils.Wait -import models.PresentationEvent -import models.PresentationStatusAdapter import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import net.serenitybdd.screenplay.rest.abilities.CallAnApi import org.apache.http.HttpStatus.SC_CREATED import org.hyperledger.identus.client.models.* -class AnoncredsPresentProofSteps { - - private var proofEvent: PresentationEvent? = null +class AnoncredProofSteps { @When("{actor} sends a anoncreds request for proof presentation to {actor} using credential definition issued by {actor}") - fun faberSendsAnAnoncredsRequestForProofPresentationToBob(faber: Actor, bob: Actor, issuer: Actor) { + fun verifierSendsAnAnoncredRequestForProofPresentationToHolder(verifier: Actor, holder: Actor, issuer: Actor) { val credentialDefinitionRegistryUrl = issuer.usingAbilityTo(CallAnApi::class.java) .resolve("/credential-definition-registry/definitions") @@ -54,49 +47,28 @@ class AnoncredsPresentProofSteps { version = "0.1", ) val presentationRequest = RequestPresentationInput( - connectionId = faber.recall("connection-with-${bob.name}").connectionId, + connectionId = verifier.recall("connection-with-${holder.name}").connectionId, credentialFormat = "AnonCreds", anoncredPresentationRequest = anoncredsPresentationRequestV1, proofs = emptyList(), ) - faber.attemptsTo( - Post.to("/present-proof/presentations") - .with { - it.body( - presentationRequest, - ) - }, - ) - faber.attemptsTo( + verifier.attemptsTo( + Post.to("/present-proof/presentations").body(presentationRequest), Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), ) val presentationStatus = SerenityRest.lastResponse().get() - faber.remember("thid", presentationStatus.thid) - bob.remember("thid", presentationStatus.thid) - } - - @When("{actor} receives the anoncreds request") - fun bobReceivesTheAnoncredsRequest(bob: Actor) { - Wait.until( - errorMessage = "ERROR: Bob did not achieve any presentation request!", - ) { - proofEvent = ListenToEvents.with(bob).presentationEvents.lastOrNull { - it.data.thid == bob.recall("thid") - } - proofEvent != null && - proofEvent!!.data.status == PresentationStatusAdapter.Status.REQUEST_RECEIVED - } - bob.remember("presentationId", proofEvent!!.data.presentationId) + verifier.remember("thid", presentationStatus.thid) + holder.remember("thid", presentationStatus.thid) } @When("{actor} accepts the anoncreds presentation request") - fun bobAcceptsTheAnoncredsPresentationWithProof(bob: Actor) { + fun holderAcceptsTheAnoncredsPresentationWithProof(holder: Actor) { val requestPresentationAction = RequestPresentationAction( anoncredPresentationRequest = AnoncredCredentialProofsV1( listOf( AnoncredCredentialProofV1( - bob.recall("issuedCredential").recordId, + holder.recall("issuedCredential").recordId, listOf("sex"), listOf("age"), ), @@ -105,13 +77,9 @@ class AnoncredsPresentProofSteps { action = RequestPresentationAction.Action.REQUEST_MINUS_ACCEPT, ) - val presentationId = bob.recall("presentationId") - bob.attemptsTo( - Patch.to("/present-proof/presentations/$presentationId").with { - it.body( - requestPresentationAction, - ) - }, + val presentationId = holder.recall("presentationId") + holder.attemptsTo( + Patch.to("/present-proof/presentations/$presentationId").body(requestPresentationAction), ) } } diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/HolderProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/HolderProofSteps.kt new file mode 100644 index 0000000000..bdf3d078f7 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/HolderProofSteps.kt @@ -0,0 +1,35 @@ +package steps.proofs + +import abilities.ListenToEvents +import interactions.* +import io.cucumber.java.en.When +import io.iohk.atala.automation.serenity.interactions.PollingWait +import models.PresentationStatusAdapter.Status.REQUEST_RECEIVED +import net.serenitybdd.screenplay.Actor +import org.hamcrest.CoreMatchers.equalTo +import org.hyperledger.identus.client.models.RequestPresentationAction + +class HolderProofSteps { + + @When("{actor} rejects the proof") + fun holderRejectsProof(holder: Actor) { + val presentationId: String = holder.recall("presentationId") + holder.attemptsTo( + Patch.to("/present-proof/presentations/$presentationId").body( + RequestPresentationAction(action = RequestPresentationAction.Action.REQUEST_MINUS_REJECT), + ), + ) + } + + @When("{actor} receives the presentation proof request") + fun holderReceivesTheRequest(holder: Actor) { + holder.attemptsTo( + PollingWait.until( + ListenToEvents.presentationProofStatus(holder), + equalTo(REQUEST_RECEIVED), + ), + ) + val presentationId = ListenToEvents.with(holder).presentationEvents.last().data.presentationId + holder.remember("presentationId", presentationId) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/JwtProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/JwtProofSteps.kt new file mode 100644 index 0000000000..78985e62b0 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/JwtProofSteps.kt @@ -0,0 +1,52 @@ +package steps.proofs + +import interactions.* +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.SC_CREATED +import org.apache.http.HttpStatus.SC_OK +import org.hyperledger.identus.client.models.* + +class JwtProofSteps { + + @When("{actor} sends a request for jwt proof presentation to {actor}") + fun verifierSendsARequestForJwtProofPresentationToHolder(verifier: Actor, holder: Actor) { + val verifierConnectionToHolder = verifier.recall("connection-with-${holder.name}").connectionId + val presentationRequest = RequestPresentationInput( + connectionId = verifierConnectionToHolder, + options = Options( + challenge = "11c91493-01b3-4c4d-ac36-b336bab5bddf", + domain = "https://example-verifier.com", + ), + proofs = listOf( + ProofRequestAux( + schemaId = "https://schema.org/Person", + trustIssuers = listOf("did:web:atalaprism.io/users/testUser"), + ), + ), + ) + verifier.attemptsTo( + Post.to("/present-proof/presentations").body(presentationRequest), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + val presentationStatus = SerenityRest.lastResponse().get() + verifier.remember("thid", presentationStatus.thid) + holder.remember("thid", presentationStatus.thid) + } + + @When("{actor} makes the jwt presentation of the proof") + fun holderMakesThePresentationOfTheProofToVerifier(holder: Actor) { + val requestPresentationAction = RequestPresentationAction( + proofId = listOf(holder.recall("issuedCredential").recordId), + action = RequestPresentationAction.Action.REQUEST_MINUS_ACCEPT, + ) + val presentationId: String = holder.recall("presentationId") + holder.attemptsTo( + Patch.to("/present-proof/presentations/$presentationId").body(requestPresentationAction), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt deleted file mode 100644 index b0599b7627..0000000000 --- a/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt +++ /dev/null @@ -1,123 +0,0 @@ -package steps.proofs - -import abilities.ListenToEvents -import interactions.Patch -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 -import io.iohk.atala.automation.serenity.ensure.Ensure -import io.iohk.atala.automation.utils.Wait -import models.PresentationStatusAdapter -import net.serenitybdd.rest.SerenityRest -import net.serenitybdd.screenplay.Actor -import org.apache.http.HttpStatus.* -import org.hyperledger.identus.client.models.* -import kotlin.time.Duration.Companion.seconds - -class PresentProofSteps { - - @When("{actor} sends a request for proof presentation to {actor}") - fun verifierSendsARequestForProofPresentationToHolder(verifier: Actor, holder: Actor) { - val verifierConnectionToHolder = verifier.recall("connection-with-${holder.name}").connectionId - val presentationRequest = RequestPresentationInput( - connectionId = verifierConnectionToHolder, - options = Options( - challenge = "11c91493-01b3-4c4d-ac36-b336bab5bddf", - domain = "https://example-verifier.com", - ), - proofs = listOf( - ProofRequestAux( - schemaId = "https://schema.org/Person", - trustIssuers = listOf("did:web:atalaprism.io/users/testUser"), - ), - ), - ) - verifier.attemptsTo( - Post.to("/present-proof/presentations").body(presentationRequest), - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), - ) - val presentationStatus = SerenityRest.lastResponse().get() - verifier.remember("thid", presentationStatus.thid) - holder.remember("thid", presentationStatus.thid) - } - - @When("{actor} receives the presentation proof request") - fun holderReceivesTheRequest(holder: Actor) { - Wait.until( - timeout = 30.seconds, - errorMessage = "ERROR: Bob did not achieve any presentation request!", - ) { - val proofEvent = ListenToEvents.with(holder).presentationEvents.lastOrNull { - it.data.thid == holder.recall("thid") - } - holder.remember("presentationId", proofEvent?.data?.presentationId) - proofEvent?.data?.status == PresentationStatusAdapter.Status.REQUEST_RECEIVED - } - } - - @When("{actor} makes the presentation of the proof") - fun holderMakesThePresentationOfTheProofToVerifier(holder: Actor) { - val requestPresentationAction = RequestPresentationAction( - proofId = listOf(holder.recall("issuedCredential").recordId), - action = RequestPresentationAction.Action.REQUEST_MINUS_ACCEPT, - ) - val presentationId: String = holder.recall("presentationId") - holder.attemptsTo( - Patch.to("/present-proof/presentations/$presentationId").body(requestPresentationAction), - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), - ) - } - - @When("{actor} rejects the proof") - fun holderRejectsProof(holder: Actor) { - val presentationId: String = holder.recall("presentationId") - holder.attemptsTo( - Patch.to("/present-proof/presentations/$presentationId").with { - it.body( - RequestPresentationAction(action = RequestPresentationAction.Action.REQUEST_MINUS_REJECT), - ) - }, - ) - } - - @Then("{actor} sees the proof is rejected") - fun bobSeesProofIsRejected(bob: Actor) { - Wait.until( - timeout = 30.seconds, - errorMessage = "ERROR: Faber did not receive presentation from Bob!", - ) { - val proofEvent = ListenToEvents.with(bob).presentationEvents.lastOrNull { - it.data.thid == bob.recall("thid") - } - proofEvent?.data?.status == PresentationStatusAdapter.Status.REQUEST_REJECTED - } - } - - @Then("{actor} has the proof verified") - fun faberHasTheProofVerified(faber: Actor) { - Wait.until( - timeout = 60.seconds, - errorMessage = "Presentation did not achieve PresentationVerified state!", - ) { - val proofEvent = ListenToEvents.with(faber).presentationEvents.lastOrNull { - it.data.thid == faber.recall("thid") - } - proofEvent?.data?.status == PresentationStatusAdapter.Status.PRESENTATION_VERIFIED - } - } - - @Then("{actor} sees the proof returned verification failed") - fun verifierSeesTheProofReturnedVerificationFailed(verifier: Actor) { - Wait.until( - timeout = 120.seconds, - errorMessage = "Presentation did not achieve PresentationVerificationFailed state!", - ) { - val proofEvent = ListenToEvents.with(verifier).presentationEvents.lastOrNull { - it.data.thid == verifier.recall("thid") - } - proofEvent?.data?.status == PresentationStatusAdapter.Status.PRESENTATION_VERIFICATION_FAILED - } - } -} diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/SdJwtProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/SdJwtProofSteps.kt new file mode 100644 index 0000000000..41eee396b0 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/SdJwtProofSteps.kt @@ -0,0 +1,99 @@ +package steps.proofs + +import abilities.ListenToEvents +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.nimbusds.jose.util.Base64URL +import interactions.* +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 models.JwtCredential +import models.SdJwtClaim +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus.SC_CREATED +import org.apache.http.HttpStatus.SC_OK +import org.hyperledger.identus.client.models.* + +class SdJwtProofSteps { + + @When("{actor} sends a request for sd-jwt proof presentation to {actor} requesting [{}] claims") + fun verifierSendsARequestForSdJwtProofPresentationToHolder(verifier: Actor, holder: Actor, keys: String) { + val claims = JsonObject() + for (key in keys.split(",")) { + claims.addProperty(key, "{}") + } + val verifierConnectionToHolder = verifier.recall("connection-with-${holder.name}").connectionId + val presentationRequest = RequestPresentationInput( + connectionId = verifierConnectionToHolder, + options = Options( + challenge = "11c91493-01b3-4c4d-ac36-b336bab5bddf", + domain = "https://example-verifier.com", + ), + proofs = listOf(), + credentialFormat = "SDJWT", + claims = claims, + ) + verifier.attemptsTo( + Post.to("/present-proof/presentations").body(presentationRequest), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + val presentationStatus = SerenityRest.lastResponse().get() + verifier.remember("thid", presentationStatus.thid) + holder.remember("thid", presentationStatus.thid) + } + + @When("{actor} makes the sd-jwt presentation of the proof disclosing [{}] claims") + fun holderMakesThePresentationOfTheProofToVerifier(holder: Actor, keys: String) { + val claims = JsonObject() + for (key in keys.split(",")) { + claims.addProperty(key, "{}") + } + + val requestPresentationAction = RequestPresentationAction( + proofId = listOf(holder.recall("issuedCredential").recordId), + action = RequestPresentationAction.Action.REQUEST_MINUS_ACCEPT, + claims = claims, + credentialFormat = "SDJWT", + ) + + val presentationId: String = holder.recall("presentationId") + holder.attemptsTo( + Patch.to("/present-proof/presentations/$presentationId").body(requestPresentationAction), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @Then("{actor} has the sd-jwt proof verified") + fun verifierHasTheSdJwtProofVerified(verifier: Actor) { + VerifierProofSteps().verifierHasTheProofVerified(verifier) + val proofEvent = ListenToEvents.with(verifier).presentationEvents.last().data + val sdjwt = proofEvent.data!!.first() + val isBound = !sdjwt.endsWith("~") // if it ends with a ~ there's no binding + + val jwt = JwtCredential.parseJwt(sdjwt.split("~").first()) + val claims = sdjwt.split("~") + .drop(1) + .dropLast(if (isBound) 1 else 0) + .dropLastWhile { it.isBlank() } + .map { Base64URL.from(it).decodeToString() } + .map { Gson().fromJson(it, Array::class.java) } + .associate { it[1] to SdJwtClaim(salt = it[0], key = it[1], value = it[2]) } + + verifier.attemptsTo( + Ensure.that(claims.containsKey("firstName")).isTrue(), + Ensure.that(claims.containsKey("lastName")).isFalse(), + ) + + if (isBound) { + val bindingJwt = JwtCredential.parseJwt(sdjwt.split("~").last()) + val payload = Gson().toJsonTree(bindingJwt.payload!!.toJSONObject()).asJsonObject + + verifier.attemptsTo( + Ensure.that(payload.get("aud").asString).isEqualTo("https://example-verifier.com"), + ) + } + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/VerifierProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/VerifierProofSteps.kt new file mode 100644 index 0000000000..e99c103c83 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/VerifierProofSteps.kt @@ -0,0 +1,32 @@ +package steps.proofs + +import abilities.ListenToEvents.Companion.presentationProofStatus +import io.cucumber.java.en.Then +import io.iohk.atala.automation.serenity.interactions.PollingWait +import models.PresentationStatusAdapter.Status.* +import net.serenitybdd.screenplay.Actor +import org.hamcrest.CoreMatchers.equalTo + +class VerifierProofSteps { + + @Then("{actor} sees the proof returned verification failed") + fun verifierSeesTheProofReturnedVerificationFailed(verifier: Actor) { + verifier.attemptsTo( + PollingWait.until(presentationProofStatus(verifier), equalTo(PRESENTATION_VERIFICATION_FAILED)), + ) + } + + @Then("{actor} sees the proof is rejected") + fun verifierSeesProofIsRejected(verifier: Actor) { + verifier.attemptsTo( + PollingWait.until(presentationProofStatus(verifier), equalTo(REQUEST_REJECTED)), + ) + } + + @Then("{actor} has the proof verified") + fun verifierHasTheProofVerified(verifier: Actor) { + verifier.attemptsTo( + PollingWait.until(presentationProofStatus(verifier), equalTo(PRESENTATION_VERIFIED)), + ) + } +} diff --git a/tests/integration-tests/src/test/resources/configs/basic.conf b/tests/integration-tests/src/test/resources/configs/basic.conf index 954af03916..af91c7bf2a 100644 --- a/tests/integration-tests/src/test/resources/configs/basic.conf +++ b/tests/integration-tests/src/test/resources/configs/basic.conf @@ -6,10 +6,15 @@ services = { } keycloak_oid4vci = { http_port = 9981 + logger_name = "keycloak_oid4vci" compose_file = "src/test/resources/containers/keycloak-oid4vci.yml" realm = "oid4vci-holder" + extra_scopes = ["StudentProfile"] extra_envs = { - IDENTUS_URL = "${ISSUER_AGENT_URL:-http://localhost:8080}" + IDENTUS_URL = "${ISSUER_AGENT_URL:-http://host.docker.internal:8080}" + } + extra_clients = { + holder = { redirectUris = ["${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}/*"] } } } } @@ -40,7 +45,7 @@ roles = [ url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" init_required = true } - oid4vci_auth_server = "http://localhost:9981" + oid4vci_auth_server = "http://host.docker.internal:9981/realms/oid4vci-holder" }, { name = "Holder" diff --git a/tests/integration-tests/src/test/resources/configs/mt_keycloak.conf b/tests/integration-tests/src/test/resources/configs/mt_keycloak.conf index b240ff1705..2e8d6da9a2 100644 --- a/tests/integration-tests/src/test/resources/configs/mt_keycloak.conf +++ b/tests/integration-tests/src/test/resources/configs/mt_keycloak.conf @@ -9,10 +9,15 @@ services = { } keycloak_oid4vci = { http_port = 9981 + logger_name = "keycloak_oid4vci" compose_file = "src/test/resources/containers/keycloak-oid4vci.yml" realm = "oid4vci-holder" + extra_scopes = ["StudentProfile"] extra_envs = { - IDENTUS_URL = "${ISSUER_AGENT_URL:-http://localhost:8080}" + IDENTUS_URL = "${ISSUER_AGENT_URL:-http://host.docker.internal:8080}" + } + extra_clients = { + holder = { redirectUris = ["${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}/*"] } } } } @@ -43,7 +48,7 @@ roles = [ url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" init_required = true } - oid4vci_auth_server = "http://localhost:9981" + oid4vci_auth_server = "http://host.docker.internal:9981/realms/oid4vci-holder" }, { name = "Holder" diff --git a/tests/integration-tests/src/test/resources/configs/mt_keycloak_agent_role.conf b/tests/integration-tests/src/test/resources/configs/mt_keycloak_agent_role.conf index d3ba070f5a..41d8550074 100644 --- a/tests/integration-tests/src/test/resources/configs/mt_keycloak_agent_role.conf +++ b/tests/integration-tests/src/test/resources/configs/mt_keycloak_agent_role.conf @@ -7,6 +7,19 @@ services = { keycloak = { http_port = 9980 } + keycloak_oid4vci = { + http_port = 9981 + logger_name = "keycloak_oid4vci" + compose_file = "src/test/resources/containers/keycloak-oid4vci.yml" + realm = "oid4vci-holder" + extra_scopes = ["StudentProfile"] + extra_envs = { + IDENTUS_URL = "${ISSUER_AGENT_URL:-http://host.docker.internal:8080}" + } + extra_clients = { + holder = { redirectUris = ["${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}/*"] } + } + } } # Specify agents that are required to be created before running tests @@ -35,6 +48,7 @@ roles = [ url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" init_required = true } + oid4vci_auth_server = "http://host.docker.internal:9981/realms/oid4vci-holder" }, { name = "Holder" diff --git a/tests/integration-tests/src/test/resources/configs/mt_keycloak_vault.conf b/tests/integration-tests/src/test/resources/configs/mt_keycloak_vault.conf index 66957bf226..37271e2a5f 100644 --- a/tests/integration-tests/src/test/resources/configs/mt_keycloak_vault.conf +++ b/tests/integration-tests/src/test/resources/configs/mt_keycloak_vault.conf @@ -7,6 +7,19 @@ services = { keycloak = { http_port = 9980 } + keycloak_oid4vci = { + http_port = 9981 + logger_name = "keycloak_oid4vci" + compose_file = "src/test/resources/containers/keycloak-oid4vci.yml" + realm = "oid4vci-holder" + extra_scopes = ["StudentProfile"] + extra_envs = { + IDENTUS_URL = "${ISSUER_AGENT_URL:-http://host.docker.internal:8080}" + } + extra_clients = { + holder = { redirectUris = ["${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}/*"] } + } + } vault = { http_port = 8200 auth_type = "APP_ROLE" @@ -41,6 +54,7 @@ roles = [ url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" init_required = true } + oid4vci_auth_server = "http://host.docker.internal:9981/realms/oid4vci-holder" }, { name = "Holder" diff --git a/tests/integration-tests/src/test/resources/configs/mt_vault_approle.conf b/tests/integration-tests/src/test/resources/configs/mt_vault_approle.conf index b50656519a..aa81ec52fd 100644 --- a/tests/integration-tests/src/test/resources/configs/mt_vault_approle.conf +++ b/tests/integration-tests/src/test/resources/configs/mt_vault_approle.conf @@ -8,6 +8,19 @@ services = { http_port = 8200, vault_auth_type = "APP_ROLE" } + keycloak_oid4vci = { + http_port = 9981 + logger_name = "keycloak_oid4vci" + compose_file = "src/test/resources/containers/keycloak-oid4vci.yml" + realm = "oid4vci-holder" + extra_scopes = ["StudentProfile"] + extra_envs = { + IDENTUS_URL = "${ISSUER_AGENT_URL:-http://host.docker.internal:8080}" + } + extra_clients = { + holder = { redirectUris = ["${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}/*"] } + } + } } # Specify agents that are required to be created before running tests @@ -37,6 +50,7 @@ roles = [ url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" init_required = true } + oid4vci_auth_server = "http://host.docker.internal:9981/realms/oid4vci-holder" }, { name = "Holder" diff --git a/tests/integration-tests/src/test/resources/configs/mt_vault_token.conf b/tests/integration-tests/src/test/resources/configs/mt_vault_token.conf index e6493e2126..85a52323de 100644 --- a/tests/integration-tests/src/test/resources/configs/mt_vault_token.conf +++ b/tests/integration-tests/src/test/resources/configs/mt_vault_token.conf @@ -8,6 +8,19 @@ services = { http_port = 8200, vault_auth_type = TOKEN } + keycloak_oid4vci = { + http_port = 9981 + logger_name = "keycloak_oid4vci" + compose_file = "src/test/resources/containers/keycloak-oid4vci.yml" + realm = "oid4vci-holder" + extra_scopes = ["StudentProfile"] + extra_envs = { + IDENTUS_URL = "${ISSUER_AGENT_URL:-http://host.docker.internal:8080}" + } + extra_clients = { + holder = { redirectUris = ["${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}/*"] } + } + } } # Specify agents that are required to be created before running tests @@ -37,6 +50,7 @@ roles = [ url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" init_required = true } + oid4vci_auth_server = "http://host.docker.internal:9981/realms/oid4vci-holder" }, { name = "Holder" diff --git a/tests/integration-tests/src/test/resources/configs/two_agents_basic.conf b/tests/integration-tests/src/test/resources/configs/two_agents_basic.conf index 0ffcb90173..f95568f44f 100644 --- a/tests/integration-tests/src/test/resources/configs/two_agents_basic.conf +++ b/tests/integration-tests/src/test/resources/configs/two_agents_basic.conf @@ -4,6 +4,19 @@ services = { http_port = 50053 version = "${PRISM_NODE_VERSION}" } + keycloak_oid4vci = { + http_port = 9981 + logger_name = "keycloak_oid4vci" + compose_file = "src/test/resources/containers/keycloak-oid4vci.yml" + realm = "oid4vci-holder" + extra_scopes = ["StudentProfile"] + extra_envs = { + IDENTUS_URL = "${ISSUER_AGENT_URL:-http://host.docker.internal:8080}" + } + extra_clients = { + holder = { redirectUris = ["${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}/*"] } + } + } } # Specify agents that are required to be created before running tests @@ -39,6 +52,7 @@ roles = [ url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" init_required = true } + oid4vci_auth_server = "http://host.docker.internal:9981/realms/oid4vci-holder" }, { name = "Holder" diff --git a/tests/integration-tests/src/test/resources/configs/two_agents_sharing_keycloak.conf b/tests/integration-tests/src/test/resources/configs/two_agents_sharing_keycloak.conf index 9824e1ff4c..c67f2312f9 100644 --- a/tests/integration-tests/src/test/resources/configs/two_agents_sharing_keycloak.conf +++ b/tests/integration-tests/src/test/resources/configs/two_agents_sharing_keycloak.conf @@ -7,6 +7,19 @@ services = { keycloak = { http_port = 9980 } + keycloak_oid4vci = { + http_port = 9981 + logger_name = "keycloak_oid4vci" + compose_file = "src/test/resources/containers/keycloak-oid4vci.yml" + realm = "oid4vci-holder" + extra_scopes = ["StudentProfile"] + extra_envs = { + IDENTUS_URL = "${ISSUER_AGENT_URL:-http://host.docker.internal:8080}" + } + extra_clients = { + holder = { redirectUris = ["${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}/*"] } + } + } } # Specify agents that are required to be created before running tests @@ -44,6 +57,7 @@ roles = [ url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" init_required = true } + oid4vci_auth_server = "http://host.docker.internal:9981/realms/oid4vci-holder" }, { name = "Holder" diff --git a/tests/integration-tests/src/test/resources/containers/keycloak-oid4vci.yml b/tests/integration-tests/src/test/resources/containers/keycloak-oid4vci.yml index 2dae22b4ae..dcfb8c4274 100644 --- a/tests/integration-tests/src/test/resources/containers/keycloak-oid4vci.yml +++ b/tests/integration-tests/src/test/resources/containers/keycloak-oid4vci.yml @@ -3,11 +3,13 @@ version: "3.8" services: keycloak: - image: ghcr.io/hyperledger/identus-keycloak-plugins:0.1.0 + image: ghcr.io/hyperledger/identus-keycloak-plugins:0.2.0 ports: - "${KEYCLOAK_HTTP_PORT}:8080" environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin IDENTUS_URL: - command: start-dev --health-enabled=true --hostname-url=http://localhost:${KEYCLOAK_HTTP_PORT} + command: start-dev --health-enabled=true --hostname-url=http://host.docker.internal:${KEYCLOAK_HTTP_PORT} + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/tests/integration-tests/src/test/resources/features/credentials/issue_anoncred_with_published_did.feature b/tests/integration-tests/src/test/resources/features/credential/anoncred/issuance.feature similarity index 78% rename from tests/integration-tests/src/test/resources/features/credentials/issue_anoncred_with_published_did.feature rename to tests/integration-tests/src/test/resources/features/credential/anoncred/issuance.feature index 051e8df412..712d6d7db6 100644 --- a/tests/integration-tests/src/test/resources/features/credentials/issue_anoncred_with_published_did.feature +++ b/tests/integration-tests/src/test/resources/features/credential/anoncred/issuance.feature @@ -1,5 +1,5 @@ -@RFC0453 @AIP20 @credentials -Feature: Issue Anoncred with published DID +@anoncred @issuance +Feature: Issue Anoncred credential Background: Given Issuer and Holder have an existing connection @@ -10,6 +10,6 @@ Feature: Issue Anoncred with published DID Given Issuer has an anoncred schema definition When Issuer offers anoncred to Holder And Holder receives the credential offer - And Holder accepts credential offer for anoncred + And Holder accepts anoncred credential offer And Issuer issues the credential Then Holder receives the issued credential diff --git a/tests/integration-tests/src/test/resources/features/proofs/present_proof_anoncred.feature b/tests/integration-tests/src/test/resources/features/credential/anoncred/present_proof.feature similarity index 86% rename from tests/integration-tests/src/test/resources/features/proofs/present_proof_anoncred.feature rename to tests/integration-tests/src/test/resources/features/credential/anoncred/present_proof.feature index 77c52dc572..8bc05395c0 100644 --- a/tests/integration-tests/src/test/resources/features/proofs/present_proof_anoncred.feature +++ b/tests/integration-tests/src/test/resources/features/credential/anoncred/present_proof.feature @@ -1,4 +1,4 @@ -@proof @anoncreds +@anoncred @proof Feature: Present Proof Protocol Scenario: Holder presents anoncreds credential proof to verifier @@ -9,10 +9,10 @@ Scenario: Holder presents anoncreds credential proof to verifier And Issuer has an anoncred schema definition And Issuer offers anoncred to Holder And Holder receives the credential offer - And Holder accepts credential offer for anoncred + And Holder accepts anoncred credential offer And Issuer issues the credential And Holder receives the issued credential When Verifier sends a anoncreds request for proof presentation to Holder using credential definition issued by Issuer - And Holder receives the anoncreds request + And Holder receives the presentation proof request And Holder accepts the anoncreds presentation request # Then Verifier has the proof verified FIXME diff --git a/tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature b/tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature new file mode 100644 index 0000000000..2e9689f772 --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature @@ -0,0 +1,41 @@ +@jwt @issuance +Feature: Issue JWT credential + + Scenario: Issuing jwt credential with published PRISM DID + Given Issuer and Holder have an existing connection + And Issuer has a published DID for JWT + And Holder has an unpublished DID for JWT + When Issuer offers a jwt credential to Holder with "short" form DID + And Holder receives the credential offer + And Holder accepts jwt credential offer + And Issuer issues the credential + Then Holder receives the issued credential + + Scenario: Issuing jwt credential with a schema + Given Issuer and Holder have an existing connection + And Issuer has a published DID for JWT + And Issuer has published STUDENT_SCHEMA schema + And Holder has an unpublished DID for JWT + When Issuer offers a jwt credential to Holder with "short" form using STUDENT_SCHEMA schema + And Holder receives the credential offer + And Holder accepts jwt credential offer + And Issuer issues the credential + Then Holder receives the issued credential + + Scenario: Issuing jwt credential with wrong claim structure for schema + Given Issuer and Holder have an existing connection + And Issuer has a published DID for JWT + And Issuer has published STUDENT_SCHEMA schema + And Holder has an unpublished DID for JWT + When Issuer offers a jwt credential to Holder with "short" form DID with wrong claims structure using STUDENT_SCHEMA schema + Then Issuer should see that credential issuance has failed + + Scenario: Issuing jwt credential with unpublished PRISM DID + Given Issuer and Holder have an existing connection + And Issuer has an unpublished DID for JWT + And Holder has an unpublished DID for JWT + And Issuer offers a jwt credential to Holder with "long" form DID + And Holder receives the credential offer + And Holder accepts jwt credential offer + And Issuer issues the credential + Then Holder receives the issued credential diff --git a/tests/integration-tests/src/test/resources/features/proofs/present_proof.feature b/tests/integration-tests/src/test/resources/features/credential/jwt/present_proof.feature similarity index 60% rename from tests/integration-tests/src/test/resources/features/proofs/present_proof.feature rename to tests/integration-tests/src/test/resources/features/credential/jwt/present_proof.feature index c2fa51b80d..5b0f46564c 100644 --- a/tests/integration-tests/src/test/resources/features/proofs/present_proof.feature +++ b/tests/integration-tests/src/test/resources/features/credential/jwt/present_proof.feature @@ -1,26 +1,26 @@ -@proof @jwt +@jwt @proof Feature: Present Proof Protocol - Scenario: Holder presents credential proof to verifier + Scenario: Holder presents jwt credential proof to verifier Given Verifier and Holder have an existing connection And Holder has a jwt issued credential from Issuer - When Verifier sends a request for proof presentation to Holder + When Verifier sends a request for jwt proof presentation to Holder And Holder receives the presentation proof request - And Holder makes the presentation of the proof + And Holder makes the jwt presentation of the proof Then Verifier has the proof verified - Scenario: Holder presents proof to verifier which is the issuer itself + Scenario: Holder presents jwt proof to verifier which is the issuer itself Given Issuer and Holder have an existing connection And Holder has a jwt issued credential from Issuer - When Issuer sends a request for proof presentation to Holder + When Issuer sends a request for jwt proof presentation to Holder And Holder receives the presentation proof request - And Holder makes the presentation of the proof + And Holder makes the jwt presentation of the proof Then Issuer has the proof verified Scenario: Verifier rejects holder proof Given Verifier and Holder have an existing connection And Holder has a jwt issued credential from Issuer - When Verifier sends a request for proof presentation to Holder + When Verifier sends a request for jwt proof presentation to Holder And Holder receives the presentation proof request And Holder rejects the proof Then Holder sees the proof is rejected diff --git a/tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature b/tests/integration-tests/src/test/resources/features/credential/jwt/revocation.feature similarity index 55% rename from tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature rename to tests/integration-tests/src/test/resources/features/credential/jwt/revocation.feature index b60e7d7bf9..508380402d 100644 --- a/tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature +++ b/tests/integration-tests/src/test/resources/features/credential/jwt/revocation.feature @@ -1,21 +1,21 @@ -@revocation @jwt -Feature: Credential revocation - JWT +@jwt @revocation +Feature: JWT Credential revocation Background: Given Holder has a jwt issued credential from Issuer - Scenario: Revoke issued credential + Scenario: Revoke jwt issued credential When Issuer revokes the credential issued to Holder Then Issuer should see the credential was revoked - When Issuer sends a request for proof presentation to Holder + When Issuer sends a request for jwt proof presentation to Holder And Holder receives the presentation proof request - And Holder makes the presentation of the proof + And Holder makes the jwt presentation of the proof Then Issuer sees the proof returned verification failed - Scenario: Holder tries to revoke credential from issuer + Scenario: Holder tries to revoke jwt credential from issuer When Holder tries to revoke credential from Issuer - And Issuer sends a request for proof presentation to Holder + And Issuer sends a request for jwt proof presentation to Holder And Holder receives the presentation proof request - And Holder makes the presentation of the proof + And Holder makes the jwt presentation of the proof Then Issuer has the proof verified And Issuer should see the credential is not revoked diff --git a/tests/integration-tests/src/test/resources/features/credential/sdjwt/issuance.feature b/tests/integration-tests/src/test/resources/features/credential/sdjwt/issuance.feature new file mode 100644 index 0000000000..29af552150 --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/credential/sdjwt/issuance.feature @@ -0,0 +1,37 @@ +@sdjwt @issuance +Feature: Issue SD-JWT credential + + Scenario: Issuing sd-jwt credential + Given Issuer and Holder have an existing connection + And Issuer has a published DID for SD_JWT + And Holder has an unpublished DID for SD_JWT + When Issuer offers a sd-jwt credential to Holder + And Holder receives the credential offer + And Holder accepts credential offer for sd-jwt + And Issuer issues the credential + Then Holder receives the issued credential + And Holder checks the sd-jwt credential contents + + Scenario: Issuing sd-jwt credential with holder binding + Given Issuer and Holder have an existing connection + And Issuer has a published DID for SD_JWT + And Holder has an unpublished DID for SD_JWT + When Issuer offers a sd-jwt credential to Holder + And Holder receives the credential offer + And Holder accepts credential offer for sd-jwt with 'auth-1' key binding + And Issuer issues the credential + Then Holder receives the issued credential + Then Holder checks the sd-jwt credential contents with holder binding + +# Scenario: Issuing sd-jwt with wrong algorithm +# Given Issuer and Holder have an existing connection +# When Issuer prepares a custom PRISM DID +# And Issuer adds a 'secp256k1' key for 'assertionMethod' purpose with 'assert-1' name to the custom PRISM DID +# And Issuer adds a 'secp256k1' key for 'authentication' purpose with 'auth-1' name to the custom PRISM DID +# And Issuer creates the custom PRISM DID +# And Holder has an unpublished DID for SD_JWT +# And Issuer offers a sd-jwt credential to Holder +# And Holder receives the credential offer +# And Holder accepts credential offer for sd-jwt +# And Issuer tries to issue the credential +# Then Issuer should see that credential issuance has failed diff --git a/tests/integration-tests/src/test/resources/features/credential/sdjwt/present_proof.feature b/tests/integration-tests/src/test/resources/features/credential/sdjwt/present_proof.feature new file mode 100644 index 0000000000..e5d273bf37 --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/credential/sdjwt/present_proof.feature @@ -0,0 +1,35 @@ +@sdjwt @proof +Feature: Present SD-JWT Proof Protocol + + Scenario Outline: Holder presents sd-jwt proof to + Given and Holder have an existing connection + And Holder has a sd-jwt issued credential from Issuer + When sends a request for sd-jwt proof presentation to Holder requesting [firstName] claims + And Holder receives the presentation proof request + And Holder makes the sd-jwt presentation of the proof disclosing [firstName] claims + Then has the proof verified + Examples: + | verifier | + | Verifier | + | Issuer | + + Scenario Outline: Holder presents a bound sd-jwt proof to + Given and Holder have an existing connection + And Holder has a bound sd-jwt issued credential from Issuer + When sends a request for sd-jwt proof presentation to Holder requesting [firstName] claims + And Holder receives the presentation proof request + And Holder makes the sd-jwt presentation of the proof disclosing [firstName] claims + Then has the sd-jwt proof verified + Examples: + | verifier | + | Verifier | + | Issuer | + +# Scenario: Holder presents sd-jwt proof with different claims from requested +# Given Verifier and Holder have an existing connection +# And Holder has a bound sd-jwt issued credential from Issuer +# When Verifier sends a request for sd-jwt proof presentation to Holder requesting [firstName] claims +# And Holder receives the presentation proof request +# And Holder makes the sd-jwt presentation of the proof disclosing [lastName] claims +# Then Verifier sees the proof returned verification failed + diff --git a/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_published_did.feature b/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_published_did.feature deleted file mode 100644 index e54fd17c2d..0000000000 --- a/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_published_did.feature +++ /dev/null @@ -1,34 +0,0 @@ -@RFC0453 @AIP20 @credentials -Feature: Issue JWT Credentials with published DID - - Background: - Given Issuer and Holder have an existing connection - And Issuer has a published DID for JWT - And Issuer has published STUDENT_SCHEMA schema - And Holder has an unpublished DID for JWT - - Scenario: Issuing credential with published PRISM DID - When Issuer offers a credential to Holder with "short" form DID - And Holder receives the credential offer - And Holder accepts credential offer for JWT - And Issuer issues the credential - Then Holder receives the issued credential - - Scenario: Issuing anoncred with published PRISM DID - Given Issuer has an anoncred schema definition - When Issuer offers anoncred to Holder - And Holder receives the credential offer - And Holder accepts credential offer for anoncred - And Issuer issues the credential - Then Holder receives the issued credential - - Scenario: Issuing credential with an existing schema - When Issuer offers a credential to Holder with "short" form using STUDENT_SCHEMA schema - And Holder receives the credential offer - And Holder accepts credential offer for JWT - And Issuer issues the credential - Then Holder receives the issued credential - - Scenario: Issuing credential with wrong claim structure for schema - When Issuer offers a credential to Holder with "short" form DID with wrong claims structure using STUDENT_SCHEMA schema - Then Issuer should see that credential issuance has failed diff --git a/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_unpublished_did.feature b/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_unpublished_did.feature deleted file mode 100644 index a658a0c453..0000000000 --- a/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_unpublished_did.feature +++ /dev/null @@ -1,14 +0,0 @@ -@RFC0453 @AIP20 @credentials -Feature: Issue JWT Credentials with unpublished DID - - Background: - Given Issuer and Holder have an existing connection - And Issuer has an unpublished DID for JWT - And Holder has an unpublished DID for JWT - - Scenario: Issuing credential with unpublished PRISM DID - And Issuer offers a credential to Holder with "long" form DID - And Holder receives the credential offer - And Holder accepts credential offer for JWT - And Issuer issues the credential - Then Holder receives the issued credential 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 new file mode 100644 index 0000000000..2f30658ad8 --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature @@ -0,0 +1,22 @@ +@oid4vci +Feature: Issue JWT Credentials using OID4VCI authorization code flow + +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 + 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 + 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 + And Holder presents the access token with JWT proof on CredentialEndpoint + Then Holder sees credential issued successfully from CredentialEndpoint diff --git a/tests/integration-tests/src/test/resources/logback-test.xml b/tests/integration-tests/src/test/resources/logback-test.xml index 357d669652..b28cee7002 100644 --- a/tests/integration-tests/src/test/resources/logback-test.xml +++ b/tests/integration-tests/src/test/resources/logback-test.xml @@ -13,4 +13,6 @@ + + diff --git a/version.sbt b/version.sbt index 2e05788ede..163d9c0802 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -ThisBuild / version := "1.37.0-SNAPSHOT" +ThisBuild / version := "1.38.0-SNAPSHOT"