diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceImpl.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceImpl.scala index 9a3578cb6f..e9d7ee5508 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceImpl.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceImpl.scala @@ -5,6 +5,7 @@ import io.iohk.atala.agent.walletapi.model.* import io.iohk.atala.agent.walletapi.model.error.{*, given} import io.iohk.atala.agent.walletapi.service.ManagedDIDService.DEFAULT_MASTER_KEY_ID import io.iohk.atala.agent.walletapi.service.handler.{DIDCreateHandler, DIDUpdateHandler, PublicationHandler} +import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage import io.iohk.atala.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage} import io.iohk.atala.agent.walletapi.util.* import io.iohk.atala.castor.core.model.did.* @@ -28,19 +29,19 @@ class ManagedDIDServiceImpl private[walletapi] ( didOpValidator: DIDOperationValidator, private[walletapi] val secretStorage: DIDSecretStorage, override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, + walletSecretStorage: WalletSecretStorage, apollo: Apollo, - seed: WalletSeed, // TODO: support dynamic seed lookup createDIDSem: Semaphore ) extends ManagedDIDService { private val AGREEMENT_KEY_ID = "agreement" private val AUTHENTICATION_KEY_ID = "authentication" - private val keyResolver = KeyResolver(apollo, nonSecretStorage)(seed) - + // TODO: implement seed caching & TTL in dispatching layer + private val keyResolver = KeyResolver(apollo, nonSecretStorage, walletSecretStorage) private val publicationHandler = PublicationHandler(didService, keyResolver)(DEFAULT_MASTER_KEY_ID) - private val didCreateHandler = DIDCreateHandler(apollo, nonSecretStorage)(seed, DEFAULT_MASTER_KEY_ID) - private val didUpdateHandler = DIDUpdateHandler(apollo, nonSecretStorage, publicationHandler)(seed) + private val didCreateHandler = DIDCreateHandler(apollo, nonSecretStorage, walletSecretStorage)(DEFAULT_MASTER_KEY_ID) + private val didUpdateHandler = DIDUpdateHandler(apollo, nonSecretStorage, walletSecretStorage, publicationHandler) def syncManagedDIDState: ZIO[WalletAccessContext, GetManagedDIDError, Unit] = nonSecretStorage .listManagedDID(offset = None, limit = None) @@ -370,7 +371,7 @@ class ManagedDIDServiceImpl private[walletapi] ( object ManagedDIDServiceImpl { val layer: RLayer[ - DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & Apollo & SeedResolver, + DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & WalletSecretStorage & Apollo, ManagedDIDService ] = { ZLayer.fromZIO { @@ -379,16 +380,16 @@ object ManagedDIDServiceImpl { didOpValidator <- ZIO.service[DIDOperationValidator] secretStorage <- ZIO.service[DIDSecretStorage] nonSecretStorage <- ZIO.service[DIDNonSecretStorage] + walletSecretStorage <- ZIO.service[WalletSecretStorage] apollo <- ZIO.service[Apollo] - seed <- ZIO.serviceWithZIO[SeedResolver](_.resolve) createDIDSem <- Semaphore.make(1) } yield ManagedDIDServiceImpl( didService, didOpValidator, secretStorage, nonSecretStorage, + walletSecretStorage, apollo, - seed, createDIDSem ) } diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala index 4f877c85ca..c8f1b7c64b 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala @@ -2,10 +2,9 @@ package io.iohk.atala.agent.walletapi.service import io.iohk.atala.agent.walletapi.crypto.Apollo import io.iohk.atala.agent.walletapi.model.ManagedDIDDetail -import io.iohk.atala.agent.walletapi.model.WalletSeed import io.iohk.atala.agent.walletapi.model.error.CommonWalletStorageError +import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage import io.iohk.atala.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage} -import io.iohk.atala.agent.walletapi.util.SeedResolver import io.iohk.atala.castor.core.model.did.CanonicalPrismDID import io.iohk.atala.castor.core.model.error import io.iohk.atala.castor.core.model.error.DIDOperationError @@ -20,8 +19,8 @@ class ManagedDIDServiceWithEventNotificationImpl( didOpValidator: DIDOperationValidator, override private[walletapi] val secretStorage: DIDSecretStorage, override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, + walletSecretStorage: WalletSecretStorage, apollo: Apollo, - seed: WalletSeed, createDIDSem: Semaphore, eventNotificationService: EventNotificationService ) extends ManagedDIDServiceImpl( @@ -29,8 +28,8 @@ class ManagedDIDServiceWithEventNotificationImpl( didOpValidator, secretStorage, nonSecretStorage, + walletSecretStorage, apollo, - seed, createDIDSem ) { @@ -59,7 +58,7 @@ class ManagedDIDServiceWithEventNotificationImpl( object ManagedDIDServiceWithEventNotificationImpl { val layer: RLayer[ - DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & Apollo & SeedResolver & + DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & WalletSecretStorage & Apollo & EventNotificationService, ManagedDIDService ] = ZLayer.fromZIO { @@ -68,8 +67,8 @@ object ManagedDIDServiceWithEventNotificationImpl { didOpValidator <- ZIO.service[DIDOperationValidator] secretStorage <- ZIO.service[DIDSecretStorage] nonSecretStorage <- ZIO.service[DIDNonSecretStorage] + walletSecretStorage <- ZIO.service[WalletSecretStorage] apollo <- ZIO.service[Apollo] - seed <- ZIO.serviceWithZIO[SeedResolver](_.resolve) createDIDSem <- Semaphore.make(1) eventNotificationService <- ZIO.service[EventNotificationService] } yield ManagedDIDServiceWithEventNotificationImpl( @@ -77,8 +76,8 @@ object ManagedDIDServiceWithEventNotificationImpl { didOpValidator, secretStorage, nonSecretStorage, + walletSecretStorage, apollo, - seed, createDIDSem, eventNotificationService ) diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceImpl.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceImpl.scala index b6bfa47084..133aad3824 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceImpl.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceImpl.scala @@ -16,7 +16,12 @@ class WalletManagementServiceImpl( override def createWallet(seed: Option[WalletSeed]): Task[WalletId] = for { - seed <- seed.fold(apollo.ecKeyFactory.randomBip32Seed().map(_._1).map(WalletSeed.fromByteArray))(ZIO.succeed) + seed <- seed.fold( + apollo.ecKeyFactory + .randomBip32Seed() + .map(_._1) + .map(WalletSeed.fromByteArray) + )(ZIO.succeed) walletId <- nonSecretStorage.createWallet _ <- secretStorage .setWalletSeed(seed) diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDCreateHandler.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDCreateHandler.scala index 3ccffff1e1..9b03f015f2 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDCreateHandler.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDCreateHandler.scala @@ -8,6 +8,7 @@ import io.iohk.atala.agent.walletapi.model.PublicationState import io.iohk.atala.agent.walletapi.model.WalletSeed import io.iohk.atala.agent.walletapi.model.error.CreateManagedDIDError import io.iohk.atala.agent.walletapi.storage.DIDNonSecretStorage +import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage import io.iohk.atala.agent.walletapi.util.OperationFactory import io.iohk.atala.castor.core.model.did.PrismDIDOperation import io.iohk.atala.shared.models.WalletAccessContext @@ -15,9 +16,9 @@ import zio.* private[walletapi] class DIDCreateHandler( apollo: Apollo, - nonSecretStorage: DIDNonSecretStorage + nonSecretStorage: DIDNonSecretStorage, + walletSecretStorage: WalletSecretStorage, )( - seed: WalletSeed, masterKeyId: String ) { def materialize( @@ -25,6 +26,10 @@ private[walletapi] class DIDCreateHandler( ): ZIO[WalletAccessContext, CreateManagedDIDError, DIDCreateMaterial] = { val operationFactory = OperationFactory(apollo) for { + walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) + seed <- walletSecretStorage.getWalletSeed + .someOrElseZIO(ZIO.dieMessage(s"Wallet seed for wallet $walletId does not exist")) + .mapError(CreateManagedDIDError.WalletStorageError.apply) didIndex <- nonSecretStorage .getMaxDIDIndex() .mapBoth( diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDUpdateHandler.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDUpdateHandler.scala index adfe92d8b3..580137150f 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDUpdateHandler.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDUpdateHandler.scala @@ -10,6 +10,7 @@ import io.iohk.atala.agent.walletapi.model.WalletSeed import io.iohk.atala.agent.walletapi.model.error.UpdateManagedDIDError import io.iohk.atala.agent.walletapi.model.error.{*, given} import io.iohk.atala.agent.walletapi.storage.DIDNonSecretStorage +import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage import io.iohk.atala.agent.walletapi.util.OperationFactory import io.iohk.atala.castor.core.model.did.PrismDIDOperation import io.iohk.atala.castor.core.model.did.PrismDIDOperation.Update @@ -22,9 +23,8 @@ import zio.* private[walletapi] class DIDUpdateHandler( apollo: Apollo, nonSecretStorage: DIDNonSecretStorage, + walletSecretStorage: WalletSecretStorage, publicationHandler: PublicationHandler -)( - seed: WalletSeed ) { def materialize( state: ManagedDIDState, @@ -36,6 +36,10 @@ private[walletapi] class DIDUpdateHandler( state.keyMode match { case KeyManagementMode.HD => for { + walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) + seed <- walletSecretStorage.getWalletSeed + .someOrElseZIO(ZIO.dieMessage(s"Wallet seed for wallet $walletId does not exist")) + .mapError(UpdateManagedDIDError.WalletStorageError.apply) keyCounter <- nonSecretStorage .getHdKeyCounter(did) .mapError(UpdateManagedDIDError.WalletStorageError.apply) diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcWalletSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcWalletSecretStorage.scala index 6f70de153a..bb0d39ea8b 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcWalletSecretStorage.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcWalletSecretStorage.scala @@ -36,17 +36,16 @@ class JdbcWalletSecretStorage(xa: Transactor[ContextAwareTask]) extends WalletSe } override def getWalletSeed: RIO[WalletAccessContext, Option[WalletSeed]] = { - val cxnIO = (walletId: WalletId) => + val cxnIO = sql""" | SELECT seed | FROM public.wallet_seed - | WHERE wallet_id = $walletId """.stripMargin .query[Array[Byte]] .option - ZIO - .serviceWithZIO[WalletAccessContext](ctx => cxnIO(ctx.walletId).transactWallet(xa)) + cxnIO + .transactWallet(xa) .map(_.map(WalletSeed.fromByteArray)) } diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/KeyResolver.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/KeyResolver.scala index a36a18048e..01ee234300 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/KeyResolver.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/KeyResolver.scala @@ -6,14 +6,13 @@ import io.iohk.atala.agent.walletapi.model.KeyManagementMode import io.iohk.atala.agent.walletapi.model.ManagedDIDState import io.iohk.atala.agent.walletapi.model.WalletSeed import io.iohk.atala.agent.walletapi.storage.DIDNonSecretStorage +import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage import io.iohk.atala.castor.core.model.did.EllipticCurve import io.iohk.atala.castor.core.model.did.PrismDID import io.iohk.atala.shared.models.WalletAccessContext import zio.* -class KeyResolver(apollo: Apollo, nonSecretStorage: DIDNonSecretStorage)( - seed: WalletSeed -) { +class KeyResolver(apollo: Apollo, nonSecretStorage: DIDNonSecretStorage, walletSecretStorage: WalletSecretStorage) { def getKey(state: ManagedDIDState, keyId: String): RIO[WalletAccessContext, Option[ECKeyPair]] = { val did = state.createOperation.did getKey(did, state.keyMode, keyId) @@ -26,14 +25,19 @@ class KeyResolver(apollo: Apollo, nonSecretStorage: DIDNonSecretStorage)( } private def resolveHdKey(did: PrismDID, keyId: String): RIO[WalletAccessContext, Option[ECKeyPair]] = { - nonSecretStorage - .getHdKeyPath(did, keyId) - .flatMap { - case None => ZIO.none - case Some(path) => - apollo.ecKeyFactory - .deriveKeyPair(EllipticCurve.SECP256K1, seed.toByteArray)(path.derivationPath: _*) - .asSome + for { + maybeSeed <- walletSecretStorage.getWalletSeed + maybeKeyPair <- maybeSeed.fold(ZIO.none) { seed => + nonSecretStorage + .getHdKeyPath(did, keyId) + .flatMap { + case None => ZIO.none + case Some(path) => + apollo.ecKeyFactory + .deriveKeyPair(EllipticCurve.SECP256K1, seed.toByteArray)(path.derivationPath: _*) + .asSome + } } + } yield maybeKeyPair } } diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala index 1fb869703e..b7b6f4a630 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala @@ -15,7 +15,6 @@ import io.iohk.atala.agent.walletapi.storage.DIDSecretStorage import io.iohk.atala.agent.walletapi.storage.StorageSpecHelper import io.iohk.atala.agent.walletapi.storage.WalletNonSecretStorage import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage -import io.iohk.atala.agent.walletapi.util.SeedResolver import io.iohk.atala.agent.walletapi.vault.VaultDIDSecretStorage import io.iohk.atala.agent.walletapi.vault.VaultWalletSecretStorage import io.iohk.atala.castor.core.model.did.InternalKeyPurpose @@ -44,6 +43,7 @@ import scala.collection.immutable.ArraySeq import zio.* import zio.test.* import zio.test.Assertion.* +import io.iohk.atala.agent.walletapi.model.error.DIDSecretStorageError object ManagedDIDServiceSpec extends ZIOSpecDefault, @@ -110,7 +110,6 @@ object ManagedDIDServiceSpec DIDOperationValidator.layer(), JdbcDIDNonSecretStorage.layer, JdbcWalletNonSecretStorage.layer, - SeedResolver.layer(isDevMode = true), transactorLayer, testDIDServiceLayer, apolloLayer @@ -156,11 +155,12 @@ object ManagedDIDServiceSpec override def spec = { def testSuite(name: String) = suite(name)( - publishStoredDIDSpec, - createAndStoreDIDSpec, - updateManagedDIDSpec, - deactivateManagedDIDSpec - ).globalWallet + publishStoredDIDSpec.globalWallet, + createAndStoreDIDSpec.globalWallet, + updateManagedDIDSpec.globalWallet, + deactivateManagedDIDSpec.globalWallet, + multitenantSpec + ) @@ TestAspect.before(DBTestUtils.runMigrationAgentDB) @@ TestAspect.sequential @@ -471,4 +471,81 @@ object ManagedDIDServiceSpec } ) + private val multitenantSpec = suite("multi-tenant managed DID")( + test("do not see Prism DID outside of the wallet") { + val template = generateDIDTemplate() + for { + walletSvc <- ZIO.service[WalletManagementService] + walletId1 <- walletSvc.createWallet() + walletId2 <- walletSvc.createWallet() + ctx1 = ZLayer.succeed(WalletAccessContext(walletId1)) + ctx2 = ZLayer.succeed(WalletAccessContext(walletId2)) + svc <- ZIO.service[ManagedDIDService] + dids1 <- ZIO.foreach(1 to 3)(_ => svc.createAndStoreDID(template).map(_.asCanonical)).provide(ctx1) + dids2 <- ZIO.foreach(1 to 3)(_ => svc.createAndStoreDID(template).map(_.asCanonical)).provide(ctx2) + ownWalletDids1 <- svc.listManagedDIDPage(0, 1000).map(_._1.map(_.did)).provide(ctx1) + ownWalletDids2 <- svc.listManagedDIDPage(0, 1000).map(_._1.map(_.did)).provide(ctx2) + crossWalletDids1 <- ZIO.foreach(dids1)(did => svc.getManagedDIDState(did)).provide(ctx2) + crossWalletDids2 <- ZIO.foreach(dids2)(did => svc.getManagedDIDState(did)).provide(ctx1) + } yield assert(dids1)(hasSameElements(ownWalletDids1)) && + assert(dids2)(hasSameElements(ownWalletDids2)) && + assert(crossWalletDids1)(forall(isNone)) && + assert(crossWalletDids2)(forall(isNone)) + }, + test("do not see Peer DID outside of the wallet") { + for { + walletSvc <- ZIO.service[WalletManagementService] + walletId1 <- walletSvc.createWallet() + walletId2 <- walletSvc.createWallet() + ctx1 = ZLayer.succeed(WalletAccessContext(walletId1)) + ctx2 = ZLayer.succeed(WalletAccessContext(walletId2)) + svc <- ZIO.service[ManagedDIDService] + dids1 <- ZIO.foreach(1 to 3)(_ => svc.createAndStorePeerDID("http://example.com")).provide(ctx1) + dids2 <- ZIO.foreach(1 to 3)(_ => svc.createAndStorePeerDID("http://example.com")).provide(ctx2) + ownWalletDids1 <- ZIO.foreach(dids1)(d => svc.getPeerDID(d.did).exit).provide(ctx1) + ownWalletDids2 <- ZIO.foreach(dids2)(d => svc.getPeerDID(d.did).exit).provide(ctx2) + crossWalletDids1 <- ZIO.foreach(dids1)(d => svc.getPeerDID(d.did).exit).provide(ctx2) + crossWalletDids2 <- ZIO.foreach(dids2)(d => svc.getPeerDID(d.did).exit).provide(ctx1) + } yield assert(ownWalletDids1)(forall(succeeds(anything))) && + assert(ownWalletDids2)(forall(succeeds(anything))) && + assert(crossWalletDids1)(forall(failsWithA[DIDSecretStorageError.KeyNotFoundError])) && + assert(crossWalletDids2)(forall(failsWithA[DIDSecretStorageError.KeyNotFoundError])) + }, + test("increment DID index based on count only on its wallet") { + val template = generateDIDTemplate() + for { + walletSvc <- ZIO.service[WalletManagementService] + walletId1 <- walletSvc.createWallet() + walletId2 <- walletSvc.createWallet() + ctx1 = ZLayer.succeed(WalletAccessContext(walletId1)) + ctx2 = ZLayer.succeed(WalletAccessContext(walletId2)) + svc <- ZIO.service[ManagedDIDService] + wallet1Counter1 <- svc.nonSecretStorage.getMaxDIDIndex().provide(ctx1) + wallet2Counter1 <- svc.nonSecretStorage.getMaxDIDIndex().provide(ctx2) + _ <- svc.createAndStoreDID(template).provide(ctx1) + wallet1Counter2 <- svc.nonSecretStorage.getMaxDIDIndex().provide(ctx1) + wallet2Counter2 <- svc.nonSecretStorage.getMaxDIDIndex().provide(ctx2) + _ <- svc.createAndStoreDID(template).provide(ctx1) + wallet1Counter3 <- svc.nonSecretStorage.getMaxDIDIndex().provide(ctx1) + wallet2Counter3 <- svc.nonSecretStorage.getMaxDIDIndex().provide(ctx2) + _ <- svc.createAndStoreDID(template).provide(ctx2) + wallet1Counter4 <- svc.nonSecretStorage.getMaxDIDIndex().provide(ctx1) + wallet2Counter4 <- svc.nonSecretStorage.getMaxDIDIndex().provide(ctx2) + } yield { + // initial counter + assert(wallet1Counter1)(isNone) && + assert(wallet2Counter1)(isNone) && + // add DID to wallet 1 + assert(wallet1Counter2)(isSome(equalTo(0))) && + assert(wallet2Counter2)(isNone) && + // add DID to wallet 1 + assert(wallet1Counter3)(isSome(equalTo(1))) && + assert(wallet2Counter3)(isNone) && + // add DID to wallet 2 + assert(wallet1Counter4)(isSome(equalTo(1))) && + assert(wallet2Counter4)(isSome(equalTo(0))) + } + } + ) + } diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala new file mode 100644 index 0000000000..2e3badad88 --- /dev/null +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala @@ -0,0 +1,119 @@ +package io.iohk.atala.agent.walletapi.service + +import io.iohk.atala.agent.walletapi.crypto.ApolloSpecHelper +import io.iohk.atala.agent.walletapi.model.WalletSeed +import io.iohk.atala.agent.walletapi.sql.JdbcWalletNonSecretStorage +import io.iohk.atala.agent.walletapi.sql.JdbcWalletSecretStorage +import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage +import io.iohk.atala.agent.walletapi.vault.VaultWalletSecretStorage +import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.test.container.DBTestUtils +import io.iohk.atala.test.container.PostgresTestContainerSupport +import io.iohk.atala.test.container.VaultTestContainerSupport +import zio.* +import zio.test.* +import zio.test.Assertion.* + +object WalletManagementServiceSpec + extends ZIOSpecDefault, + PostgresTestContainerSupport, + ApolloSpecHelper, + VaultTestContainerSupport { + + override def spec = { + def testSuite(name: String) = + suite(name)( + createWalletSpec + ) @@ TestAspect.before(DBTestUtils.runMigrationAgentDB) @@ TestAspect.sequential + + val suite1 = testSuite("jdbc as secret storage") + .provide( + WalletManagementServiceImpl.layer, + JdbcWalletNonSecretStorage.layer, + JdbcWalletSecretStorage.layer, + transactorLayer, + pgContainerLayer, + apolloLayer + ) + + val suite2 = testSuite("vault as secret storage") + .provide( + WalletManagementServiceImpl.layer, + JdbcWalletNonSecretStorage.layer, + VaultWalletSecretStorage.layer, + transactorLayer, + pgContainerLayer, + apolloLayer, + vaultKvClientLayer + ) + + suite("WalletManagementService")(suite1, suite2) + } + + private def createWalletSpec = suite("createWallet")( + test("initialize with no wallet") { + for { + svc <- ZIO.service[WalletManagementService] + walletIds <- svc.listWallets + } yield assert(walletIds)(isEmpty) + }, + test("create a wallet with random seed") { + for { + svc <- ZIO.service[WalletManagementService] + secretStorage <- ZIO.service[WalletSecretStorage] + createdWallet <- svc.createWallet() + listedWallets <- svc.listWallets + seed <- secretStorage.getWalletSeed.provide(ZLayer.succeed(WalletAccessContext(createdWallet))) + } yield assert(listedWallets)(hasSameElements(Seq(createdWallet))) && + assert(seed)(isSome) + }, + test("create multiple wallets with random seed") { + for { + svc <- ZIO.service[WalletManagementService] + secretStorage <- ZIO.service[WalletSecretStorage] + createdWallets <- ZIO.foreach(1 to 10)(_ => svc.createWallet()) + listedWallets <- svc.listWallets + seeds <- ZIO.foreach(listedWallets) { walletId => + secretStorage.getWalletSeed.provide(ZLayer.succeed(WalletAccessContext(walletId))) + } + } yield assert(createdWallets)(hasSameElements(listedWallets)) && + assert(seeds)(forall(isSome)) + }, + test("create a wallet with provided seed") { + for { + svc <- ZIO.service[WalletManagementService] + secretStorage <- ZIO.service[WalletSecretStorage] + seed1 = WalletSeed.fromByteArray(Array.fill[Byte](64)(0)) + createdWallet <- svc.createWallet(Some(seed1)) + listedWallets <- svc.listWallets + seed2 <- secretStorage.getWalletSeed.provide(ZLayer.succeed(WalletAccessContext(createdWallet))) + } yield assert(listedWallets)(hasSameElements(Seq(createdWallet))) && + assert(seed2)(isSome(equalTo(seed1))) + }, + test("create multiple wallets with provided seed") { + for { + svc <- ZIO.service[WalletManagementService] + secretStorage <- ZIO.service[WalletSecretStorage] + seeds1 = (1 to 10).map(i => WalletSeed.fromByteArray(Array.fill[Byte](64)(i.toByte))) + createdWallets <- ZIO.foreach(seeds1) { seed => svc.createWallet(Some(seed)) } + listedWallets <- svc.listWallets + seeds2 <- ZIO.foreach(listedWallets) { walletId => + secretStorage.getWalletSeed.provide(ZLayer.succeed(WalletAccessContext(walletId))) + } + } yield assert(createdWallets)(hasSameElements(listedWallets)) && + assert(seeds2.flatten)(hasSameElements(seeds1)) + }, + test("create multiple wallets with same seed must not fail") { + for { + svc <- ZIO.service[WalletManagementService] + seed = WalletSeed.fromByteArray(Array.fill[Byte](64)(0)) + _ <- svc.createWallet(Some(seed)) + _ <- svc.createWallet(Some(seed)) + _ <- svc.createWallet(Some(seed)) + wallets <- svc.listWallets + } yield assert(wallets)(hasSize(equalTo(3))) && + assert(wallets)(isDistinct) + } + ) + +} diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/WalletSecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/WalletSecretStorageSpec.scala new file mode 100644 index 0000000000..e337af65d4 --- /dev/null +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/WalletSecretStorageSpec.scala @@ -0,0 +1,76 @@ +package io.iohk.atala.agent.walletapi.storage + +import io.iohk.atala.agent.walletapi.model.WalletSeed +import io.iohk.atala.agent.walletapi.sql.JdbcWalletNonSecretStorage +import io.iohk.atala.agent.walletapi.sql.JdbcWalletSecretStorage +import io.iohk.atala.agent.walletapi.vault.VaultWalletSecretStorage +import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.test.container.DBTestUtils +import io.iohk.atala.test.container.PostgresTestContainerSupport +import zio.* +import zio.test.* +import zio.test.Assertion.* +import io.iohk.atala.test.container.VaultTestContainerSupport + +object WalletSecretStorageSpec extends ZIOSpecDefault, PostgresTestContainerSupport, VaultTestContainerSupport { + + override def spec = { + def testSuite(name: String) = suite(name)( + setWalletSeedSpec + ) @@ TestAspect.before(DBTestUtils.runMigrationAgentDB) + + val suite1 = testSuite("jdbc as storage") + .provide( + JdbcWalletSecretStorage.layer, + JdbcWalletNonSecretStorage.layer, + transactorLayer, + pgContainerLayer + ) + + val suite2 = testSuite("jdbc as storage") + .provide( + VaultWalletSecretStorage.layer, + JdbcWalletNonSecretStorage.layer, + transactorLayer, + pgContainerLayer, + vaultKvClientLayer + ) + + suite("WalletSecretStorage")(suite1, suite2) + } + + private def setWalletSeedSpec = suite("setWalletSeed")( + test("set seed on a new wallet") { + for { + storage <- ZIO.service[WalletSecretStorage] + walletId <- ZIO.serviceWithZIO[WalletNonSecretStorage](_.createWallet) + walletAccessCtx = ZLayer.succeed(WalletAccessContext(walletId)) + seed = WalletSeed.fromByteArray(Array.fill[Byte](64)(0)) + seedBefore <- storage.getWalletSeed.provide(walletAccessCtx) + _ <- storage.setWalletSeed(seed).provide(walletAccessCtx) + seedAfter <- storage.getWalletSeed.provide(walletAccessCtx) + } yield assert(seedBefore)(isNone) && + assert(seedAfter)(isSome(equalTo(seed))) + }, + test("set seed on multiple wallets") { + for { + storage <- ZIO.service[WalletSecretStorage] + walletIds <- ZIO.foreach(1 to 10) { i => + for { + walletId <- ZIO.serviceWithZIO[WalletNonSecretStorage](_.createWallet) + seed = WalletSeed.fromByteArray(Array.fill[Byte](64)(i.toByte)) + walletAccessCtx = ZLayer.succeed(WalletAccessContext(walletId)) + _ <- storage.setWalletSeed(seed).provideSomeLayer(walletAccessCtx) + } yield walletId + } + seeds <- ZIO + .foreach(walletIds) { walletId => + val walletAccessCtx = ZLayer.succeed(WalletAccessContext(walletId)) + storage.getWalletSeed.provideSomeLayer(walletAccessCtx) + } + .map(_.flatten) + } yield assert(seeds.size)(equalTo(10)) && assert(seeds)(isDistinct) + } + ) + +} diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/PostgresTestContainerSupport.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/PostgresTestContainerSupport.scala index a22de5c0cf..6dc5203166 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/PostgresTestContainerSupport.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/PostgresTestContainerSupport.scala @@ -2,14 +2,60 @@ package io.iohk.atala.test.container import com.dimafeng.testcontainers.PostgreSQLContainer import doobie.util.transactor.Transactor -import zio.* import io.iohk.atala.shared.db.ContextAwareTask +import io.iohk.atala.shared.db.TransactorLayer +import zio.* trait PostgresTestContainerSupport { protected val pgContainerLayer: TaskLayer[PostgreSQLContainer] = PostgresLayer.postgresLayer() - protected val transactorLayer: TaskLayer[Transactor[ContextAwareTask]] = - pgContainerLayer >>> PostgresLayer.dbConfigLayer >>> PostgresLayer.transactor + protected val transactorLayer: TaskLayer[Transactor[ContextAwareTask]] = { + import doobie.* + import doobie.implicits.* + import zio.interop.catz.* + + val appUser = "test-application-user" + val appPassword = "password" + + val createAppUser = (xa: Transactor[Task]) => + doobie.free.connection.createStatement + .map { stm => + stm.execute(s"""CREATE USER "$appUser" WITH PASSWORD '$appPassword';""") + stm.execute( + s"""ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "$appUser";""" + ) + stm + } + .transact(xa) + .unit + + val superUserTransactor = ZLayer.makeSome[PostgreSQLContainer, Transactor[Task]]( + TransactorLayer.task, + PostgresLayer.dbConfigLayer, + ) + + val appUserTransactor = ZLayer.makeSome[PostgreSQLContainer, Transactor[ContextAwareTask]]( + TransactorLayer.contextAwareTask, + PostgresLayer.dbConfigLayer.map(conf => + ZEnvironment( + conf.get.copy( + username = appUser, + password = appPassword + ) + ) + ), + ) + + val initializedTransactor = ZLayer.fromZIO { + for { + _ <- ZIO + .serviceWithZIO[Transactor[Task]](createAppUser) + .provideSomeLayer(superUserTransactor) + } yield appUserTransactor + }.flatten + + pgContainerLayer >>> initializedTransactor + } }