diff --git a/TASKs.md b/TASKs.md new file mode 100644 index 00000000..a7312e78 --- /dev/null +++ b/TASKs.md @@ -0,0 +1,77 @@ +# ATL-5236 - Error Handling (PRISM Agent and Mediator) + +https://input-output.atlassian.net/browse/ATL-5236 + +https://identity.foundation/didcomm-messaging/spec/#problem-reports + + +On each step of all our protocols processing, when something wrong is happening, we need to: +Goals +- Update the record to a documented error state +- Log the error in the service logs +- Send the problem report message when appropriate + +Goal other: error recover/resilient +- [optional] Send event that record state changed to error +- Decide on the policy of re-trying sending errors (one of the proposals is just to send it once, and if a recipient did not get this, then it’s on its own requesting record ID state) + + + +Tasks: +- (2W * 2Dev) Mediator (Note: most errors in Mediator will be synchronous) + - Store messages when sending (1w) + - Catch Errors and send Problem Reports (1w): + - (sync) e.p.crypto - is message is tampering (any crypto error). + - (sync) e.p.crypto.unsupported - is message is tampering (any crypto error). + - (sync & async) e.p.crypto.replay - if the message is replay (possible he replay attack). + - (sync) e.p.req - pickup message before enroling. + - (sync) e.p.me.res.storage - connection MongoBD is not working. + - (sync) e.p.me.res.storage - business logic MongoBD is not working. + - (sync) e.p.did - for any DID method that is not `did.peer`. + - (sync) e.p.did.malformed - for any DID method malformed. + - (sync) e.p.msg - for parsing error from the message. + - (sync) e.p.msg.unsupported - for the message type LiveModeChange and all message that is not role of the mediator + - [DONE] MediateGrant + - [DONE] MediateDeny + - [DONE] KeylistResponse + - [DONE] Status = https://didcomm.org/messagepickup/3.0/status + - [DONE] LiveModeChange - https://didcomm.org/messagepickup/3.0/live-delivery-change + - [TODO] ... + - (sync) e.p.msg.unsupported - for parsing error due to unsupported version or protocol. + - [DONE] MissingProtocolExecuter (unsupported protocol it also works fine for unsupported versions) + - (sync & async) e.p.req.not_enroll - Get a Forward message to a DID that is not enrolled. + - (sync & async) e.p.me - catch all error at the end. + - Receive a problem report (1w): + - in case of Warnings Reply `w.p` -> log warnings and escalate to an error `e.p` on the reply + - in case of Error `e.p` -> log error + +- (4/6W * 2Dev) PRISM Agent (Note: most error in PRISM Agent will be asynchronous) + - Store DID Comm messages in a searchable way (4W) + - Store receiving messages (in a searchable way) + - Store sending messages (in a searchable way) + - both plaintext and encrypted + - [optional] Maybe later MongoBD for PRISM Agent + - Implement Problem Reports in Mercury (DONE??) + - Catch all the Errors and send Problem Reports: (4W) + - e.p.xfer - if it found nobody listening on the specified port. (service endpoint unavailable) + - (async) e.p.crypto - is message is tampering (any crypto error). + - (async) e.p.crypto.unsupported - is message is tampering (any crypto error). + - (sync & async) e.p.crypto.replay - if the message is replay (possible he replay attack). + - (async) e.p.me.res.storage - connection MongoBD is not working. + - (async) e.p.me.res.storage - business logic MongoBD is not working. + - (async) e.p.did - for any DID method that is not `did.peer`. + - (async) e.p.did.malformed - for any DID method malformed. + - (async) e.p.msg - for parsing errors from the message. + - (async) e.p.msg.unsupported - for the message with the wrong role of the agent. + - (async) e.p.msg.unsupported - for parsing error due to unsupported version or protocol. + - (sync & async) e.p.me - catch all error at the end. + - (async) TODO `w.m` + - Receive e Problem Reports (1W): + - support for scope `m` -> state update (of the protocol execution) + - support for scope `p` -> state update (and fail protocol execution) + +- Other task (not for this ticket) + - Traceability of the MsgID of the Problem Report to the original error (2d) -> ATL-4147 + - [optional] Log - https://input-output.atlassian.net/browse/ATL-4147 + - escalate_to must be configurable (1d) + - [optional] update the protocol with new tokens (2d) diff --git a/build.sbt b/build.sbt index ac9639e5..c0d62393 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,7 @@ inThisBuild( /** Versions */ lazy val V = new { - val scalaDID = "0.1.0-M6" + val scalaDID = "0.1.0-M6+0-b02d2b9e+20230724-1637-SNAPSHOT" // val scalajsJavaSecureRandom = "1.0.0" // FIXME another bug in the test framework https://github.com/scalameta/munit/issues/554 diff --git a/mediator/src/main/scala/io/iohk/atala/mediator/actions/ProtocolExecute.scala b/mediator/src/main/scala/io/iohk/atala/mediator/actions/ProtocolExecute.scala index 63ffc2b2..a94c476c 100644 --- a/mediator/src/main/scala/io/iohk/atala/mediator/actions/ProtocolExecute.scala +++ b/mediator/src/main/scala/io/iohk/atala/mediator/actions/ProtocolExecute.scala @@ -13,6 +13,7 @@ import io.iohk.atala.mediator.db.* import io.iohk.atala.mediator.protocols.NullProtocolExecuter import zio.* import zio.json.* +import io.iohk.atala.mediator.protocols.MissingProtocolExecuter //TODO pick a better name // maybe "Protocol" only trait ProtocolExecuter[-R] { @@ -29,7 +30,7 @@ trait ProtocolExecuter[-R] { object ProtocolExecuter { type Services = Resolver & Agent & Operations & MessageDispatcher } -case class ProtocolExecuterCollection[-R](executers: ProtocolExecuter[R]*) extends ProtocolExecuter[R] { +case class ProtocolExecuterCollection[-R <: Agent](executers: ProtocolExecuter[R]*) extends ProtocolExecuter[R] { override def suportedPIURI: Seq[PIURI] = executers.flatMap(_.suportedPIURI) @@ -39,14 +40,16 @@ case class ProtocolExecuterCollection[-R](executers: ProtocolExecuter[R]*) exten plaintextMessage: PlaintextMessage, ): ZIO[R1, MediatorError, Option[EncryptedMessage]] = selectExecutersFor(plaintextMessage.`type`) match - case None => NullProtocolExecuter.execute(plaintextMessage) + // case None => NullProtocolExecuter.execute(plaintextMessage) + case None => MissingProtocolExecuter.execute(plaintextMessage) case Some(px) => px.execute(plaintextMessage) override def program[R1 <: R]( plaintextMessage: PlaintextMessage, ): ZIO[R1, MediatorError, Action] = selectExecutersFor(plaintextMessage.`type`) match - case None => NullProtocolExecuter.program(plaintextMessage) + // case None => NullProtocolExecuter.program(plaintextMessage) + case None => MissingProtocolExecuter.program(plaintextMessage) case Some(px) => px.program(plaintextMessage) } diff --git a/mediator/src/main/scala/io/iohk/atala/mediator/protocols/MediatorCoordinationExecuter.scala b/mediator/src/main/scala/io/iohk/atala/mediator/protocols/MediatorCoordinationExecuter.scala index ddcfe424..0800b47c 100644 --- a/mediator/src/main/scala/io/iohk/atala/mediator/protocols/MediatorCoordinationExecuter.scala +++ b/mediator/src/main/scala/io/iohk/atala/mediator/protocols/MediatorCoordinationExecuter.scala @@ -44,8 +44,34 @@ object MediatorCoordinationExecuter extends ProtocolExecuterWithServices[Protoco case `piuriKeylistQuery` => plaintextMessage.toKeylistQuery case `piuriKeylist` => plaintextMessage.toKeylist }).map { - case m: MediateGrant => ZIO.logWarning("MediateGrant") *> ZIO.succeed(NoReply) - case m: MediateDeny => ZIO.logWarning("MediateDeny") *> ZIO.succeed(NoReply) + case m: MediateGrant => + ZIO.logWarning("MediateGrant") *> ZIO.succeed(NoReply) *> + ZIO.succeed( + SyncReplyOnly( + Problems + .unsupportedProtocolRole( + from = m.to.asFROM, + to = m.from.asTO, + pthid = m.id, // TODO CHECK pthid + piuri = m.piuri, + ) + .toPlaintextMessage + ) + ) + case m: MediateDeny => + ZIO.logWarning("MediateDeny") *> ZIO.succeed(NoReply) *> + ZIO.succeed( + SyncReplyOnly( + Problems + .unsupportedProtocolRole( + from = m.to.asFROM, + to = m.from.asTO, + pthid = m.id, // TODO CHECK pthid + piuri = m.piuri, + ) + .toPlaintextMessage + ) + ) case m: MediateRequest => for { _ <- ZIO.logInfo("MediateRequest") @@ -74,7 +100,20 @@ object MediatorCoordinationExecuter extends ProtocolExecuterWithServices[Protoco } } } yield SyncReplyOnly(m.makeKeylistResponse(updateResponse).toPlaintextMessage) - case m: KeylistResponse => ZIO.logWarning("KeylistResponse") *> ZIO.succeed(NoReply) + case m: KeylistResponse => + ZIO.logWarning("KeylistResponse") *> ZIO.succeed(NoReply) *> + ZIO.succeed( + SyncReplyOnly( + Problems + .unsupportedProtocolRole( + from = m.to.asFROM, + to = m.from.asTO, + pthid = m.id, // TODO CHECK pthid + piuri = m.piuri, + ) + .toPlaintextMessage + ) + ) case m: KeylistQuery => for { _ <- ZIO.logInfo("KeylistQuery") @@ -94,7 +133,7 @@ object MediatorCoordinationExecuter extends ProtocolExecuterWithServices[Protoco case Some(response) => SyncReplyOnly(response.toPlaintextMessage) case m: Keylist => ZIO.logWarning("Keylist") *> ZIO.succeed(NoReply) } match - case Left(error) => ZIO.logError(error) *> ZIO.succeed(NoReply) + case Left(error) => ZIO.logError(error) *> ZIO.succeed(NoReply) // TODO error report case Right(program) => program } diff --git a/mediator/src/main/scala/io/iohk/atala/mediator/protocols/MissingProtocolExecuter.scala b/mediator/src/main/scala/io/iohk/atala/mediator/protocols/MissingProtocolExecuter.scala new file mode 100644 index 00000000..99e54771 --- /dev/null +++ b/mediator/src/main/scala/io/iohk/atala/mediator/protocols/MissingProtocolExecuter.scala @@ -0,0 +1,29 @@ +package io.iohk.atala.mediator.protocols + +import zio.ZIO + +import fmgp.did.* +import fmgp.did.comm.PlaintextMessage +import io.iohk.atala.mediator.MissingProtocolError +import io.iohk.atala.mediator.actions.ProtocolExecuter +import io.iohk.atala.mediator.actions.Reply + +object MissingProtocolExecuter extends ProtocolExecuter[Agent] { + + override def suportedPIURI = Seq() + override def program[R1 <: Agent](plaintextMessage: PlaintextMessage) = + ZIO + .service[Agent] + .map(agent => + Reply( + Problems + .unsupportedProtocolType( + to = plaintextMessage.from.map(_.asTO).toSet, + from = agent.id, + pthid = plaintextMessage.id, + piuri = plaintextMessage.`type`, + ) + .toPlaintextMessage + ) + ) +} diff --git a/mediator/src/main/scala/io/iohk/atala/mediator/protocols/PickupExecuter.scala b/mediator/src/main/scala/io/iohk/atala/mediator/protocols/PickupExecuter.scala index f2a5687d..77b06ee4 100644 --- a/mediator/src/main/scala/io/iohk/atala/mediator/protocols/PickupExecuter.scala +++ b/mediator/src/main/scala/io/iohk/atala/mediator/protocols/PickupExecuter.scala @@ -64,7 +64,20 @@ object PickupExecuter live_delivery = None, // TODO ) } yield SyncReplyOnly(status.toPlaintextMessage) - case m: Status => ZIO.logInfo("Status") *> ZIO.succeed(NoReply) + case m: Status => + ZIO.logInfo("Status") *> + ZIO.succeed( + SyncReplyOnly( + Problems + .unsupportedProtocolRole( + from = m.to.asFROM, + to = m.from.asTO, + pthid = m.id, // TODO CHECK pthid + piuri = m.piuri, + ) + .toPlaintextMessage + ) + ) case m: DeliveryRequest => for { _ <- ZIO.logInfo("DeliveryRequest") @@ -73,7 +86,7 @@ object PickupExecuter didRequestingMessages = m.from.asFROMTO mDidAccount <- repoDidAccount.getDidAccount(didRequestingMessages.toDID) msgHash = mDidAccount match - case None => ??? + case None => ??? // TODO ERROR case Some(didAccount) => didAccount.messagesRef.filter(_.state == false).map(_.hash) allMessagesFor <- repoMessageItem.findByIds(msgHash) messagesToReturn = @@ -115,7 +128,20 @@ object PickupExecuter m.message_id_list ) } yield NoReply - case m: LiveModeChange => ZIO.logWarning("LiveModeChange not implemented") *> ZIO.succeed(NoReply) // TODO + case m: LiveModeChange => + ZIO.logWarning("LiveModeChange not implemented") *> + ZIO.succeed( + SyncReplyOnly( + Problems + .protocolNotImplemented( + from = m.to.asFROM, + to = m.from.asTO, + pthid = m.id, // TODO CHECK pthid + piuri = m.piuri, + ) + .toPlaintextMessage + ) + ) } match case Left(error) => ZIO.logError(error) *> ZIO.succeed(NoReply) diff --git a/mediator/src/main/scala/io/iohk/atala/mediator/protocols/Problems.scala b/mediator/src/main/scala/io/iohk/atala/mediator/protocols/Problems.scala new file mode 100644 index 00000000..41bfedc6 --- /dev/null +++ b/mediator/src/main/scala/io/iohk/atala/mediator/protocols/Problems.scala @@ -0,0 +1,59 @@ +package io.iohk.atala.mediator.protocols + +import fmgp.did.* +import fmgp.did.comm.* +import fmgp.did.comm.protocol.reportproblem2.* + +object Problems { + def unsupportedProtocolType( + to: Set[TO], + from: FROM, + pthid: MsgID, + piuri: PIURI, + ) = ProblemReport( + // id: MsgID = MsgID(), + to = to, + from = from, // Can it be Option? + pthid = pthid, + ack = None, // Option[Seq[MsgID]], + code = ProblemCode.ErroFail("msg", "unsupported"), + comment = None, // Option[String], + args = None, // Option[Seq[String]], + escalate_to = None, // Option[String], + ) + + def unsupportedProtocolRole( + to: TO, + from: FROM, + pthid: MsgID, + piuri: PIURI, + ) = ProblemReport( + // id: MsgID = MsgID(), + to = Set(to), + from = from, // Can it be Option? + pthid = pthid, + ack = None, // Option[Seq[MsgID]], + code = ProblemCode.ErroFail("msg", "unsupported"), + comment = None, // Option[String], + args = None, // Option[Seq[String]], + escalate_to = None, // Option[String], + ) + + def protocolNotImplemented( + to: TO, + from: FROM, + pthid: MsgID, + piuri: PIURI, + ) = ProblemReport( + // id: MsgID = MsgID(), + to = Set(to), + from = from, // Can it be Option? + pthid = pthid, + ack = None, // Option[Seq[MsgID]], + code = ProblemCode.ErroFail("msg", "unsupported"), + comment = None, // Option[String], + args = None, // Option[Seq[String]], + escalate_to = None, // Option[String], + ) + +}