Skip to content

Commit

Permalink
feat(agent): ATL-6839 migrate DIDComm endpoint to tapir (#1116)
Browse files Browse the repository at this point in the history
Signed-off-by: Benjamin Voiturier <benjamin.voiturier@iohk.io>
  • Loading branch information
bvoiturier committed May 29, 2024
1 parent 4b8acc2 commit 2f4f7c3
Show file tree
Hide file tree
Showing 16 changed files with 384 additions and 257 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import org.hyperledger.identus.pollux.credentialschema.{
}
import org.hyperledger.identus.pollux.vc.jwt.DidResolver as JwtDidResolver
import org.hyperledger.identus.presentproof.controller.PresentProofServerEndpoints
import org.hyperledger.identus.shared.models.{HexString, WalletAccessContext, WalletAdministrationContext, WalletId}
import org.hyperledger.identus.resolvers.DIDResolver
import org.hyperledger.identus.shared.models.{HexString, WalletAccessContext, WalletAdministrationContext, WalletId}
import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds
import org.hyperledger.identus.system.controller.SystemServerEndpoints
import org.hyperledger.identus.verification.controller.VcVerificationServerEndpoints
Expand All @@ -44,8 +44,8 @@ object CloudAgentApp {
_ <- connectDidCommExchangesJob.debug.fork
_ <- syncDIDPublicationStateFromDltJob.debug.fork
_ <- syncRevocationStatusListsJob.debug.fork
_ <- AgentHttpServer.run.fork
fiber <- DidCommHttpServer.run.fork
_ <- AgentHttpServer.run.tapDefect(e => ZIO.logErrorCause("Agent HTTP Server failure", e)).fork
fiber <- DidCommHttpServer.run.tapDefect(e => ZIO.logErrorCause("DIDComm HTTP Server failure", e)).fork
_ <- WebhookPublisher.layer.build.map(_.get[WebhookPublisher]).flatMap(_.run.debug.fork)
_ <- fiber.join *> ZIO.log(s"Server End")
_ <- ZIO.never
Expand Down Expand Up @@ -153,7 +153,7 @@ object AgentHttpServer {
for {
allEndpoints <- agentRESTServiceEndpoints
allEndpointsWithDocumentation = ZHttpEndpoints.withDocumentations[Task](allEndpoints)
server <- ZHttp4sBlazeServer.make
server <- ZHttp4sBlazeServer.make("rest_api")
appConfig <- ZIO.service[AppConfig]
_ <- server.start(allEndpointsWithDocumentation, port = appConfig.agent.httpEndpoint.http.port).debug
} yield ()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,241 +1,19 @@
package org.hyperledger.identus.agent.server

import io.circe.*
import io.circe.parser.*
import org.hyperledger.identus.agent.server.DidCommHttpServerError.{
DIDCommMessageParsingError,
InvalidContentTypeError,
RequestBodyParsingError
}
import org.hyperledger.identus.agent.server.config.AppConfig
import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError
import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError.{KeyNotFoundError, WalletNotFoundError}
import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService
import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage
import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError
import org.hyperledger.identus.connect.core.service.ConnectionService
import org.hyperledger.identus.mercury.*
import org.hyperledger.identus.mercury.DidOps.*
import org.hyperledger.identus.mercury.model.*
import org.hyperledger.identus.mercury.error.*
import org.hyperledger.identus.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse}
import org.hyperledger.identus.mercury.protocol.issuecredential.*
import org.hyperledger.identus.mercury.protocol.presentproof.*
import org.hyperledger.identus.mercury.protocol.revocationnotificaiton.RevocationNotification
import org.hyperledger.identus.pollux.core.model.error.{CredentialServiceError, PresentationError}
import org.hyperledger.identus.pollux.core.service.{CredentialService, PresentationService}
import org.hyperledger.identus.resolvers.DIDResolver
import org.hyperledger.identus.shared.models.WalletAccessContext
import org.hyperledger.identus.agent.server.http.ZHttp4sBlazeServer
import org.hyperledger.identus.didcomm.controller.DIDCommServerEndpoints
import zio.*
import zio.http.*
import java.util.UUID

object DidCommHttpServer {

def run = {
def server(didCommServicePort: Int) = {
val config = Server.Config.default.copy(address = new java.net.InetSocketAddress(didCommServicePort))
ZLayer.succeed(config) >>> Server.live
}
for {
appConfig <- ZIO.service[AppConfig]
didCommServicePort = appConfig.agent.didCommEndpoint.http.port
_ <- ZIO.logInfo(s"Server Started on port $didCommServicePort")
_ <- Server
.serve(didCommServiceEndpoint)
.provideSomeLayer(server(didCommServicePort))
.debug *> ZIO
.logWarning(s"Server STOP (on port $didCommServicePort)")
.tapDefect(error => ZIO.logErrorCause("Defect processing incoming DIDComm message", Cause.fail(error)))
} yield ()
}

private def validateContentType(req: Request) = {
import zio.http.Header.ContentType
for {
contentType <- ZIO
.fromOption(req.rawHeader(ContentType.name))
.mapError(_ => InvalidContentTypeError(s"The '${ContentType.name}' header is required"))
_ <-
if (contentType.equalsIgnoreCase(MediaTypes.contentTypeEncrypted)) ZIO.unit
else ZIO.fail(InvalidContentTypeError(s"Unsupported '${ContentType.name}' header value: $contentType"))
} yield contentType
}

private def didCommServiceEndpoint: HttpApp[
DidOps & CredentialService & PresentationService & ConnectionService & ManagedDIDService & HttpClient &
DIDResolver & DIDNonSecretStorage & AppConfig
] =
val rootRoute = Method.POST / "" -> handler { (req: Request) =>
val result = for {
_ <- validateContentType(req)
body <- req.body.asString.mapError(e => RequestBodyParsingError(e.getMessage))
_ <- webServerProgram(body)
} yield Response.ok
result
.tapError(error => ZIO.logErrorCause("Error processing incoming DIDComm message", Cause.fail(error)))
.catchAll {
case _: RequestBodyParsingError => ZIO.succeed(Response.status(Status.BadRequest))
case _: InvalidContentTypeError => ZIO.succeed(Response.status(Status.BadRequest))
case _: DIDCommMessageParsingError => ZIO.succeed(Response.status(Status.BadRequest))
case _: ParseResponse => ZIO.succeed(Response.status(Status.BadRequest))
case _: DIDSecretStorageError => ZIO.succeed(Response.status(Status.UnprocessableEntity))
case _: ConnectionServiceError => ZIO.succeed(Response.status(Status.UnprocessableEntity))
case _: CredentialServiceError => ZIO.succeed(Response.status(Status.UnprocessableEntity))
case _: PresentationError => ZIO.succeed(Response.status(Status.UnprocessableEntity))
}
}
Routes(rootRoute).toHttpApp

private[this] def extractFirstRecipientDid(jsonMessage: String): IO[ParsingFailure | DecodingFailure, String] = {
val doc = parse(jsonMessage).getOrElse(Json.Null)
val cursor = doc.hcursor
ZIO.fromEither(
cursor.downField("recipients").downArray.downField("header").downField("kid").as[String].map(_.split("#")(0))
)
}

private[this] def unpackMessage(
jsonString: String
): ZIO[
DidOps & ManagedDIDService & DIDNonSecretStorage,
ParseResponse | DIDSecretStorageError,
(Message, WalletAccessContext)
] = {
// Needed for implicit conversion from didcommx UnpackResuilt to mercury UnpackMessage
for {
recipientDid <- extractFirstRecipientDid(jsonString).mapError(err => ParseResponse(err))
_ <- ZIO.logInfo(s"Extracted recipient Did => $recipientDid")
didId = DidId(recipientDid)
nonSecretStorage <- ZIO.service[DIDNonSecretStorage]
maybePeerDIDRecord <- nonSecretStorage.getPeerDIDRecord(didId).orDie
peerDIDRecord <- ZIO.fromOption(maybePeerDIDRecord).mapError(_ => WalletNotFoundError(didId))
_ <- ZIO.logInfo(s"PeerDID record successfully loaded in DIDComm receiver endpoint: $peerDIDRecord")
walletAccessContext = WalletAccessContext(peerDIDRecord.walletId)
managedDIDService <- ZIO.service[ManagedDIDService]
peerDID <- managedDIDService.getPeerDID(didId).provide(ZLayer.succeed(walletAccessContext))
agent = AgentPeerService.makeLayer(peerDID)
msg <- unpack(jsonString).provideSomeLayer(agent)
} yield (msg.message, walletAccessContext)
}

private def webServerProgram(jsonString: String) = {
ZIO.logAnnotate("request-id", UUID.randomUUID.toString) {
for {
_ <- ZIO.logInfo("Received new message")
_ <- ZIO.logTrace(jsonString)
msgAndContext <- unpackMessage(jsonString)
_ <- (handleConnect orElse
handleIssueCredential orElse
handlePresentProof orElse
revocationNotification orElse
handleUnknownMessage)(msgAndContext._1).provideSomeLayer(ZLayer.succeed(msgAndContext._2))
} yield ()
}
}

/*
* Connect
*/
private val handleConnect: PartialFunction[Message, ZIO[
ConnectionService & WalletAccessContext & AppConfig,
DIDCommMessageParsingError | ConnectionServiceError,
Unit
]] = {
case msg if msg.piuri == ConnectionRequest.`type` =>
for {
connectionRequest <- ZIO
.fromEither(ConnectionRequest.fromMessage(msg))
.mapError(DIDCommMessageParsingError.apply)
_ <- ZIO.logInfo("As an Inviter in connect got ConnectionRequest: " + connectionRequest)
connectionService <- ZIO.service[ConnectionService]
config <- ZIO.service[AppConfig]
record <- connectionService.receiveConnectionRequest(
connectionRequest,
Some(config.connect.connectInvitationExpiry)
)
_ <- connectionService.acceptConnectionRequest(record.id)
} yield ()
case msg if msg.piuri == ConnectionResponse.`type` =>
for {
connectionResponse <- ZIO
.fromEither(ConnectionResponse.fromMessage(msg))
.mapError(DIDCommMessageParsingError.apply)
_ <- ZIO.logInfo("As an Invitee in connect got ConnectionResponse: " + connectionResponse)
connectionService <- ZIO.service[ConnectionService]
_ <- connectionService.receiveConnectionResponse(connectionResponse)
} yield ()
}

/*
* Issue Credential
*/
private val handleIssueCredential
: PartialFunction[Message, ZIO[CredentialService & WalletAccessContext, CredentialServiceError, Unit]] = {
case msg if msg.piuri == OfferCredential.`type` =>
for {
offerFromIssuer <- ZIO.succeed(OfferCredential.readFromMessage(msg))
_ <- ZIO.logInfo("As an Holder in issue-credential got OfferCredential: " + offerFromIssuer)
credentialService <- ZIO.service[CredentialService]
_ <- credentialService.receiveCredentialOffer(offerFromIssuer)
} yield ()
case msg if msg.piuri == RequestCredential.`type` =>
for {
requestCredential <- ZIO.succeed(RequestCredential.readFromMessage(msg))
_ <- ZIO.logInfo("As an Issuer in issue-credential got RequestCredential: " + requestCredential)
credentialService <- ZIO.service[CredentialService]
_ <- credentialService.receiveCredentialRequest(requestCredential)
} yield ()
case msg if msg.piuri == IssueCredential.`type` =>
for {
issueCredential <- ZIO.succeed(IssueCredential.readFromMessage(msg))
_ <- ZIO.logInfo("As an Holder in issue-credential got IssueCredential: " + issueCredential)
credentialService <- ZIO.service[CredentialService]
_ <- credentialService.receiveCredentialIssue(issueCredential)
} yield ()
}

/*
* Present Proof
*/
private val handlePresentProof
: PartialFunction[Message, ZIO[PresentationService & WalletAccessContext, PresentationError, Unit]] = {
case msg if msg.piuri == ProposePresentation.`type` =>
for {
proposePresentation <- ZIO.succeed(ProposePresentation.readFromMessage(msg))
_ <- ZIO.logInfo("As a Verifier in present-proof got ProposePresentation: " + proposePresentation)
service <- ZIO.service[PresentationService]
_ <- service.receiveProposePresentation(proposePresentation)
} yield ()
case msg if msg.piuri == RequestPresentation.`type` =>
for {
requestPresentation <- ZIO.succeed(RequestPresentation.readFromMessage(msg))
_ <- ZIO.logInfo("As a Prover in present-proof got RequestPresentation: " + requestPresentation)
service <- ZIO.service[PresentationService]
_ <- service.receiveRequestPresentation(None, requestPresentation)
} yield ()
case msg if msg.piuri == Presentation.`type` =>
for {
presentation <- ZIO.succeed(Presentation.readFromMessage(msg))
_ <- ZIO.logInfo("As a Verifier in present-proof got Presentation: " + presentation)
service <- ZIO.service[PresentationService]
_ <- service.receivePresentation(presentation)
} yield ()
}

private val revocationNotification: PartialFunction[Message, ZIO[Any, Throwable, Unit]] = {
case msg if msg.piuri == RevocationNotification.`type` =>
for {
revocationNotification <- ZIO.attempt(RevocationNotification.readFromMessage(msg))
_ <- ZIO.logInfo("Got RevocationNotification: " + revocationNotification)
} yield ()
}

/*
* Unknown Message
*/
private val handleUnknownMessage: PartialFunction[Message, UIO[String]] = { case _ =>
ZIO.succeed("Unknown Message Type")
}
def run = for {
allEndpoints <- DIDCommServerEndpoints.all
server <- ZHttp4sBlazeServer.make("didcomm")
appConfig <- ZIO.service[AppConfig]
didCommServicePort = appConfig.agent.didCommEndpoint.http.port
_ <- ZIO.logInfo(s"Running DIDComm Server on port '$didCommServicePort''")
_ <- server.start(allEndpoints, didCommServicePort).debug
} yield ()

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import org.hyperledger.identus.resolvers.DIDResolver
import org.hyperledger.identus.system.controller.SystemControllerImpl
import org.hyperledger.identus.verification.controller.VcVerificationControllerImpl
import io.micrometer.prometheus.{PrometheusConfig, PrometheusMeterRegistry}
import org.hyperledger.identus.didcomm.controller.DIDCommControllerImpl
import zio.*
import zio.metrics.connectors.micrometer
import zio.metrics.connectors.micrometer.MicrometerConfig
Expand Down Expand Up @@ -163,6 +164,7 @@ object MainApp extends ZIOAppDefault {
EntityControllerImpl.layer,
WalletManagementControllerImpl.layer,
EventControllerImpl.layer,
DIDCommControllerImpl.layer,
// domain
AppModule.apolloLayer,
AppModule.didJwtResolverLayer,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package org.hyperledger.identus.agent.server.http

import org.hyperledger.identus.api.http.ErrorResponse
import org.hyperledger.identus.system.controller.SystemEndpoints
import io.micrometer.prometheus.PrometheusMeterRegistry
import org.http4s.*
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.server.Router
import org.hyperledger.identus.api.http.ErrorResponse
import org.hyperledger.identus.system.controller.SystemEndpoints
import sttp.tapir.*
import sttp.tapir.server.http4s.Http4sServerOptions
import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
Expand All @@ -14,10 +14,10 @@ import sttp.tapir.ztapir.ZServerEndpoint
import zio.*
import zio.interop.catz.*

class ZHttp4sBlazeServer(micrometerRegistry: PrometheusMeterRegistry) {
class ZHttp4sBlazeServer(micrometerRegistry: PrometheusMeterRegistry, metricsNamespace: String) {

private val tapirPrometheusMetricsZIO: Task[PrometheusMetrics[Task]] = ZIO.attempt {
PrometheusMetrics.default[Task](registry = micrometerRegistry.getPrometheusRegistry)
PrometheusMetrics.default[Task](namespace = metricsNamespace, registry = micrometerRegistry.getPrometheusRegistry)
}

private val serverOptionsZIO: ZIO[PrometheusMetrics[Task], Throwable, Http4sServerOptions[Task]] = for {
Expand Down Expand Up @@ -69,10 +69,10 @@ class ZHttp4sBlazeServer(micrometerRegistry: PrometheusMeterRegistry) {
}

object ZHttp4sBlazeServer {
def make: URIO[PrometheusMeterRegistry, ZHttp4sBlazeServer] = {
def make(metricsNamespace: String): URIO[PrometheusMeterRegistry, ZHttp4sBlazeServer] = {
for {
micrometerRegistry <- ZIO.service[PrometheusMeterRegistry]
zHttp4sBlazeServer = ZHttp4sBlazeServer(micrometerRegistry)
zHttp4sBlazeServer = ZHttp4sBlazeServer(micrometerRegistry, metricsNamespace)
} yield zHttp4sBlazeServer
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import zio.ZIO
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder}

import java.util.UUID
import scala.language.implicitConversions
import scala.util.matching.Regex

private val INSTANCE_URI_PREFIX = "error:instance:"
Expand Down Expand Up @@ -40,16 +41,18 @@ object ErrorResponse {
given schema: Schema[ErrorResponse] = Schema.derived

private val CamelCaseSplitRegex: Regex = "(([A-Z]?[a-z]+)|([A-Z]))".r
given failureToErrorResponseConversion[R, A]: Conversion[ZIO[R, Failure, A], ZIO[R, ErrorResponse, A]] = { effect =>
effect.mapError { failure =>
val simpleName = failure.getClass.getSimpleName
ErrorResponse(
failure.statusCode.code,
s"error:ConnectionServiceError:$simpleName",
CamelCaseSplitRegex.findAllIn(simpleName).mkString(" "),
Some(failure.userFacingMessage)
)
}
given failureToErrorResponseConversionZIO[R, A]: Conversion[ZIO[R, Failure, A], ZIO[R, ErrorResponse, A]] = {
effect => effect.mapError { failure => failure }
}

given failureToErrorResponseConversion[R, A]: Conversion[Failure, ErrorResponse] = { failure =>
val simpleName = failure.getClass.getSimpleName
ErrorResponse(
failure.statusCode.code,
s"error:${failure.namespace}:$simpleName",
CamelCaseSplitRegex.findAllIn(simpleName).mkString(" "),
Some(failure.userFacingMessage)
)
}

object annotations {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.hyperledger.identus.didcomm.controller

import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext}
import org.hyperledger.identus.didcomm.controller.http.DIDCommMessage
import zio.IO

trait DIDCommController {
def handleDIDCommMessage(msg: DIDCommMessage)(implicit rc: RequestContext): IO[ErrorResponse, Unit]
}
Loading

0 comments on commit 2f4f7c3

Please sign in to comment.