diff --git a/prism-agent/service/api/http/connect/schemas.yaml b/prism-agent/service/api/http/connect/schemas.yaml new file mode 100644 index 0000000000..6dbdfc6562 --- /dev/null +++ b/prism-agent/service/api/http/connect/schemas.yaml @@ -0,0 +1,149 @@ +components: + parameters: + connectionId: + in: path + name: connectionId + required: true + description: Id of connection state record. + schema: + type: string + schemas: + CreateConnectionRequest: + type: object + properties: + label: + type: string + example: "Peter" + + AcceptConnectionInvitationRequest: + type: object + required: + - invitation + properties: + invitation: + type: string + example: "eyJAaWQiOiIzZmE4NWY2NC01NzE3LTQ1NjItYjNmYy0yYzk2M2Y2NmFmYTYiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvbXktZmFtaWx5LzEuMC9teS1tZXNzYWdlLXR5cGUiLCJkaWQiOiJXZ1d4cXp0ck5vb0c5MlJYdnhTVFd2IiwiaW1hZ2VVcmwiOiJodHRwOi8vMTkyLjE2OC41Ni4xMDEvaW1nL2xvZ28uanBnIiwibGFiZWwiOiJCb2IiLCJyZWNpcGllbnRLZXlzIjpbIkgzQzJBVnZMTXY2Z21NTmFtM3VWQWpacGZrY0pDd0R3blpuNnozd1htcVBWIl0sInJvdXRpbmdLZXlzIjpbIkgzQzJBVnZMTXY2Z21NTmFtM3VWQWpacGZrY0pDd0R3blpuNnozd1htcVBWIl0sInNlcnZpY2VFbmRwb2ludCI6Imh0dHA6Ly8xOTIuMTY4LjU2LjEwMTo4MDIwIn0=" + + Connection: + type: object + allOf: + - $ref: "#/components/schemas/CreateConnectionRequest" + - type: object + required: + - self + - kind + - connectionId + - state + - createdAt + - invitation + properties: + self: + type: string + example: https://atala-prism-products.io/connections/ABCD-1234 + kind: + type: string + example: ConnectionState + connectionId: + type: string + format: uuid + example: "12345-9876" + myDid: + type: string + example: "did:prism:12345" + theirDid: + type: string + example: "did:peer:12345" + state: + type: string + enum: ["pending", "success", "failed"] + createdAt: + type: string + format: date-time + example: 2021-10-31T09:22:23Z + updatedAt: + type: string + format: date-time + example: 2021-12-31T13:59:60Z + invitation: + $ref: "#/components/schemas/ConnectionInvitation" + + ConnectionCollection: + type: object + required: + - self + - kind + - contents + properties: + self: + type: string + example: https://atala-prism-products.io/connections + kind: + type: string + example: Collection + contents: + type: array + items: + $ref: "#/components/schemas/Connection" + + ConnectionInvitation: + type: object + required: + - id + - type + - from + - invitationUrl + properties: + id: + type: string + format: uuid + description: The invitation identifier used as parent thread ID (pthid) for the response message that follows. + example: "3fa85f64-5717-4562-b3fc-2c963f66afa6" + type: + type: string + description: The DIDComm Message Type URI (MTURI) the invitation message coplies with. + example: "https://didcomm.org/out-of-band/2.0/invitation" + from: + type: string + description: The DID representing the sender to be used by recipients for future interactions. + example: "did:prism:1234457" + invitationUrl: + type: string + description: The invitation message encoded as a URL. + example: https://domain.com/path?_oob=eyJAaWQiOiIzZmE4NWY2NC01NzE3LTQ1NjItYjNmYy0yYzk2M2Y2NmFmYTYiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvbXktZmFtaWx5LzEuMC9teS1tZXNzYWdlLXR5cGUiLCJkaWQiOiJXZ1d4cXp0ck5vb0c5MlJYdnhTVFd2IiwiaW1hZ2VVcmwiOiJodHRwOi8vMTkyLjE2OC41Ni4xMDEvaW1nL2xvZ28uanBnIiwibGFiZWwiOiJCb2IiLCJyZWNpcGllbnRLZXlzIjpbIkgzQzJBVnZMTXY2Z21NTmFtM3VWQWpacGZrY0pDd0R3blpuNnozd1htcVBWIl0sInJvdXRpbmdLZXlzIjpbIkgzQzJBVnZMTXY2Z21NTmFtM3VWQWpacGZrY0pDd0R3blpuNnozd1htcVBWIl0sInNlcnZpY2VFbmRwb2ludCI6Imh0dHA6Ly8xOTIuMTY4LjU2LjEwMTo4MDIwIn0= + + ErrorResponse: + type: object + description: An RFC-7807 compliant data structure for reporting errors to the client + required: + - type + - title + - status + - instance + properties: + type: + type: string + description: A URI reference that identifies the problem type. + example: https://example.org/doc/#model-MalformedEmail + title: + type: string + example: "Malformed email" + description: |- + A short, human-readable summary of the problem type. It does not + change from occurrence to occurrence of the problem. + status: + type: integer + format: int32 + example: 400 + description: |- + The HTTP status code for this occurrence of the problem. + detail: + type: string + description: |- + A human-readable explanation specific to this occurrence of the problem. + example: "The received '{}à!è@!.b}' email does not conform to the email format" + instance: + type: string + example: "/problems/d914e" + description: |- + A URI reference that identifies the specific occurrence of the problem. + It may or may not yield further information if dereferenced. diff --git a/prism-agent/service/api/http/prism-agent-openapi-spec.yaml b/prism-agent/service/api/http/prism-agent-openapi-spec.yaml index 036b098661..716c57a397 100644 --- a/prism-agent/service/api/http/prism-agent-openapi-spec.yaml +++ b/prism-agent/service/api/http/prism-agent-openapi-spec.yaml @@ -39,6 +39,10 @@ tags: description: Verifiable Credentials revocation REST API - name: Present Proof description: Present Proof REST API + # Connect + - name: Connections Management + description: API for driving connection process. + paths: # ---------------------------------- @@ -926,3 +930,115 @@ paths: application/json: schema: $ref: "./pollux/schemas.yaml#/components/schemas/W3CPresentation" + + # ---------------------------------- + # Connect + # ---------------------------------- + /connections: + post: + tags: ["Connections Management"] + operationId: createConnection + summary: Creates new connection and returns an invitation. + description: |- + Returns new invitation object and creates new connection state record in `pending` state. + Content of invitation depends on DIDComm protocol used, here is an example of how it would look like for `AIP 1.0 connection/v1` protocol. + Once connection invitation is accepted, Agent should filter all additional attempts to accept it. + We consider mult-party connections as out of scope for now. + requestBody: + required: true + content: + application/json: + schema: + $ref: "./connect/schemas.yaml#/components/schemas/CreateConnectionRequest" + + responses: + "201": + description: The connection record + content: + application/json: + schema: + $ref: "./connect/schemas.yaml#/components/schemas/Connection" + "422": + description: The connection record creation failed. + content: + application/json: + schema: + $ref: "./connect/schemas.yaml#/components/schemas/ErrorResponse" + + get: + tags: ["Connections Management"] + operationId: getConnections + summary: Returns a list of connections. + responses: + "200": + description: List of connection state objects + content: + application/json: + schema: + $ref: "./connect/schemas.yaml#/components/schemas/ConnectionCollection" + "500": + description: Retrieving the connection records failed for an unexpected reason. + content: + application/json: + schema: + $ref: "./connect/schemas.yaml#/components/schemas/ErrorResponse" + + /connections/{connectionId}: + get: + tags: ["Connections Management"] + parameters: + - $ref: "./connect/schemas.yaml#/components/parameters/connectionId" + operationId: getConnection + summary: Returns an existing connection record by id. + responses: + "200": + description: Connection state record + content: + application/json: + schema: + $ref: "./connect/schemas.yaml#/components/schemas/Connection" + "404": + description: There is no issue credential record matching the given 'recordId'. + content: + application/json: + schema: + $ref: "./connect/schemas.yaml#/components/schemas/ErrorResponse" + delete: + tags: ["Connections Management"] + parameters: + - $ref: "./connect/schemas.yaml#/components/parameters/connectionId" + operationId: deleteConnection + summary: Deletes existing connection record. + description: Just deletes connection state in the Agent, it does not include notifing other party that connection is deleted. We should consider this feature for the future. If additional action is attempted over deleted connection, it should thow error (no matter which side deleted connection). + responses: + "200": + description: Successful delete + "404": + description: Connection state record not found for given `connectionId` + + /connection-invitations: + post: + tags: ["Connections Management"] + operationId: acceptConnectionInvitation + summary: Accepts externally received invitation. + description: Creates new connection state record in `pending` state. It is assumed that application would first decode and validate the invitation. When it is accepted in the application side, it should be submitted in raw format to this API. + requestBody: + required: true + content: + application/json: + schema: + $ref: "./connect/schemas.yaml#/components/schemas/AcceptConnectionInvitationRequest" + + responses: + "200": + description: Invitation successfully accepted. + content: + application/json: + schema: + $ref: "./connect/schemas.yaml#/components/schemas/Connection" + "500": + description: Storing the connection invitation failed for an unexpected reason. + content: + application/json: + schema: + $ref: "./connect/schemas.yaml#/components/schemas/ErrorResponse" diff --git a/prism-agent/service/project/Dependencies.scala b/prism-agent/service/project/Dependencies.scala index f7ca91b73a..dced7ac5f2 100644 --- a/prism-agent/service/project/Dependencies.scala +++ b/prism-agent/service/project/Dependencies.scala @@ -9,10 +9,11 @@ object Dependencies { val akka = "2.6.20" val akkaHttp = "10.2.9" val castor = "0.2.0" - val pollux = "0.3.0" + val pollux = "0.4.0" + val connect = "0.1.0" val bouncyCastle = "1.70" val logback = "1.4.4" - val mercury = "0.6.0" + val mercury = "0.7.0" val zioJson = "0.3.0" val tapir = "1.2.2" } @@ -41,6 +42,9 @@ object Dependencies { private lazy val polluxCore = "io.iohk.atala" %% "pollux-core" % Versions.pollux private lazy val polluxSqlDoobie = "io.iohk.atala" %% "pollux-sql-doobie" % Versions.pollux + private lazy val connectCore = "io.iohk.atala" %% "connect-core" % Versions.connect + private lazy val connectSqlDoobie = "io.iohk.atala" %% "connect-sql-doobie" % Versions.connect + private lazy val mercuryAgent = "io.iohk.atala" %% "mercury-agent-didcommx" % Versions.mercury // Added here to make prism-crypto works. @@ -66,6 +70,7 @@ object Dependencies { private lazy val castorDependencies: Seq[ModuleID] = Seq(castorCore, castorSqlDoobie) private lazy val polluxDependencies: Seq[ModuleID] = Seq(polluxCore, polluxSqlDoobie) private lazy val mercuryDependencies: Seq[ModuleID] = Seq(mercuryAgent) + private lazy val connectDependencies: Seq[ModuleID] = Seq(connectCore, connectSqlDoobie) private lazy val akkaHttpDependencies: Seq[ModuleID] = Seq(akkaTyped, akkaStream, akkaHttp, akkaSprayJson).map(_.cross(CrossVersion.for3Use2_13)) private lazy val bouncyDependencies: Seq[ModuleID] = Seq(bouncyBcpkix, bouncyBcprov) @@ -92,5 +97,6 @@ object Dependencies { castorDependencies ++ polluxDependencies ++ mercuryDependencies ++ + connectDependencies ++ tapirDependencies } diff --git a/prism-agent/service/server/src/main/resources/application.conf b/prism-agent/service/server/src/main/resources/application.conf index 8b9715cbf9..f95da389eb 100644 --- a/prism-agent/service/server/src/main/resources/application.conf +++ b/prism-agent/service/server/src/main/resources/application.conf @@ -37,6 +37,21 @@ pollux { } } +connect { + database { + host = "localhost" + host = ${?CONNECT_DB_HOST} + port = 5433 + port = ${?CONNECT_DB_PORT} + databaseName = "connect" + databaseName = ${?CONNECT_DB_NAME} + username = "postgres" + username = ${?CONNECT_DB_USER} + password = "postgres" + password = ${?CONNECT_DB_PASSWORD} + } +} + agent { httpEndpoint { http { 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 f03d171290..5b7d5581d9 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 @@ -6,6 +6,7 @@ import org.didcommx.didcomm.DIDComm import io.iohk.atala.resolvers.UniversalDidResolver import io.iohk.atala.castor.sql.repository.{Migrations => CastorMigrations} import io.iohk.atala.pollux.sql.repository.{Migrations => PolluxMigrations} +import io.iohk.atala.connect.sql.repository.{Migrations => ConnectMigrations} object Main extends ZIOAppDefault { def agentLayer(peer: PeerDID): ZLayer[Any, Nothing, AgentServiceAny] = ZLayer.succeed( @@ -54,6 +55,9 @@ object Main extends ZIOAppDefault { _ <- ZIO .serviceWithZIO[PolluxMigrations](_.migrate) .provide(RepoModule.polluxDbConfigLayer >>> PolluxMigrations.layer) + _ <- ZIO + .serviceWithZIO[ConnectMigrations](_.migrate) + .provide(RepoModule.connectDbConfigLayer >>> ConnectMigrations.layer) agentDID <- for { peer <- ZIO.succeed(PeerDID.makePeerDid(serviceEndpoint = Some(s"http://localhost:$didCommServicePort"))) @@ -69,9 +73,14 @@ object Main extends ZIOAppDefault { .debug .fork + connectDidCommExchangesFiber <- Modules.connectDidCommExchangesJob + .provide(didCommLayer) + .debug + .fork + didCommServiceFiber <- Modules .didCommServiceEndpoint(didCommServicePort) - .provide(didCommLayer, AppModule.credentialServiceLayer) + .provide(didCommLayer, AppModule.credentialServiceLayer, AppModule.connectionServiceLayer) .debug .fork 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 18ea3aa94a..d7345da8cf 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 @@ -13,14 +13,16 @@ import io.iohk.atala.agent.server.http.marshaller.{ DIDAuthenticationApiMarshallerImpl, DIDOperationsApiMarshallerImpl, DIDRegistrarApiMarshallerImpl, - IssueCredentialsApiMarshallerImpl + IssueCredentialsApiMarshallerImpl, + ConnectionsManagementApiMarshallerImpl } import io.iohk.atala.agent.server.http.service.{ DIDApiServiceImpl, DIDAuthenticationApiServiceImpl, DIDOperationsApiServiceImpl, DIDRegistrarApiServiceImpl, - IssueCredentialsApiServiceImpl + IssueCredentialsApiServiceImpl, + ConnectionsManagementApiServiceImpl } import io.iohk.atala.castor.core.repository.DIDOperationRepository import io.iohk.atala.agent.openapi.api.{ @@ -28,7 +30,8 @@ import io.iohk.atala.agent.openapi.api.{ DIDAuthenticationApi, DIDOperationsApi, DIDRegistrarApi, - IssueCredentialsApi + IssueCredentialsApi, + ConnectionsManagementApi } import io.iohk.atala.castor.sql.repository.{JdbcDIDOperationRepository, TransactorLayer} import zio.* @@ -55,6 +58,7 @@ import io.iohk.atala.pollux.core.repository.CredentialRepository import io.iohk.atala.pollux.core.service.CredentialService import io.iohk.atala.pollux.sql.repository.JdbcCredentialRepository import io.iohk.atala.pollux.sql.repository.{DbConfig => PolluxDbConfig} +import io.iohk.atala.connect.sql.repository.{DbConfig => ConnectDbConfig} import io.iohk.atala.agent.server.jobs.* import zio.* import zio.config.typesafe.TypesafeConfigSource @@ -76,6 +80,13 @@ import java.io.IOException import cats.implicits.* import io.iohk.atala.pollux.schema.SchemaRegistryServerEndpoints import io.iohk.atala.pollux.service.SchemaRegistryServiceInMemory +import io.iohk.atala.connect.core.service.ConnectionService +import io.iohk.atala.connect.core.service.ConnectionServiceImpl +import io.iohk.atala.connect.core.repository.ConnectionRepository +import io.iohk.atala.connect.sql.repository.JdbcConnectionRepository +import io.iohk.atala.mercury.protocol.connection.ConnectionRequest +import io.iohk.atala.mercury.protocol.connection.ConnectionResponse +import io.iohk.atala.connect.core.model.error.ConnectionError object Modules { @@ -102,7 +113,7 @@ object Modules { def didCommServiceEndpoint(port: Int) = { val header = "content-type" -> MediaTypes.contentTypeEncrypted - val app: HttpApp[DidComm with CredentialService, Throwable] = + val app: HttpApp[DidComm & CredentialService & ConnectionService, Throwable] = Http.collectZIO[Request] { // // TODO add DIDComm messages parsing logic here! // Response.text("Hello World!").setStatus(Status.Accepted) @@ -136,15 +147,23 @@ object Modules { .unit .provideSomeLayer(AppModule.credentialServiceLayer) + val connectDidCommExchangesJob: RIO[DidComm, Unit] = + ConnectBackgroundJobs.didCommExchanges + .repeat(Schedule.spaced(10.seconds)) + .unit + .provideSomeLayer(AppModule.connectionServiceLayer) + def webServerProgram( jsonString: String - ): ZIO[DidComm with CredentialService, MercuryThrowable, Unit] = { + ): ZIO[DidComm & CredentialService & ConnectionService, MercuryThrowable, Unit] = { import io.iohk.atala.mercury.DidComm.* ZIO.logAnnotate("request-id", java.util.UUID.randomUUID.toString()) { for { _ <- ZIO.logInfo("Received new message") _ <- ZIO.logTrace(jsonString) msg <- unpack(jsonString).map(_.getMessage) + credentialService <- ZIO.service[CredentialService] + connectionService <- ZIO.service[ConnectionService] ret <- { msg.piuri match { // ######################## @@ -165,7 +184,6 @@ object Modules { _ <- ZIO.logInfo("*" * 100) _ <- ZIO.logInfo("As an Holder in issue-credential:") _ <- ZIO.logInfo("Got OfferCredential: " + msg) - credentialService <- ZIO.service[CredentialService] offerFromIssuer = OfferCredential.readFromMessage(msg) _ <- credentialService .receiveCredentialOffer(offerFromIssuer) @@ -211,10 +229,50 @@ object Modules { .catchAll { case ex: IOException => ZIO.fail(ex) } } yield () + case s if s == ConnectionRequest.`type` => + for { + _ <- ZIO.logInfo("*" * 100) + _ <- ZIO.logInfo("As an Inviter in connect:") + connectionRequest = ConnectionRequest.readFromMessage(msg) + _ <- ZIO.logInfo("Got ConnectionRequest: " + connectionRequest) + // Receive and store ConnectionRequest + maybeRecord <- connectionService + .receiveConnectionRequest(connectionRequest) + .catchSome { case ConnectionError.RepositoryError(cause) => + ZIO.logError(cause.getMessage()) *> + ZIO.fail(cause) + } + .catchAll { case ex: IOException => ZIO.fail(ex) } + // Accept the ConnectionRequest + _ <- connectionService + .acceptConnectionRequest(maybeRecord.get.id) // TODO: get + .catchSome { case ConnectionError.RepositoryError(cause) => + ZIO.logError(cause.getMessage()) *> + ZIO.fail(cause) + } + .catchAll { case ex: IOException => ZIO.fail(ex) } + } yield () + + // As an Invitee, I received a ConnectionResponse from an Inviter who replied to my ConnectionRequest. + case s if s == ConnectionResponse.`type` => + for { + _ <- ZIO.logInfo("*" * 100) + _ <- ZIO.logInfo("As an Invitee in connect:") + connectionResponse = ConnectionResponse.readFromMessage(msg) + _ <- ZIO.logInfo("Got ConnectionResponse: " + connectionResponse) + _ <- connectionService + .receiveConnectionResponse(connectionResponse) + .catchSome { case ConnectionError.RepositoryError(cause) => + ZIO.logError(cause.getMessage()) *> + ZIO.fail(cause) + } + .catchAll { case ex: IOException => ZIO.fail(ex) } + } yield () + case _ => ZIO.succeed("Unknown Message Type") } } - } yield (ret) + } yield () } } @@ -263,6 +321,9 @@ object AppModule { val credentialServiceLayer: RLayer[DidComm, CredentialService] = (GrpcModule.layers ++ RepoModule.layers) >>> CredentialServiceImpl.layer + + val connectionServiceLayer: RLayer[DidComm, ConnectionService] = + (GrpcModule.layers ++ RepoModule.layers) >>> ConnectionServiceImpl.layer } object GrpcModule { @@ -324,8 +385,15 @@ object HttpModule { (apiServiceLayer ++ apiMarshallerLayer) >>> ZLayer.fromFunction(new IssueCredentialsProtocolApi(_, _)) } + val connectionsManagementApiLayer: RLayer[DidComm, ConnectionsManagementApi] = { + val serviceLayer = AppModule.connectionServiceLayer + val apiServiceLayer = serviceLayer >>> ConnectionsManagementApiServiceImpl.layer + val apiMarshallerLayer = ConnectionsManagementApiMarshallerImpl.layer + (apiServiceLayer ++ apiMarshallerLayer) >>> ZLayer.fromFunction(new ConnectionsManagementApi(_, _)) + } + val layers = - didApiLayer ++ didOperationsApiLayer ++ didAuthenticationApiLayer ++ didRegistrarApiLayer ++ issueCredentialsApiLayer ++ issueCredentialsProtocolApiLayer + didApiLayer ++ didOperationsApiLayer ++ didAuthenticationApiLayer ++ didRegistrarApiLayer ++ issueCredentialsApiLayer ++ issueCredentialsProtocolApiLayer ++ connectionsManagementApiLayer } object RepoModule { @@ -381,11 +449,40 @@ object RepoModule { polluxDbConfigLayer >>> transactorLayer } + val connectDbConfigLayer: TaskLayer[ConnectDbConfig] = { + val dbConfigLayer = ZLayer.fromZIO { + ZIO.service[AppConfig].map(_.connect.database) map { config => + ConnectDbConfig( + username = config.username, + password = config.password, + jdbcUrl = s"jdbc:postgresql://${config.host}:${config.port}/${config.databaseName}", + awaitConnectionThreads = 2 + ) + } + } + SystemModule.configLayer >>> dbConfigLayer + } + + val connectTransactorLayer: TaskLayer[Transactor[Task]] = { + val transactorLayer = ZLayer.fromZIO { + ZIO.service[ConnectDbConfig].flatMap { config => + Dispatcher[Task].allocated.map { case (dispatcher, _) => + given Dispatcher[Task] = dispatcher + io.iohk.atala.connect.sql.repository.TransactorLayer.hikari[Task](config) + } + } + }.flatten + connectDbConfigLayer >>> transactorLayer + } + val didOperationRepoLayer: TaskLayer[DIDOperationRepository[Task]] = castorTransactorLayer >>> JdbcDIDOperationRepository.layer val credentialRepoLayer: TaskLayer[CredentialRepository[Task]] = polluxTransactorLayer >>> JdbcCredentialRepository.layer - val layers = didOperationRepoLayer ++ credentialRepoLayer + val connectionRepoLayer: TaskLayer[ConnectionRepository[Task]] = + connectTransactorLayer >>> JdbcConnectionRepository.layer + + val layers = didOperationRepoLayer ++ credentialRepoLayer ++ connectionRepoLayer } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala index 57db4989e1..b8d2fb53d0 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala @@ -7,7 +7,8 @@ final case class AppConfig( iris: IrisConfig, castor: CastorConfig, pollux: PolluxConfig, - agent: AgentConfig + agent: AgentConfig, + connect: ConnectConfig ) object AppConfig { @@ -18,6 +19,7 @@ final case class IrisConfig(service: GrpcServiceConfig) final case class CastorConfig(database: DatabaseConfig) final case class PolluxConfig(database: DatabaseConfig) +final case class ConnectConfig(database: DatabaseConfig) final case class GrpcServiceConfig(host: String, port: Int) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/HttpRoutes.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/HttpRoutes.scala index aae9e27a3d..e355797cca 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/HttpRoutes.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/HttpRoutes.scala @@ -7,7 +7,8 @@ import io.iohk.atala.agent.openapi.api.{ DIDAuthenticationApi, DIDOperationsApi, DIDRegistrarApi, - IssueCredentialsApi + IssueCredentialsApi, + ConnectionsManagementApi } import zio.* import io.iohk.atala.agent.openapi.api.IssueCredentialsProtocolApi @@ -17,7 +18,7 @@ object HttpRoutes { def routes: URIO[ DIDApi & DIDOperationsApi & DIDAuthenticationApi & DIDRegistrarApi & IssueCredentialsApi & - IssueCredentialsProtocolApi, + IssueCredentialsProtocolApi & ConnectionsManagementApi, Route ] = for { @@ -27,7 +28,8 @@ object HttpRoutes { disRegistrarApi <- ZIO.service[DIDRegistrarApi] issueCredentialApi <- ZIO.service[IssueCredentialsApi] issueCredentialsProtocolApi <- ZIO.service[IssueCredentialsProtocolApi] - } yield didApi.route ~ didOperationsApi.route ~ didAuthApi.route ~ disRegistrarApi.route ~ issueCredentialApi.route ~ issueCredentialsProtocolApi.route ~ additionalRoute + connectionsManagementApi <- ZIO.service[ConnectionsManagementApi] + } yield didApi.route ~ didOperationsApi.route ~ didAuthApi.route ~ disRegistrarApi.route ~ issueCredentialApi.route ~ issueCredentialsProtocolApi.route ~ connectionsManagementApi.route ~ additionalRoute private def additionalRoute: Route = { // swagger-ui expects this particular header when resolving relative $ref diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/ConnectionsManagementApiMarshallerImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/ConnectionsManagementApiMarshallerImpl.scala new file mode 100644 index 0000000000..575c5561d4 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/ConnectionsManagementApiMarshallerImpl.scala @@ -0,0 +1,31 @@ +package io.iohk.atala.agent.server.http.marshaller + +import io.circe.Json +import zio._ +import io.iohk.atala.agent.openapi.api.ConnectionsManagementApiMarshaller +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import io.iohk.atala.agent.openapi.model.* +import spray.json.RootJsonFormat + +object ConnectionsManagementApiMarshallerImpl extends JsonSupport { + val layer: ULayer[ConnectionsManagementApiMarshaller] = ZLayer.succeed { + new ConnectionsManagementApiMarshaller { + implicit def fromEntityUnmarshallerCreateConnectionRequest: FromEntityUnmarshaller[CreateConnectionRequest] = + summon[RootJsonFormat[CreateConnectionRequest]] + + implicit def fromEntityUnmarshallerAcceptConnectionInvitationRequest + : FromEntityUnmarshaller[AcceptConnectionInvitationRequest] = + summon[RootJsonFormat[AcceptConnectionInvitationRequest]] + + implicit def toEntityMarshallerConnectionCollection: ToEntityMarshaller[ConnectionCollection] = + summon[RootJsonFormat[ConnectionCollection]] + + implicit def toEntityMarshallerConnection: ToEntityMarshaller[Connection] = + summon[RootJsonFormat[Connection]] + + implicit def toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] = + summon[RootJsonFormat[ErrorResponse]] + } + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/JsonSupport.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/JsonSupport.scala index 469d7c7b88..a78d2ebae5 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/JsonSupport.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/JsonSupport.scala @@ -103,4 +103,11 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { given RootJsonFormat[W3CSchemaMeta] = jsonFormat6(W3CSchemaMeta.apply) given RootJsonFormat[W3CSchemaPaginated] = jsonFormat4(W3CSchemaPaginated.apply) + // Connections Management + given RootJsonFormat[CreateConnectionRequest] = jsonFormat1(CreateConnectionRequest.apply) + given RootJsonFormat[AcceptConnectionInvitationRequest] = jsonFormat1(AcceptConnectionInvitationRequest.apply) + given RootJsonFormat[ConnectionCollection] = jsonFormat3(ConnectionCollection.apply) + given RootJsonFormat[Connection] = jsonFormat10(Connection.apply) + given RootJsonFormat[ConnectionInvitation] = jsonFormat4(ConnectionInvitation.apply) + } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASDomainModelHelper.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASDomainModelHelper.scala index 98206889b3..75bf4477c2 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASDomainModelHelper.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASDomainModelHelper.scala @@ -16,6 +16,7 @@ import io.iohk.atala.castor.core.model.did as castorDomain import io.iohk.atala.castor.core.model.did.PublishedDIDOperation import io.iohk.atala.agent.walletapi.model as walletDomain import io.iohk.atala.pollux.core.model as polluxdomain +import io.iohk.atala.connect.core.model as connectdomain import io.iohk.atala.shared.models.HexStrings.* import io.iohk.atala.shared.models.Base64UrlStrings.* import io.iohk.atala.shared.utils.Traverse.* @@ -28,6 +29,12 @@ import java.time.OffsetDateTime import java.time.ZoneOffset import io.iohk.atala.mercury.model.AttachmentDescriptor import io.iohk.atala.mercury.model.Base64 +import io.iohk.atala.agent.openapi.model.Connection +import io.iohk.atala.agent.openapi.model.ConnectionInvitation +import zio.ZIO +import io.iohk.atala.agent.server.http.model.HttpServiceError.InvalidPayload +import java.util.UUID +import io.iohk.atala.connect.core.model.ConnectionRecord.Role trait OASDomainModelHelper { @@ -163,5 +170,41 @@ trait OASDomainModelHelper { }) ) } + extension (domain: connectdomain.ConnectionRecord) { + def toOAS: Connection = Connection( + label = domain.label, + self = "Connection", + kind = s"/connections/${domain.id.toString}", + connectionId = domain.id, + myDid = domain.role match + case Role.Inviter => + domain.connectionResponse.map(_.from).orElse(domain.connectionRequest.map(_.to)).map(_.value) + case Role.Invitee => + domain.connectionResponse.map(_.to).orElse(domain.connectionRequest.map(_.from)).map(_.value) + , + theirDid = domain.role match + case Role.Inviter => + domain.connectionResponse.map(_.to).orElse(domain.connectionRequest.map(_.from)).map(_.value) + case Role.Invitee => + domain.connectionResponse.map(_.from).orElse(domain.connectionRequest.map(_.to)).map(_.value) + , + state = domain.protocolState.toString, + createdAt = domain.createdAt.atOffset(ZoneOffset.UTC), + updatedAt = domain.updatedAt.map(_.atOffset(ZoneOffset.UTC)), + invitation = ConnectionInvitation( + id = UUID.fromString(domain.invitation.id), + `type` = domain.invitation.`type`, + from = domain.invitation.from.value, + invitationUrl = s"https://domain.com/path?_oob=${domain.invitation.toBase64}" + ) + ) + } + + extension (str: String) { + def toUUID: ZIO[Any, InvalidPayload, UUID] = + ZIO + .fromTry(Try(UUID.fromString(str))) + .mapError(e => HttpServiceError.InvalidPayload(s"Error parsing string as UUID: ${e.getMessage()}")) + } } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASErrorModelHelper.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASErrorModelHelper.scala index 42faea3ba2..244fa6faba 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASErrorModelHelper.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASErrorModelHelper.scala @@ -6,6 +6,7 @@ import io.iohk.atala.agent.walletapi.model.error.{CreateManagedDIDError, Publish import io.iohk.atala.castor.core.model.error.DIDOperationError import java.util.UUID import io.iohk.atala.pollux.core.model.error.IssueCredentialError +import io.iohk.atala.connect.core.model.error.ConnectionError trait ToErrorResponse[E] { def toErrorResponse(e: E): ErrorResponse @@ -77,6 +78,18 @@ trait OASErrorModelHelper { } } + given ToErrorResponse[ConnectionError] with { + def toErrorResponse(error: ConnectionError): ErrorResponse = { + ErrorResponse( + `type` = "error-type", + title = "error-title", + status = 500, + detail = Some(error.toString), + instance = "error-instance" + ) + } + } + def notFoundErrorResponse(detail: Option[String] = None) = ErrorResponse( `type` = "not-found", title = "Resource not found", diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/ConnectionsManagementApiServiceImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/ConnectionsManagementApiServiceImpl.scala new file mode 100644 index 0000000000..efc3376184 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/ConnectionsManagementApiServiceImpl.scala @@ -0,0 +1,110 @@ +package io.iohk.atala.agent.server.http.service + +import io.iohk.atala.agent.openapi.api._ +import io.iohk.atala.agent.openapi.model._ +import akka.http.scaladsl.server.Directives.* +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.server.Route +import zio._ +import io.iohk.atala.connect.core.service.ConnectionService +import io.iohk.atala.agent.server.http.model.OASDomainModelHelper +import io.iohk.atala.agent.server.http.model.OASErrorModelHelper +import io.iohk.atala.agent.server.http.model.HttpServiceError +import io.iohk.atala.connect.core.model.error.ConnectionError +import io.iohk.atala.mercury.PeerDID +import io.iohk.atala.mercury.protocol.invitation.v2.Invitation + +class ConnectionsManagementApiServiceImpl(connectionService: ConnectionService)(using runtime: zio.Runtime[Any]) + extends ConnectionsManagementApiService, + AkkaZioSupport, + OASDomainModelHelper, + OASErrorModelHelper { + + override def createConnection(request: CreateConnectionRequest)(implicit + toEntityMarshallerConnection: ToEntityMarshaller[Connection], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = { + val result = for { + record <- connectionService + .createConnectionInvitation(request.label) + .mapError(HttpServiceError.DomainError[ConnectionError].apply) + } yield record + + onZioSuccess(result.mapBoth(_.toOAS, _.toOAS).either) { + case Left(error) => complete(error.status -> error) + case Right(result) => createConnection201(result) + } + } + + override def getConnections()(implicit + toEntityMarshallerConnectionCollection: ToEntityMarshaller[ConnectionCollection], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = { + val result = for { + outcome <- connectionService + .getConnectionRecords() + .mapError(HttpServiceError.DomainError[ConnectionError].apply) + } yield outcome + + onZioSuccess(result.mapBoth(_.toOAS, _.map(_.toOAS)).either) { + case Left(error) => complete(error.status -> error) + case Right(result) => + getConnections200( + ConnectionCollection( + self = "/collections", + kind = "Collection", + contents = result + ) + ) + } + } + + override def getConnection(connectionId: String)(implicit + toEntityMarshallerConnection: ToEntityMarshaller[Connection], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = { + val result = for { + recordId <- connectionId.toUUID + outcome <- connectionService + .getConnectionRecord(recordId) + .mapError(HttpServiceError.DomainError[ConnectionError].apply) + } yield outcome + + onZioSuccess(result.mapBoth(_.toOAS, _.map(_.toOAS)).either) { + case Left(error) => complete(error.status -> error) + case Right(Some(result)) => getConnection200(result) + case Right(None) => getConnection404(notFoundErrorResponse(Some("Connection record not found"))) + } + } + + override def acceptConnectionInvitation(request: AcceptConnectionInvitationRequest)(implicit + toEntityMarshallerConnection: ToEntityMarshaller[Connection], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = { + val result = for { + record <- connectionService + .receiveConnectionInvitation(request.invitation) + .mapError(HttpServiceError.DomainError[ConnectionError].apply) + record <- connectionService + .acceptConnectionInvitation(record.id) + .mapError(HttpServiceError.DomainError[ConnectionError].apply) + } yield record + + onZioSuccess(result.mapBoth(_.toOAS, _.map(_.toOAS)).either) { + case Left(error) => complete(error.status -> error) + case Right(Some(result)) => acceptConnectionInvitation200(result) + case Right(None) => acceptConnectionInvitation500(notFoundErrorResponse(Some("Connection record not found"))) + } + } + + override def deleteConnection(connectionId: String): Route = ??? +} + +object ConnectionsManagementApiServiceImpl { + val layer: URLayer[ConnectionService, ConnectionsManagementApiService] = ZLayer.fromZIO { + for { + rt <- ZIO.runtime[Any] + svc <- ZIO.service[ConnectionService] + } yield ConnectionsManagementApiServiceImpl(svc)(using rt) + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/IssueCredentialsProtocolApiServiceImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/IssueCredentialsProtocolApiServiceImpl.scala index a623eb1a82..7d900587d2 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/IssueCredentialsProtocolApiServiceImpl.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/IssueCredentialsProtocolApiServiceImpl.scala @@ -69,13 +69,6 @@ class IssueCredentialsProtocolApiServiceImpl(credentialService: CredentialServic } } - extension (str: String) { - def toUUID: ZIO[Any, InvalidPayload, UUID] = - ZIO - .fromTry(Try(UUID.fromString(str))) - .mapError(e => HttpServiceError.InvalidPayload(s"Error parsing string as UUID: ${e.getMessage()}")) - } - def getCredentialRecord(recordId: String)(implicit toEntityMarshallerIssueCredentialRecord: ToEntityMarshaller[IssueCredentialRecord], toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/ConnectBackgroundJobs.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/ConnectBackgroundJobs.scala new file mode 100644 index 0000000000..78087c6fd3 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/ConnectBackgroundJobs.scala @@ -0,0 +1,62 @@ +package io.iohk.atala.agent.server.jobs + +import zio._ +import io.iohk.atala.connect.core.service.ConnectionService +import io.iohk.atala.connect.core.model.ConnectionRecord +import io.iohk.atala.mercury.DidComm +import io.iohk.atala.connect.core.model.ConnectionRecord._ +import io.iohk.atala.agent.server.jobs.MercuryUtils.sendMessage +import io.iohk.atala.mercury.DidComm +import io.iohk.atala.mercury.MediaTypes +import io.iohk.atala.mercury.model._ +import io.iohk.atala.mercury.model.error._ +import io.iohk.atala.mercury.protocol.issuecredential._ +import java.io.IOException + +object ConnectBackgroundJobs { + + val didCommExchanges = { + for { + connectionService <- ZIO.service[ConnectionService] + records <- connectionService + .getConnectionRecords() + .mapError(err => Throwable(s"Error occured while getting connection records: $err")) + _ <- ZIO.foreach(records)(performExchange) + } yield () + } + + private[this] def performExchange( + record: ConnectionRecord + ): ZIO[DidComm & ConnectionService, Throwable, Unit] = { + import Role._ + import ProtocolState._ + val exchange = record match { + case ConnectionRecord(id, _, _, _, _, Invitee, ConnectionRequestPending, _, Some(request), _) => + for { + didComm <- ZIO.service[DidComm] + _ <- sendMessage(request.makeMessage) + connectionService <- ZIO.service[ConnectionService] + _ <- connectionService.markConnectionRequestSent(id) + } yield () + + case ConnectionRecord(id, _, _, _, _, Inviter, ConnectionResponsePending, _, _, Some(response)) => + for { + didComm <- ZIO.service[DidComm] + _ <- sendMessage(response.makeMessage) + connectionService <- ZIO.service[ConnectionService] + _ <- connectionService.markConnectionResponseSent(id) + } yield () + + case _ => ZIO.unit + } + + exchange.catchAll { + case ex: TransportError => // : io.iohk.atala.mercury.model.error.MercuryError | java.io.IOException => + ex.printStackTrace() + ZIO.logError(ex.getMessage()) *> + ZIO.fail(mercuryErrorAsThrowable(ex)) + case ex: IOException => ZIO.fail(ex) + } + } + +}