From 3d4aacdd9a7f66f2f656d3c31b3f8202cc37c51b Mon Sep 17 00:00:00 2001 From: patlo-iog Date: Wed, 20 Sep 2023 14:19:26 +0700 Subject: [PATCH] fix(prism-agent): introduce generic secret store for CD (#727) Signed-off-by: Pat Losoponkul --- .../CredentialDefinitionServiceImpl.scala | 18 +- ...redentialDefinitionServiceSpecHelper.scala | 4 +- .../io/iohk/atala/agent/server/Main.scala | 2 - .../io/iohk/atala/agent/server/Modules.scala | 15 +- .../apikey/ApiKeyAuthenticatorImpl.scala | 1 + .../CredentialDefinitionBasicSpec.scala | 23 +-- .../CredentialDefinitionTestTools.scala | 8 +- .../sql/agent/V12__generic_secret.sql | 15 ++ .../memory/DIDSecretStorageInMemory.scala | 23 +-- .../memory/GenericSecretStorageInMemory.scala | 60 +++++++ .../service/ManagedDIDServiceImpl.scala | 8 +- ...dDIDServiceWithEventNotificationImpl.scala | 8 +- .../walletapi/sql/JdbcDIDSecretStorage.scala | 35 +--- .../sql/JdbcGenericSecretStorage.scala | 61 +++++++ .../atala/agent/walletapi/sql/package.scala | 13 ++ .../storage/DIDKeySecretStorage.scala | 16 -- .../storage/DIDKeySecretStorageImpl.scala | 33 ---- .../walletapi/storage/DIDSecretStorage.scala | 9 +- .../storage/GenericSecretStorage.scala | 29 ++++ .../vault/VaultDIDSecretStorage.scala | 12 +- .../vault/VaultGenericSecretStorage.scala | 43 +++++ .../atala/agent/walletapi/vault/package.scala | 40 +++-- .../service/ManagedDIDServiceSpec.scala | 1 - ...eSpec.scala => DIDSecretStorageSpec.scala} | 63 ++++++- .../storage/GenericSecretStorageSpec.scala | 163 ++++++++++++++++++ .../test/containers/PostgresLayer.scala | 2 +- .../containers/PostgresTestContainer.scala | 5 +- 27 files changed, 536 insertions(+), 174 deletions(-) create mode 100644 prism-agent/service/wallet-api/src/main/resources/sql/agent/V12__generic_secret.sql create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/memory/GenericSecretStorageInMemory.scala create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcGenericSecretStorage.scala delete mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorage.scala delete mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorageImpl.scala create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorage.scala create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/VaultGenericSecretStorage.scala rename prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/{DIDKeySecretStorageSpec.scala => DIDSecretStorageSpec.scala} (62%) create mode 100644 prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceImpl.scala index 9d90e837d3..0fd9309467 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceImpl.scala @@ -1,8 +1,8 @@ package io.iohk.atala.pollux.core.service import io.iohk.atala.agent.walletapi.storage -import io.iohk.atala.agent.walletapi.storage.{DIDSecret, DIDSecretStorage} -import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage +import io.iohk.atala.agent.walletapi.storage.CredentialDefinitionSecret import io.iohk.atala.pollux.anoncreds.{AnoncredLib, SchemaDef} import io.iohk.atala.pollux.core.model.error.CredentialSchemaError import io.iohk.atala.pollux.core.model.error.CredentialSchemaError.URISyntaxError @@ -27,11 +27,10 @@ import java.util.UUID import scala.util.Try class CredentialDefinitionServiceImpl( - didSecretStorage: DIDSecretStorage, + genericSecretStorage: GenericSecretStorage, credentialDefinitionRepository: CredentialDefinitionRepository, uriDereferencer: URIDereferencer ) extends CredentialDefinitionService { - private val KEY_ID = "anoncred-credential-definition-private-key" override def create(in: CredentialDefinition.Input): Result[CredentialDefinition] = { for { @@ -80,11 +79,10 @@ class CredentialDefinitionServiceImpl( proofKeyCredentialDefinitionJson ) createdCredentialDefinition <- credentialDefinitionRepository.create(cd) - _ <- - didSecretStorage.insertKey( - DidId(in.author), - s"$KEY_ID/${createdCredentialDefinition.guid}", - DIDSecret(privateCredentialDefinitionJson, PrivateCredentialDefinitionSchemaSerDesV1.version) + _ <- genericSecretStorage + .set( + createdCredentialDefinition.guid, + CredentialDefinitionSecret(privateCredentialDefinitionJson) ) } yield createdCredentialDefinition }.mapError { @@ -123,7 +121,7 @@ class CredentialDefinitionServiceImpl( object CredentialDefinitionServiceImpl { val layer: URLayer[ - DIDSecretStorage & CredentialDefinitionRepository & URIDereferencer, + GenericSecretStorage & CredentialDefinitionRepository & URIDereferencer, CredentialDefinitionService ] = ZLayer.fromFunction(CredentialDefinitionServiceImpl(_, _, _)) diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala index cd0f7e937a..723007a49f 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala @@ -1,6 +1,6 @@ package io.iohk.atala.pollux.core.service -import io.iohk.atala.agent.walletapi.memory.DIDSecretStorageInMemory +import io.iohk.atala.agent.walletapi.memory.GenericSecretStorageInMemory import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.schema.CredentialDefinition import io.iohk.atala.pollux.core.repository.CredentialDefinitionRepositoryInMemory @@ -15,7 +15,7 @@ trait CredentialDefinitionServiceSpecHelper { protected val defaultWalletLayer = ZLayer.succeed(WalletAccessContext(WalletId.default)) protected val credentialDefinitionServiceLayer = - DIDSecretStorageInMemory.layer ++ CredentialDefinitionRepositoryInMemory.layer ++ ResourceURIDereferencerImpl.layer >>> + GenericSecretStorageInMemory.layer ++ CredentialDefinitionRepositoryInMemory.layer ++ ResourceURIDereferencerImpl.layer >>> CredentialDefinitionServiceImpl.layer ++ defaultWalletLayer val defaultDefinition = diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index a2a091b7b2..64be841df5 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -11,7 +11,6 @@ import io.iohk.atala.agent.walletapi.service.{ WalletManagementServiceImpl } import io.iohk.atala.agent.walletapi.sql.{JdbcDIDNonSecretStorage, JdbcEntityRepository, JdbcWalletNonSecretStorage} -import io.iohk.atala.agent.walletapi.storage.DIDKeySecretStorageImpl import io.iohk.atala.castor.controller.{DIDControllerImpl, DIDRegistrarControllerImpl} import io.iohk.atala.castor.core.service.DIDServiceImpl import io.iohk.atala.castor.core.util.DIDOperationValidator @@ -170,7 +169,6 @@ object MainApp extends ZIOAppDefault { GrpcModule.irisStubLayer, GrpcModule.prismNodeStubLayer, // storage - DIDKeySecretStorageImpl.layer, RepoModule.agentContextAwareTransactorLayer ++ RepoModule.agentTransactorLayer >>> JdbcDIDNonSecretStorage.layer, RepoModule.agentContextAwareTransactorLayer >>> JdbcWalletNonSecretStorage.layer, RepoModule.allSecretStorageLayer, diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala index 0d5b7c3329..2f25123b19 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala @@ -5,10 +5,14 @@ import doobie.util.transactor.Transactor import io.grpc.ManagedChannelBuilder import io.iohk.atala.agent.server.config.AppConfig import io.iohk.atala.agent.walletapi.crypto.Apollo +import io.iohk.atala.agent.walletapi.memory.GenericSecretStorageInMemory import io.iohk.atala.agent.walletapi.memory.{DIDSecretStorageInMemory, WalletSecretStorageInMemory} +import io.iohk.atala.agent.walletapi.sql.JdbcGenericSecretStorage import io.iohk.atala.agent.walletapi.sql.{JdbcDIDSecretStorage, JdbcWalletSecretStorage} +import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage import io.iohk.atala.agent.walletapi.storage.{DIDSecretStorage, WalletSecretStorage} import io.iohk.atala.agent.walletapi.util.SeedResolver +import io.iohk.atala.agent.walletapi.vault.VaultGenericSecretStorage import io.iohk.atala.agent.walletapi.vault.{ VaultDIDSecretStorage, VaultKVClient, @@ -139,7 +143,7 @@ object RepoModule { SystemModule.configLayer >>> vaultClientConfig } - val allSecretStorageLayer: TaskLayer[DIDSecretStorage & WalletSecretStorage] = { + val allSecretStorageLayer: TaskLayer[DIDSecretStorage & WalletSecretStorage & GenericSecretStorage] = { ZLayer.fromZIO { ZIO .service[AppConfig] @@ -148,25 +152,28 @@ object RepoModule { .flatMap { case "vault" => ZIO.succeed( - ZLayer.make[DIDSecretStorage & WalletSecretStorage]( + ZLayer.make[DIDSecretStorage & WalletSecretStorage & GenericSecretStorage]( VaultDIDSecretStorage.layer, VaultWalletSecretStorage.layer, + VaultGenericSecretStorage.layer, vaultClientLayer, ) ) case "postgres" => ZIO.succeed( - ZLayer.make[DIDSecretStorage & WalletSecretStorage]( + ZLayer.make[DIDSecretStorage & WalletSecretStorage & GenericSecretStorage]( JdbcDIDSecretStorage.layer, JdbcWalletSecretStorage.layer, + JdbcGenericSecretStorage.layer, agentContextAwareTransactorLayer, ) ) case "memory" => ZIO.succeed( - ZLayer.make[DIDSecretStorage & WalletSecretStorage]( + ZLayer.make[DIDSecretStorage & WalletSecretStorage & GenericSecretStorage]( DIDSecretStorageInMemory.layer, WalletSecretStorageInMemory.layer, + GenericSecretStorageInMemory.layer ) ) case backend => diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorImpl.scala index 70b4016857..d5fb15e61e 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorImpl.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorImpl.scala @@ -11,6 +11,7 @@ import io.iohk.atala.shared.models.WalletId import java.util.UUID import scala.util.Try +import scala.language.implicitConversions case class ApiKeyAuthenticatorImpl( apiKeyConfig: ApiKeyConfig, diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala index db4c63deae..f91e2c1ba8 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala @@ -1,11 +1,11 @@ package io.iohk.atala.pollux.credentialdefinition import io.iohk.atala.agent.walletapi.model.Entity -import io.iohk.atala.agent.walletapi.storage.DIDSecretStorage +import io.iohk.atala.agent.walletapi.storage.CredentialDefinitionSecret +import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.container.util.MigrationAspects.* import io.iohk.atala.iam.authentication.Authenticator -import io.iohk.atala.mercury.model.DidId import io.iohk.atala.pollux.core.service.serdes.{ PrivateCredentialDefinitionSchemaSerDesV1, ProofKeyCredentialDefinitionSchemaSerDesV1, @@ -116,23 +116,17 @@ object CredentialDefinitionBasicSpec extends ZIOSpecDefault with CredentialDefin fetchedCredentialDefinition.keyCorrectnessProof.toString() ) assertValidKeyCorrectnessProof = assert(maybeValidKeyCorrectnessProof)(Assertion.isTrue) - svc <- ZIO.service[DIDSecretStorage] - maybeDidSecret <- svc - .getKey( - DidId(credentialDefinitionInput.author), - s"anoncred-credential-definition-private-key/${fetchedCredentialDefinition.guid}", - PrivateCredentialDefinitionSchemaSerDesV1.version - ) + storage <- ZIO.service[GenericSecretStorage] + maybeDidSecret <- storage + .get(fetchedCredentialDefinition.guid) .provideSomeLayer(Entity.Default.wacLayer) - (assertCorrectPrivateDefinitionSchema, maybeValidPrivateDefinitionZIO) = maybeDidSecret match { + maybeValidPrivateDefinitionZIO = maybeDidSecret match { case Some(didSecret) => - val schemaAssertion = - assert(didSecret.schemaId)(equalTo(PrivateCredentialDefinitionSchemaSerDesV1.version)) val validPrivateDefinition = PrivateCredentialDefinitionSchemaSerDesV1.schemaSerDes.validate(didSecret.json.toString()) - (schemaAssertion, validPrivateDefinition) + validPrivateDefinition case None => - (assert(false)(Assertion.isTrue), ZIO.succeed(false)) + ZIO.succeed(false) } maybeValidPrivateDefinition <- maybeValidPrivateDefinitionZIO assertValidPrivateDefinition = assert(maybeValidPrivateDefinition)(Assertion.isTrue) @@ -141,7 +135,6 @@ object CredentialDefinitionBasicSpec extends ZIOSpecDefault with CredentialDefin credentialDefinitionIsFetched && assertValidPublicDefinition && assertValidKeyCorrectnessProof && - assertCorrectPrivateDefinitionSchema && assertValidPrivateDefinition }, test("get the credential definition by the wrong id") { diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionTestTools.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionTestTools.scala index f84ae20783..4b609533d9 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionTestTools.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionTestTools.scala @@ -1,10 +1,10 @@ package io.iohk.atala.pollux.credentialdefinition import com.dimafeng.testcontainers.PostgreSQLContainer -import io.iohk.atala.agent.walletapi.memory.DIDSecretStorageInMemory +import io.iohk.atala.agent.walletapi.memory.GenericSecretStorageInMemory import io.iohk.atala.agent.walletapi.model.{ManagedDIDState, PublicationState} import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} -import io.iohk.atala.agent.walletapi.storage.DIDSecretStorage +import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.castor.core.model.did.PrismDIDOperation import io.iohk.atala.iam.authentication.{Authenticator, DefaultEntityAuthenticator} @@ -53,7 +53,7 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { ] private val controllerLayer = - DIDSecretStorageInMemory.layer >+> + GenericSecretStorageInMemory.layer >+> systemTransactorLayer >+> contextAwareTransactorLayer >+> JdbcCredentialDefinitionRepository.layer >+> ResourceURIDereferencerImpl.layer >+> CredentialDefinitionServiceImpl.layer >+> @@ -78,7 +78,7 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { lazy val testEnvironmentLayer = ZLayer.makeSome[ ManagedDIDService, CredentialDefinitionController & CredentialDefinitionRepository & CredentialDefinitionService & - PostgreSQLContainer & Authenticator & DIDSecretStorage + PostgreSQLContainer & Authenticator & GenericSecretStorage ]( controllerLayer, pgContainerLayer, diff --git a/prism-agent/service/wallet-api/src/main/resources/sql/agent/V12__generic_secret.sql b/prism-agent/service/wallet-api/src/main/resources/sql/agent/V12__generic_secret.sql new file mode 100644 index 0000000000..9b8c43b43c --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/resources/sql/agent/V12__generic_secret.sql @@ -0,0 +1,15 @@ +ALTER TABLE public.peer_did_rand_key DROP COLUMN "schema_id"; + +CREATE TABLE public.generic_secret ( + "key" TEXT NOT NULL, + "payload" TEXT NOT NULL, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "wallet_id" UUID REFERENCES public.wallet ("wallet_id") NOT NULL, + CONSTRAINT unique_key_wallet UNIQUE ("key", "wallet_id") +); + +ALTER TABLE public.generic_secret ENABLE ROW LEVEL SECURITY; + +CREATE POLICY generic_secret_wallet_isolation +ON public.generic_secret +USING (wallet_id = current_setting('app.current_wallet_id')::UUID); diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/memory/DIDSecretStorageInMemory.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/memory/DIDSecretStorageInMemory.scala index d6800e0b76..0f43d8e457 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/memory/DIDSecretStorageInMemory.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/memory/DIDSecretStorageInMemory.scala @@ -1,15 +1,16 @@ package io.iohk.atala.agent.walletapi.memory -import io.iohk.atala.agent.walletapi.storage.DIDSecret +import com.nimbusds.jose.jwk.OctetKeyPair import io.iohk.atala.agent.walletapi.storage.DIDSecretStorage import io.iohk.atala.mercury.model.DidId import io.iohk.atala.shared.models.WalletAccessContext import io.iohk.atala.shared.models.WalletId import zio.* -class DIDSecretStorageInMemory(walletRefs: Ref[Map[WalletId, Ref[Map[String, DIDSecret]]]]) extends DIDSecretStorage { +class DIDSecretStorageInMemory(walletRefs: Ref[Map[WalletId, Ref[Map[String, OctetKeyPair]]]]) + extends DIDSecretStorage { - private def walletStoreRef: URIO[WalletAccessContext, Ref[Map[String, DIDSecret]]] = + private def walletStoreRef: URIO[WalletAccessContext, Ref[Map[String, OctetKeyPair]]] = for { walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) refs <- walletRefs.get @@ -17,30 +18,30 @@ class DIDSecretStorageInMemory(walletRefs: Ref[Map[WalletId, Ref[Map[String, DID walletRef <- maybeWalletRef .fold { for { - ref <- Ref.make(Map.empty[String, DIDSecret]) + ref <- Ref.make(Map.empty[String, OctetKeyPair]) _ <- walletRefs.set(refs.updated(walletId, ref)) } yield ref }(ZIO.succeed) } yield walletRef - private def constructKey(did: DidId, keyId: String, schemaId: String): String = s"${did}_${keyId}_${schemaId}" + private def constructKey(did: DidId, keyId: String): String = s"${did}_${keyId}" - override def getKey(did: DidId, keyId: String, schemaId: String): RIO[WalletAccessContext, Option[DIDSecret]] = + override def getKey(did: DidId, keyId: String): RIO[WalletAccessContext, Option[OctetKeyPair]] = walletStoreRef.flatMap { storeRef => - storeRef.get.map(_.get(constructKey(did, keyId, schemaId))) + storeRef.get.map(_.get(constructKey(did, keyId))) } - override def insertKey(did: DidId, keyId: String, didSecret: DIDSecret): RIO[WalletAccessContext, Int] = + override def insertKey(did: DidId, keyId: String, keyPair: OctetKeyPair): RIO[WalletAccessContext, Int] = walletStoreRef.flatMap(storeRef => storeRef.modify { store => - val key = constructKey(did, keyId, didSecret.schemaId) + val key = constructKey(did, keyId) if (store.contains(key)) { ( ZIO.fail(Exception(s"Unique constraint violation, key $key already exist.")), store ) // Already exists, so we're effectively updating it } else { - (ZIO.succeed(1), store.updated(key, didSecret)) + (ZIO.succeed(1), store.updated(key, keyPair)) } }.flatten ) @@ -50,7 +51,7 @@ object DIDSecretStorageInMemory { val layer: ULayer[DIDSecretStorage] = ZLayer.fromZIO( Ref - .make(Map.empty[WalletId, Ref[Map[String, DIDSecret]]]) + .make(Map.empty[WalletId, Ref[Map[String, OctetKeyPair]]]) .map(DIDSecretStorageInMemory(_)) ) diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/memory/GenericSecretStorageInMemory.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/memory/GenericSecretStorageInMemory.scala new file mode 100644 index 0000000000..954c02af25 --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/memory/GenericSecretStorageInMemory.scala @@ -0,0 +1,60 @@ +package io.iohk.atala.agent.walletapi.memory + +import io.iohk.atala.agent.walletapi.storage.GenericSecret +import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage +import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.shared.models.WalletId +import zio.* +import zio.json.ast.Json + +class GenericSecretStorageInMemory(walletRefs: Ref[Map[WalletId, Ref[Map[String, Json]]]]) + extends GenericSecretStorage { + + private def walletStoreRef: URIO[WalletAccessContext, Ref[Map[String, Json]]] = + for { + walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) + refs <- walletRefs.get + maybeWalletRef = refs.get(walletId) + walletRef <- maybeWalletRef + .fold { + for { + ref <- Ref.make(Map.empty[String, Json]) + _ <- walletRefs.set(refs.updated(walletId, ref)) + } yield ref + }(ZIO.succeed) + } yield walletRef + + override def set[K, V](key: K, secret: V)(implicit ev: GenericSecret[K, V]): RIO[WalletAccessContext, Unit] = { + for { + storeRef <- walletStoreRef + _ <- storeRef.modify { store => + val keyPath = ev.keyPath(key) + if (store.contains(keyPath)) + ( + ZIO.fail(Exception(s"Unique constaint violation, key $key already exists.")), + store + ) + else (ZIO.unit, store.updated(keyPath, ev.encodeValue(secret))) + }.flatten + } yield () + } + + override def get[K, V](key: K)(implicit ev: GenericSecret[K, V]): RIO[WalletAccessContext, Option[V]] = { + val keyPath = ev.keyPath(key) + for { + storeRef <- walletStoreRef + json <- storeRef.get.map(_.get(keyPath)) + result <- json.fold(ZIO.none)(json => ZIO.fromTry(ev.decodeValue(json)).asSome) + } yield result + } + +} + +object GenericSecretStorageInMemory { + val layer: ULayer[GenericSecretStorage] = + ZLayer.fromZIO( + Ref + .make(Map.empty[WalletId, Ref[Map[String, Json]]]) + .map(GenericSecretStorageInMemory(_)) + ) +} 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 0f4220af02..543c49f5b2 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,7 +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.{DIDKeySecretStorage, DIDNonSecretStorage, WalletSecretStorage} +import io.iohk.atala.agent.walletapi.storage.{DIDSecretStorage, DIDNonSecretStorage, WalletSecretStorage} import io.iohk.atala.agent.walletapi.util.* import io.iohk.atala.castor.core.model.did.* import io.iohk.atala.castor.core.model.error.DIDOperationError @@ -26,7 +26,7 @@ import scala.language.implicitConversions class ManagedDIDServiceImpl private[walletapi] ( didService: DIDService, didOpValidator: DIDOperationValidator, - private[walletapi] val secretStorage: DIDKeySecretStorage, + private[walletapi] val secretStorage: DIDSecretStorage, override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, walletSecretStorage: WalletSecretStorage, apollo: Apollo, @@ -371,14 +371,14 @@ class ManagedDIDServiceImpl private[walletapi] ( object ManagedDIDServiceImpl { val layer: RLayer[ - DIDOperationValidator & DIDService & DIDKeySecretStorage & DIDNonSecretStorage & WalletSecretStorage & Apollo, + DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & WalletSecretStorage & Apollo, ManagedDIDService ] = { ZLayer.fromZIO { for { didService <- ZIO.service[DIDService] didOpValidator <- ZIO.service[DIDOperationValidator] - secretStorage <- ZIO.service[DIDKeySecretStorage] + secretStorage <- ZIO.service[DIDSecretStorage] nonSecretStorage <- ZIO.service[DIDNonSecretStorage] walletSecretStorage <- ZIO.service[WalletSecretStorage] apollo <- ZIO.service[Apollo] 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 04af44029d..9b3237f3e8 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 @@ -4,7 +4,7 @@ import io.iohk.atala.agent.walletapi.crypto.Apollo import io.iohk.atala.agent.walletapi.model.ManagedDIDDetail 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, DIDKeySecretStorage} +import io.iohk.atala.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage} 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 @@ -17,7 +17,7 @@ import zio.* class ManagedDIDServiceWithEventNotificationImpl( didService: DIDService, didOpValidator: DIDOperationValidator, - override private[walletapi] val secretStorage: DIDKeySecretStorage, + override private[walletapi] val secretStorage: DIDSecretStorage, override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, walletSecretStorage: WalletSecretStorage, apollo: Apollo, @@ -59,14 +59,14 @@ class ManagedDIDServiceWithEventNotificationImpl( object ManagedDIDServiceWithEventNotificationImpl { val layer: RLayer[ - DIDOperationValidator & DIDService & DIDKeySecretStorage & DIDNonSecretStorage & WalletSecretStorage & Apollo & + DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & WalletSecretStorage & Apollo & EventNotificationService, ManagedDIDService ] = ZLayer.fromZIO { for { didService <- ZIO.service[DIDService] didOpValidator <- ZIO.service[DIDOperationValidator] - secretStorage <- ZIO.service[DIDKeySecretStorage] + secretStorage <- ZIO.service[DIDSecretStorage] nonSecretStorage <- ZIO.service[DIDNonSecretStorage] walletSecretStorage <- ZIO.service[WalletSecretStorage] apollo <- ZIO.service[Apollo] diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcDIDSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcDIDSecretStorage.scala index 64c0a6c1dc..134d614c2d 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcDIDSecretStorage.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcDIDSecretStorage.scala @@ -1,17 +1,14 @@ package io.iohk.atala.agent.walletapi.sql +import com.nimbusds.jose.jwk.OctetKeyPair import doobie.* import doobie.implicits.* -import io.iohk.atala.agent.walletapi.storage.DIDSecret import io.iohk.atala.agent.walletapi.storage.DIDSecretStorage import io.iohk.atala.mercury.model.DidId import io.iohk.atala.shared.db.ContextAwareTask import io.iohk.atala.shared.db.Implicits.* import io.iohk.atala.shared.models.WalletAccessContext import zio.* -import zio.json.* -import zio.json.ast.Json -import zio.json.ast.Json.* import java.time.Instant import java.util.UUID @@ -32,50 +29,32 @@ class JdbcDIDSecretStorage(xa: Transactor[ContextAwareTask]) extends DIDSecretSt given didIdPut: Put[DidId] = Put[String].contramap(_.value) - given didSecretGet: Read[DIDSecret] = - Read[(Json, String)].map { case (json, schemaId) => DIDSecret(json, schemaId) } - - given didSecretPut: Write[DIDSecret] = - Write[(Json, String)].contramap(ds => (ds.json, ds.schemaId)) - - given jsonGet: Get[Json] = Get[String].map(_.fromJson[Json] match { - case Right(value) => value - case Left(error) => throw new RuntimeException(error) - }) - - given jsonPut: Put[Json] = Put[String].contramap(_.toString()) - - override def getKey(did: DidId, keyId: String, schemaId: String): RIO[WalletAccessContext, Option[DIDSecret]] = { + override def getKey(did: DidId, keyId: String): RIO[WalletAccessContext, Option[OctetKeyPair]] = { val cxnIO = sql""" - | SELECT - | key_pair, - | schema_id + | SELECT key_pair | FROM public.peer_did_rand_key | WHERE | did = $did - | AND schema_id = $schemaId | AND key_id = $keyId """.stripMargin - .query[DIDSecret] + .query[OctetKeyPair] .option cxnIO.transactWallet(xa) } - override def insertKey(did: DidId, keyId: String, didSecret: DIDSecret): RIO[WalletAccessContext, Int] = { + override def insertKey(did: DidId, keyId: String, keyPair: OctetKeyPair): RIO[WalletAccessContext, Int] = { val cxnIO = (now: InstantAsBigInt) => sql""" | INSERT INTO public.peer_did_rand_key( | did, | created_at, | key_id, - | key_pair, - | schema_id + | key_pair | ) values ( | ${did}, | ${now}, | ${keyId}, - | ${didSecret.json}, - | ${didSecret.schemaId} + | ${keyPair} | ) """.stripMargin.update diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcGenericSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcGenericSecretStorage.scala new file mode 100644 index 0000000000..e67756911a --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcGenericSecretStorage.scala @@ -0,0 +1,61 @@ +package io.iohk.atala.agent.walletapi.sql + +import doobie.* +import doobie.implicits.* +import doobie.postgres.implicits.* +import io.iohk.atala.agent.walletapi.storage.GenericSecret +import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage +import io.iohk.atala.shared.db.ContextAwareTask +import io.iohk.atala.shared.db.Implicits.* +import io.iohk.atala.shared.models.WalletAccessContext +import zio.* +import zio.json.ast.Json + +import java.time.Instant + +class JdbcGenericSecretStorage(xa: Transactor[ContextAwareTask]) extends GenericSecretStorage { + + override def set[K, V](key: K, secret: V)(implicit ev: GenericSecret[K, V]): RIO[WalletAccessContext, Unit] = { + val keyPath = ev.keyPath(key) + val payload = ev.encodeValue(secret) + val cxnIO = (now: Instant) => sql""" + | INSERT INTO public.generic_secret( + | key, + | payload, + | created_at, + | wallet_id + | ) values ( + | ${keyPath}, + | ${payload}, + | ${now}, + | current_setting('app.current_wallet_id')::UUID + | ) + """.stripMargin.update + + for { + now <- Clock.instant + _ <- cxnIO(now).run.transactWallet(xa) + } yield () + } + + override def get[K, V](key: K)(implicit ev: GenericSecret[K, V]): RIO[WalletAccessContext, Option[V]] = { + val keyPath = ev.keyPath(key) + val cxnIO = sql""" + | SELECT payload + | FROM public.generic_secret + | WHERE key = ${keyPath} + """.stripMargin + .query[Json] + .option + + cxnIO + .transactWallet(xa) + .flatMap(_.fold(ZIO.none)(json => ZIO.fromTry(ev.decodeValue(json)).asSome)) + } + +} + +object JdbcGenericSecretStorage { + val layer: URLayer[Transactor[ContextAwareTask], JdbcGenericSecretStorage] = + ZLayer.fromFunction(new JdbcGenericSecretStorage(_)) +} diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/package.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/package.scala index e9068ea2dc..105699c31f 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/package.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/package.scala @@ -1,5 +1,6 @@ package io.iohk.atala.agent.walletapi +import com.nimbusds.jose.jwk.OctetKeyPair import doobie.* import doobie.postgres.implicits.* import doobie.util.invariant.InvalidEnum @@ -15,6 +16,9 @@ import io.iohk.atala.castor.core.model.did.{PrismDID, PrismDIDOperation, Schedul import io.iohk.atala.event.notification.EventNotificationConfig import io.iohk.atala.prism.protos.node_models import io.iohk.atala.shared.models.WalletId +import zio.json.* +import zio.json.ast.Json +import zio.json.ast.Json.* import java.net.URL import java.time.Instant @@ -110,6 +114,15 @@ package object sql { given urlGet: Get[URL] = Get[String].map(URL(_)) given urlPut: Put[URL] = Put[String].contramap(_.toString()) + given octetKeyPairGet: Get[OctetKeyPair] = Get[String].map(OctetKeyPair.parse) + given octetKeyPairPut: Put[OctetKeyPair] = Put[String].contramap(_.toJSONString) + + given jsonGet: Get[Json] = Get[String].map(_.fromJson[Json] match { + case Right(value) => value + case Left(error) => throw new RuntimeException(error) + }) + given jsonPut: Put[Json] = Put[String].contramap(_.toString()) + final case class DIDStateRow( did: PrismDID, publicationStatus: PublicationStatusType, diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorage.scala deleted file mode 100644 index 7aa06407c7..0000000000 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorage.scala +++ /dev/null @@ -1,16 +0,0 @@ -package io.iohk.atala.agent.walletapi.storage - -import com.nimbusds.jose.jwk.OctetKeyPair -import io.iohk.atala.mercury.model.DidId -import io.iohk.atala.shared.models.WalletAccessContext -import zio.* - -/** A simple single-user DID key storage */ -trait DIDKeySecretStorage { - - /** PeerDID related methods. TODO: Refactor to abstract over PrismDID & PeerDID and merge methods */ - def insertKey(did: DidId, keyId: String, keyPair: OctetKeyPair): RIO[WalletAccessContext, Int] - - def getKey(did: DidId, keyId: String): RIO[WalletAccessContext, Option[OctetKeyPair]] - -} diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorageImpl.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorageImpl.scala deleted file mode 100644 index 323cd854bb..0000000000 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorageImpl.scala +++ /dev/null @@ -1,33 +0,0 @@ -package io.iohk.atala.agent.walletapi.storage - -import com.nimbusds.jose.jwk.OctetKeyPair -import io.iohk.atala.mercury.model.DidId -import io.iohk.atala.shared.models.WalletAccessContext -import zio.* -import zio.json.* -import zio.json.ast.Json -import zio.json.ast.Json.* - -class DIDKeySecretStorageImpl(didSecretStorage: DIDSecretStorage) extends DIDKeySecretStorage { - - private val schemaId = "jwk" - - def insertKey(did: DidId, keyId: String, keyPair: OctetKeyPair): RIO[WalletAccessContext, Int] = { - val didSecret = keyPair.toJSONString - .fromJson[Json] - .map(json => DIDSecret(json, schemaId)) - .getOrElse(throw new RuntimeException("Unexpected Serialisation Failure")) - didSecretStorage.insertKey(did, keyId, didSecret) - } - - def getKey(did: DidId, keyId: String): RIO[WalletAccessContext, Option[OctetKeyPair]] = { - for { - maybeDidSecret <- didSecretStorage.getKey(did, keyId, schemaId) - } yield maybeDidSecret.map(didSecret => OctetKeyPair.parse(didSecret.json.toString())) - } -} - -object DIDKeySecretStorageImpl { - val layer: URLayer[DIDSecretStorage, DIDKeySecretStorage] = - ZLayer.fromFunction(new DIDKeySecretStorageImpl(_)) -} diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorage.scala index 7d9e0d0f4f..1f1e2f3f1c 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorage.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorage.scala @@ -1,15 +1,12 @@ package io.iohk.atala.agent.walletapi.storage +import com.nimbusds.jose.jwk.OctetKeyPair import io.iohk.atala.mercury.model.DidId import io.iohk.atala.shared.models.WalletAccessContext import zio.* -import zio.json.ast.Json - -case class DIDSecret(json: Json, schemaId: String) /** A simple single-user DID key storage */ trait DIDSecretStorage { - def insertKey(did: DidId, keyId: String, didSecret: DIDSecret): RIO[WalletAccessContext, Int] - - def getKey(did: DidId, keyId: String, schemaId: String): RIO[WalletAccessContext, Option[DIDSecret]] + def insertKey(did: DidId, keyId: String, keyPair: OctetKeyPair): RIO[WalletAccessContext, Int] + def getKey(did: DidId, keyId: String): RIO[WalletAccessContext, Option[OctetKeyPair]] } diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorage.scala new file mode 100644 index 0000000000..16172828c4 --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorage.scala @@ -0,0 +1,29 @@ +package io.iohk.atala.agent.walletapi.storage + +import io.iohk.atala.shared.models.WalletAccessContext +import zio.* +import zio.json.ast.Json + +import java.util.UUID +import scala.util.Try + +final case class CredentialDefinitionSecret(json: Json) // to be moved to pollux? + +trait GenericSecretStorage { + def set[K, V](key: K, secret: V)(implicit ev: GenericSecret[K, V]): RIO[WalletAccessContext, Unit] + def get[K, V](key: K)(implicit ev: GenericSecret[K, V]): RIO[WalletAccessContext, Option[V]] +} + +trait GenericSecret[K, V] { + def keyPath(id: K): String + def encodeValue(secret: V): Json + def decodeValue(json: Json): Try[V] +} + +object GenericSecret { + given GenericSecret[UUID, CredentialDefinitionSecret] = new { + override def keyPath(id: UUID): String = s"credential-definitions/${id.toString()}" + override def encodeValue(secret: CredentialDefinitionSecret): Json = secret.json + override def decodeValue(json: Json): Try[CredentialDefinitionSecret] = Try(CredentialDefinitionSecret(json)) + } +} diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/VaultDIDSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/VaultDIDSecretStorage.scala index 71ff17960e..261032a624 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/VaultDIDSecretStorage.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/VaultDIDSecretStorage.scala @@ -1,6 +1,6 @@ package io.iohk.atala.agent.walletapi.vault -import io.iohk.atala.agent.walletapi.storage.DIDSecret +import com.nimbusds.jose.jwk.OctetKeyPair import io.iohk.atala.agent.walletapi.storage.DIDSecretStorage import io.iohk.atala.mercury.model.DidId import io.iohk.atala.shared.models.WalletAccessContext @@ -9,23 +9,23 @@ import zio.* class VaultDIDSecretStorage(vaultKV: VaultKVClient) extends DIDSecretStorage { - override def insertKey(did: DidId, keyId: String, didSecret: DIDSecret): RIO[WalletAccessContext, Int] = { + override def insertKey(did: DidId, keyId: String, keyPair: OctetKeyPair): RIO[WalletAccessContext, Int] = { for { walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) path = peerDidKeyPath(walletId)(did, keyId) - alreadyExist <- vaultKV.get[DIDSecret](path).map(_.isDefined) + alreadyExist <- vaultKV.get[OctetKeyPair](path).map(_.isDefined) _ <- vaultKV - .set[DIDSecret](path, didSecret) + .set[OctetKeyPair](path, keyPair) .when(!alreadyExist) .someOrFail(Exception(s"Secret on path $path already exists.")) } yield 1 } - override def getKey(did: DidId, keyId: String, schemaId: String): RIO[WalletAccessContext, Option[DIDSecret]] = { + override def getKey(did: DidId, keyId: String): RIO[WalletAccessContext, Option[OctetKeyPair]] = { for { walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) path = peerDidKeyPath(walletId)(did, keyId) - keyPair <- vaultKV.get[DIDSecret](path) + keyPair <- vaultKV.get[OctetKeyPair](path) } yield keyPair } diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/VaultGenericSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/VaultGenericSecretStorage.scala new file mode 100644 index 0000000000..6022d138d4 --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/VaultGenericSecretStorage.scala @@ -0,0 +1,43 @@ +package io.iohk.atala.agent.walletapi.vault + +import io.iohk.atala.agent.walletapi.storage.GenericSecret +import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage +import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.shared.models.WalletId +import zio.* +import zio.json.ast.Json + +class VaultGenericSecretStorage(vaultKV: VaultKVClient) extends GenericSecretStorage { + + override def set[K, V](key: K, secret: V)(implicit ev: GenericSecret[K, V]): RIO[WalletAccessContext, Unit] = { + val payload = ev.encodeValue(secret) + for { + walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) + path = constructKeyPath(walletId)(key) + alreadyExist <- vaultKV.get[Json](path).map(_.isDefined) + _ <- vaultKV + .set[Json](path, payload) + .when(!alreadyExist) + .someOrFail(Exception(s"Secret on path $path already exists.")) + } yield () + } + + override def get[K, V](key: K)(implicit ev: GenericSecret[K, V]): RIO[WalletAccessContext, Option[V]] = { + for { + walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) + path = constructKeyPath(walletId)(key) + json <- vaultKV.get[Json](path) + result <- json.fold(ZIO.none)(json => ZIO.fromTry(ev.decodeValue(json)).asSome) + } yield result + } + + private def constructKeyPath[K, V](walletId: WalletId)(key: K)(implicit ev: GenericSecret[K, V]): String = { + val keyPath = ev.keyPath(key) + s"secret/${walletId.toUUID}/generic-secrets/$keyPath" + } + +} + +object VaultGenericSecretStorage { + def layer: URLayer[VaultKVClient, GenericSecretStorage] = ZLayer.fromFunction(VaultGenericSecretStorage(_)) +} diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/package.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/package.scala index 93347eb5cd..7e320abd56 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/package.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/vault/package.scala @@ -1,7 +1,7 @@ package io.iohk.atala.agent.walletapi +import com.nimbusds.jose.jwk.OctetKeyPair import io.iohk.atala.agent.walletapi.model.WalletSeed -import io.iohk.atala.agent.walletapi.storage.DIDSecret import io.iohk.atala.shared.models.HexString import zio.json.* import zio.json.ast.Json @@ -26,7 +26,7 @@ package object vault { override def decode(kv: Map[String, String]): Try[WalletSeed] = { kv.get("value") match { - case None => Failure(Exception("A property 'value' is missing from KV data")) + case None => Failure(Exception("A property 'value' is missing from vault KV data")) case Some(encodedSeed) => HexString .fromString(encodedSeed) @@ -36,23 +36,29 @@ package object vault { } } - given KVCodec[DIDSecret] = new { - override def encode(value: DIDSecret): Map[String, String] = { - Map( - "schemaId" -> value.schemaId, - "json" -> value.json.toString() - ) - } + given KVCodec[OctetKeyPair] = new { + override def encode(value: OctetKeyPair): Map[String, String] = + Map("value" -> value.toJSONString()) + + override def decode(kv: Map[String, String]): Try[OctetKeyPair] = + for { + jwk <- kv.get("value").toRight(Exception("A property 'value' is missing from vault KV data")).toTry + keyPair <- Try(OctetKeyPair.parse(jwk)) + } yield keyPair + } - override def decode(kv: Map[String, String]): Try[DIDSecret] = { + given KVCodec[Json] = new { + override def encode(value: Json): Map[String, String] = + Map("value" -> value.toJson) + + override def decode(kv: Map[String, String]): Try[Json] = for { - schemaId <- kv.get("schemaId").toRight(Exception(s"A property 'schemaId' is missing from KV data")).toTry - json <- kv - .get("json") - .toRight(Exception(s"A property 'json' is missing from KV data")) - .flatMap { jsonStr => jsonStr.fromJson[Json].left.map(RuntimeException(_)) } + json <- kv.get("value").toRight(Exception("A property 'value' is missing from vault KV data")).toTry + keyPair <- json + .fromJson[Json] + .left + .map(s => Exception(s"Fail to parse JSON from string: $s")) .toTry - } yield DIDSecret(json, schemaId) - } + } yield keyPair } } 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 5b631156d8..c474468d18 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 @@ -93,7 +93,6 @@ object ManagedDIDServiceSpec DIDOperationValidator.layer(), JdbcDIDNonSecretStorage.layer, JdbcWalletNonSecretStorage.layer, - DIDKeySecretStorageImpl.layer, systemTransactorLayer, contextAwareTransactorLayer, testDIDServiceLayer, diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala similarity index 62% rename from prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorageSpec.scala rename to prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala index d1502d818f..851668899b 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDKeySecretStorageSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala @@ -18,8 +18,9 @@ import zio.test.* import zio.test.Assertion.* import io.iohk.atala.agent.walletapi.memory.DIDSecretStorageInMemory import io.iohk.atala.agent.walletapi.memory.WalletSecretStorageInMemory +import io.iohk.atala.agent.walletapi.model.Wallet -object DIDKeySecretStorageSpec +object DIDSecretStorageSpec extends ZIOSpecDefault, StorageSpecHelper, PostgresTestContainerSupport, @@ -40,7 +41,6 @@ object DIDKeySecretStorageSpec JdbcDIDNonSecretStorage.layer, JdbcDIDSecretStorage.layer, JdbcWalletSecretStorage.layer, - DIDKeySecretStorageImpl.layer, systemTransactorLayer, contextAwareTransactorLayer, pgContainerLayer, @@ -52,7 +52,6 @@ object DIDKeySecretStorageSpec JdbcDIDNonSecretStorage.layer, VaultDIDSecretStorage.layer, VaultWalletSecretStorage.layer, - DIDKeySecretStorageImpl.layer, systemTransactorLayer, contextAwareTransactorLayer, pgContainerLayer, @@ -65,7 +64,6 @@ object DIDKeySecretStorageSpec JdbcDIDNonSecretStorage.layer, DIDSecretStorageInMemory.layer, WalletSecretStorageInMemory.layer, - DIDKeySecretStorageImpl.layer, systemTransactorLayer, contextAwareTransactorLayer, pgContainerLayer, @@ -75,11 +73,14 @@ object DIDKeySecretStorageSpec suite("DIDSecretStorage")(jdbcTestSuite, vaultTestSuite, inMemoryTestSuite) @@ TestAspect.sequential } - private def commonSpec(name: String) = suite(name)( + private def commonSpec(name: String) = + suite(name)(singleWalletSpec, multiWalletSpec) @@ TestAspect.before(DBTestUtils.runMigrationAgentDB) + + private val singleWalletSpec = suite("single-wallet")( test("insert and get the same key for OctetKeyPair") { for { nonSecretStorage <- ZIO.service[DIDNonSecretStorage] - secretStorage <- ZIO.service[DIDKeySecretStorage] + secretStorage <- ZIO.service[DIDSecretStorage] peerDID = PeerDID.makePeerDid() _ <- nonSecretStorage.createPeerDIDRecord(peerDID.did) n1 <- secretStorage.insertKey(peerDID.did, "agreement", peerDID.jwkForKeyAgreement) @@ -94,7 +95,7 @@ object DIDKeySecretStorageSpec test("insert same key id return error") { for { nonSecretStorage <- ZIO.service[DIDNonSecretStorage] - secretStorage <- ZIO.service[DIDKeySecretStorage] + secretStorage <- ZIO.service[DIDSecretStorage] peerDID = PeerDID.makePeerDid() _ <- nonSecretStorage.createPeerDIDRecord(peerDID.did) n1 <- secretStorage.insertKey(peerDID.did, "agreement", peerDID.jwkForKeyAgreement) @@ -108,11 +109,55 @@ object DIDKeySecretStorageSpec }, test("get non-exist key return none") { for { - secretStorage <- ZIO.service[DIDKeySecretStorage] + secretStorage <- ZIO.service[DIDSecretStorage] peerDID = PeerDID.makePeerDid() key1 <- secretStorage.getKey(peerDID.did, "agreement") } yield assert(key1)(isNone) }, - ).globalWallet @@ TestAspect.before(DBTestUtils.runMigrationAgentDB) + ).globalWallet + + private val multiWalletSpec = suite("multi-wallet")( + test("do not see peer DID key outside of the wallet") { + for { + walletSvc <- ZIO.service[WalletManagementService] + walletId1 <- walletSvc.createWallet(Wallet("wallet-1")).map(_.id) + walletId2 <- walletSvc.createWallet(Wallet("wallet-2")).map(_.id) + nonSecretStorage <- ZIO.service[DIDNonSecretStorage] + secretStorage <- ZIO.service[DIDSecretStorage] + // wallet1 setup + peerDID1 = PeerDID.makePeerDid() + _ <- nonSecretStorage + .createPeerDIDRecord(peerDID1.did) + .provide(ZLayer.succeed(WalletAccessContext(walletId1))) + _ <- secretStorage + .insertKey(peerDID1.did, "key-1", peerDID1.jwkForKeyAgreement) + .provide(ZLayer.succeed(WalletAccessContext(walletId1))) + // wallet2 setup + peerDID2 = PeerDID.makePeerDid() + _ <- nonSecretStorage + .createPeerDIDRecord(peerDID2.did) + .provide(ZLayer.succeed(WalletAccessContext(walletId2))) + _ <- secretStorage + .insertKey(peerDID2.did, "key-1", peerDID2.jwkForKeyAgreement) + .provide(ZLayer.succeed(WalletAccessContext(walletId2))) + // assertions + ownWallet1 <- secretStorage + .getKey(peerDID1.did, "key-1") + .provide(ZLayer.succeed(WalletAccessContext(walletId1))) + ownWallet2 <- secretStorage + .getKey(peerDID2.did, "key-1") + .provide(ZLayer.succeed(WalletAccessContext(walletId2))) + crossWallet1 <- secretStorage + .getKey(peerDID1.did, "key-1") + .provide(ZLayer.succeed(WalletAccessContext(walletId2))) + crossWallet2 <- secretStorage + .getKey(peerDID2.did, "key-1") + .provide(ZLayer.succeed(WalletAccessContext(walletId1))) + } yield assert(ownWallet1)(isSome(equalTo(peerDID1.jwkForKeyAgreement))) && + assert(ownWallet2)(isSome(equalTo(peerDID2.jwkForKeyAgreement))) && + assert(crossWallet1)(isNone) && + assert(crossWallet2)(isNone) + } + ) } diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala new file mode 100644 index 0000000000..992f6fb03f --- /dev/null +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala @@ -0,0 +1,163 @@ +package io.iohk.atala.agent.walletapi.storage + +import io.iohk.atala.agent.walletapi.crypto.ApolloSpecHelper +import io.iohk.atala.agent.walletapi.memory.GenericSecretStorageInMemory +import io.iohk.atala.agent.walletapi.model.Wallet +import io.iohk.atala.agent.walletapi.service.WalletManagementService +import io.iohk.atala.agent.walletapi.service.WalletManagementServiceImpl +import io.iohk.atala.agent.walletapi.sql.JdbcGenericSecretStorage +import io.iohk.atala.agent.walletapi.sql.JdbcWalletNonSecretStorage +import io.iohk.atala.agent.walletapi.sql.JdbcWalletSecretStorage +import io.iohk.atala.agent.walletapi.vault.VaultGenericSecretStorage +import io.iohk.atala.agent.walletapi.vault.VaultWalletSecretStorage +import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.test.container.DBTestUtils +import io.iohk.atala.test.container.VaultTestContainerSupport +import zio.* +import zio.test.* +import zio.test.Assertion.* + +import java.util.UUID +import zio.json.ast.Json +import io.iohk.atala.shared.models.WalletAccessContext + +object GenericSecretStorageSpec + extends ZIOSpecDefault, + StorageSpecHelper, + PostgresTestContainerSupport, + VaultTestContainerSupport, + ApolloSpecHelper { + + private def walletManagementServiceLayer = + ZLayer.makeSome[WalletSecretStorage, WalletManagementService]( + WalletManagementServiceImpl.layer, + JdbcWalletNonSecretStorage.layer, + contextAwareTransactorLayer, + apolloLayer + ) + + override def spec = { + val jdbcTestSuite = commonSpec("JdbcGenericSecretStorage") + .provide( + JdbcWalletSecretStorage.layer, + JdbcGenericSecretStorage.layer, + contextAwareTransactorLayer, + pgContainerLayer, + walletManagementServiceLayer + ) + + val vaultTestSuite = commonSpec("VaultGenericSecretStorage") + .provide( + VaultWalletSecretStorage.layer, + VaultGenericSecretStorage.layer, + pgContainerLayer, + vaultKvClientLayer, + walletManagementServiceLayer + ) + + val inMemoryTestSuite = commonSpec("InMemoryGenericSecretStorage") + .provide( + JdbcWalletSecretStorage.layer, + GenericSecretStorageInMemory.layer, + contextAwareTransactorLayer, + pgContainerLayer, + walletManagementServiceLayer + ) + + suite("GenericSecretStorage")(jdbcTestSuite, vaultTestSuite, inMemoryTestSuite) @@ TestAspect.sequential + } + + private def commonSpec(name: String) = + suite(name)(singleWalletSpec, multiWalletSpec) @@ TestAspect.before(DBTestUtils.runMigrationAgentDB) + + private val singleWalletSpec = suite("single-wallet")( + test("insert and get the same item") { + for { + storage <- ZIO.service[GenericSecretStorage] + id = UUID.randomUUID() + secret = CredentialDefinitionSecret(json = Json.Obj("foo" -> Json.Str("bar"))) + _ <- storage.set(id, secret) + result: Option[CredentialDefinitionSecret] <- storage.get(id) + } yield assert(result)(isSome(equalTo(secret))) + }, + test("insert item with same path return error") { + for { + storage <- ZIO.service[GenericSecretStorage] + id = UUID.randomUUID() + secret1 = CredentialDefinitionSecret(json = Json.Obj("foo1" -> Json.Str("bar1"))) + secret2 = CredentialDefinitionSecret(json = Json.Obj("foo2" -> Json.Str("bar2"))) + _ <- storage.set(id, secret1) + exit <- storage.set(id, secret2).exit + } yield assert(exit)(fails(anything)) + }, + test("get non-existing secret return none") { + for { + storage <- ZIO.service[GenericSecretStorage] + id = UUID.randomUUID() + result <- storage.get(id) + } yield assert(result)(isNone) + } + ).globalWallet + + private val multiWalletSpec = suite("multi-wallet")( + test("insert item with same path for different wallet do not fail") { + for { + walletSvc <- ZIO.service[WalletManagementService] + walletId1 <- walletSvc.createWallet(Wallet("wallet-1")).map(_.id) + walletId2 <- walletSvc.createWallet(Wallet("wallet-2")).map(_.id) + storage <- ZIO.service[GenericSecretStorage] + id = UUID.randomUUID() + secret = CredentialDefinitionSecret(json = Json.Obj("foo" -> Json.Str("bar"))) + _ <- storage + .set(id, secret) + .provide(ZLayer.succeed(WalletAccessContext(walletId1))) + _ <- storage + .set(id, secret) + .provide(ZLayer.succeed(WalletAccessContext(walletId2))) + secret1 <- storage + .get(id) + .provide(ZLayer.succeed(WalletAccessContext(walletId1))) + secret2 <- storage + .get(id) + .provide(ZLayer.succeed(WalletAccessContext(walletId2))) + } yield assert(secret1)(equalTo(secret2)) && assert(secret1)(isSome) + }, + test("do no see secret outside of the wallet") { + for { + walletSvc <- ZIO.service[WalletManagementService] + walletId1 <- walletSvc.createWallet(Wallet("wallet-1")).map(_.id) + walletId2 <- walletSvc.createWallet(Wallet("wallet-2")).map(_.id) + storage <- ZIO.service[GenericSecretStorage] + // wallet1 setup + id1 = UUID.randomUUID() + secret1 = CredentialDefinitionSecret(json = Json.Obj("foo1" -> Json.Str("bar1"))) + _ <- storage + .set(id1, secret1) + .provide(ZLayer.succeed(WalletAccessContext(walletId1))) + // wallet2 setup + id2 = UUID.randomUUID() + secret2 = CredentialDefinitionSecret(json = Json.Obj("foo2" -> Json.Str("bar2"))) + _ <- storage + .set(id2, secret2) + .provide(ZLayer.succeed(WalletAccessContext(walletId2))) + // assertions + ownWallet1 <- storage + .get(id1) + .provide(ZLayer.succeed(WalletAccessContext(walletId1))) + ownWallet2 <- storage + .get(id2) + .provide(ZLayer.succeed(WalletAccessContext(walletId2))) + crossWallet1 <- storage + .get(id1) + .provide(ZLayer.succeed(WalletAccessContext(walletId2))) + crossWallet2 <- storage + .get(id2) + .provide(ZLayer.succeed(WalletAccessContext(walletId1))) + } yield assert(ownWallet1)(isSome(equalTo(secret1))) && + assert(ownWallet2)(isSome(equalTo(secret2))) && + assert(crossWallet1)(isNone) && + assert(crossWallet2)(isNone) + } + ) + +} diff --git a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresLayer.scala b/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresLayer.scala index fd0737cf26..cf9e2608c9 100644 --- a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresLayer.scala +++ b/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresLayer.scala @@ -11,7 +11,7 @@ import zio.* object PostgresLayer { def postgresLayer( - imageName: Option[String] = Some("postgres"), + imageName: Option[String] = Some("postgres:13"), verbose: Boolean = false ): TaskLayer[PostgreSQLContainer] = ZLayer.scoped { diff --git a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresTestContainer.scala b/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresTestContainer.scala index 9e30407da8..132ab26b92 100644 --- a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresTestContainer.scala +++ b/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresTestContainer.scala @@ -5,7 +5,10 @@ import org.testcontainers.containers.output.OutputFrame import org.testcontainers.utility.DockerImageName object PostgresTestContainer { - def postgresContainer(imageName: Option[String] = Some("postgres"), verbose: Boolean = false): PostgreSQLContainer = { + def postgresContainer( + imageName: Option[String] = Some("postgres:13"), + verbose: Boolean = false + ): PostgreSQLContainer = { val container = if (sys.env.contains("GITHUB_NETWORK")) new PostgreSQLContainerCustom(dockerImageNameOverride = imageName.map(DockerImageName.parse))