Skip to content

Commit

Permalink
docs(prism-agent): ATL-6269 improve connections OpenAPI doc (#833)
Browse files Browse the repository at this point in the history
Signed-off-by: Benjamin Voiturier <benjamin.voiturier@iohk.io>
Signed-off-by: Shota Jolbordi <shota.jolbordi@iohk.io>
  • Loading branch information
bvoiturier authored and shotexa committed Mar 18, 2024
1 parent 74f4f8b commit 996a406
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 114 deletions.
@@ -0,0 +1,126 @@
package io.iohk.atala.agent.server.http

import io.iohk.atala.connect.controller.ConnectionEndpoints
import sttp.apispec.openapi.*
import sttp.apispec.{SecurityScheme, Tag}
import sttp.model.headers.AuthenticationScheme

import scala.collection.immutable.ListMap

object DocModels {

private val apiKeySecuritySchema = SecurityScheme(
`type` = "apiKey",
description = Some("API Key Authentication. The header `apikey` must be set with the API key."),
name = Some("apikey"),
in = Some("header"),
scheme = None,
bearerFormat = None,
flows = None,
openIdConnectUrl = None
)

private val adminApiKeySecuritySchema = SecurityScheme(
`type` = "apiKey",
description =
Some("Admin API Key Authentication. The header `x-admin-api-key` must be set with the Admin API key."),
name = Some("x-admin-api-key"),
in = Some("header"),
scheme = None,
bearerFormat = None,
flows = None,
openIdConnectUrl = None
)

private val jwtSecurityScheme = SecurityScheme(
`type` = "http",
description =
Some("JWT Authentication. The header `Authorization` must be set with the JWT token using `Bearer` scheme"),
name = Some("Authorization"),
in = Some("header"),
scheme = Some(AuthenticationScheme.Bearer.name),
bearerFormat = None,
flows = None,
openIdConnectUrl = None
)

val customiseDocsModel: OpenAPI => OpenAPI = { oapi =>
oapi
.info(
Info(
title = "Open Enterprise Agent API Reference",
version = "1.0", // Will be replaced dynamically by 'Tapir2StaticOAS'
summary = Some("Info - Summary"),
description = Some("Info - Description"),
termsOfService = Some("Info - Terms Of Service"),
contact = Some(
Contact(
name = Some("Contact - Name"),
email = Some("Contact - Email"),
url = Some("Contact - URL"),
extensions = ListMap.empty
)
),
license = Some(
License(
name = "License - Name",
url = Some("License - URL"),
extensions = ListMap.empty
)
),
extensions = ListMap.empty
)
)
.servers(
List(
Server(url = "http://localhost:8085", description = Some("Local Prism Agent")),
Server(url = "http://localhost/prism-agent", description = Some("Local Prism Agent with APISIX proxy")),
Server(
url = "https://k8s-dev.atalaprism.io/prism-agent",
description = Some("Prism Agent on the Staging Environment")
),
)
)
.components(
oapi.components
.getOrElse(sttp.apispec.openapi.Components.Empty)
.copy(securitySchemes =
ListMap(
"apiKeyAuth" -> Right(apiKeySecuritySchema),
"adminApiKeyAuth" -> Right(adminApiKeySecuritySchema),
"jwtAuth" -> Right(jwtSecurityScheme)
)
)
)
.addSecurity(
ListMap(
"apiKeyAuth" -> Vector.empty[String],
"adminApiKeyAuth" -> Vector.empty[String],
"jwtAuth" -> Vector.empty[String]
)
)
.tags(
List(
Tag(
ConnectionEndpoints.TAG,
Some(
s"""
|The '${ConnectionEndpoints.TAG}' endpoints facilitate the initiation of connection flows between the current agent and peer agents, regardless of whether they reside in cloud or edge environments.
|<br>
|This implementation adheres to the DIDComm Messaging v2.0 - [Out of Band Messages](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) specification [section 9.5.4](https://identity.foundation/didcomm-messaging/spec/v2.0/#invitation) - to generate invitations.
|The <b>from</b> field of the out-of-band invitation message contains a freshly generated Peer DID that complies with the [did:peer:2](https://identity.foundation/peer-did-method-spec/#generating-a-didpeer2) specification.
|This Peer DID includes the 'uri' location of the DIDComm messaging service, essential for the invitee's subsequent execution of the connection flow.
|<br>
|Upon accepting an invitation, the invitee sends a connection request to the inviter's DIDComm messaging service endpoint.
|The connection request's 'type' attribute must be specified as "https://atalaprism.io/mercury/connections/1.0/request".
|The inviter agent responds with a connection response message, indicated by a 'type' attribute of "https://atalaprism.io/mercury/connections/1.0/response".
|Both request and response types are proprietary to the Open Enterprise Agent ecosystem.
|""".stripMargin
)
)
)
)

}

}
Expand Up @@ -13,84 +13,19 @@ import scala.collection.immutable.ListMap

object ZHttpEndpoints {

val swaggerUIOptions = SwaggerUIOptions.default
private val swaggerUIOptions = SwaggerUIOptions.default
.contextPath(List("docs", "prism-agent", "api"))

val customiseDocsModel: OpenAPI => OpenAPI = { oapi =>
oapi
.servers(
List(
Server(url = "http://localhost:8085", description = Some("Local Prism Agent")),
Server(url = "http://localhost/prism-agent", description = Some("Local Prism Agent with APISIX proxy")),
Server(
url = "https://k8s-dev.atalaprism.io/prism-agent",
description = Some("Prism Agent on the Staging Environment")
),
)
)
.components(
oapi.components
.getOrElse(sttp.apispec.openapi.Components.Empty)
.copy(securitySchemes =
ListMap(
"apiKeyAuth" -> Right(apiKeySecuritySchema),
"adminApiKeyAuth" -> Right(adminApiKeySecuritySchema),
"jwtAuth" -> Right(jwtSecurityScheme)
)
)
)
.addSecurity(
ListMap(
"apiKeyAuth" -> Vector.empty[String],
"adminApiKeyAuth" -> Vector.empty[String],
"jwtAuth" -> Vector.empty[String]
)
)

}

private val apiKeySecuritySchema = SecurityScheme(
`type` = "apiKey",
description = Some("API Key Authentication. The header `apikey` must be set with the API key."),
name = Some("apikey"),
in = Some("header"),
scheme = None,
bearerFormat = None,
flows = None,
openIdConnectUrl = None
)

private val adminApiKeySecuritySchema = SecurityScheme(
`type` = "apiKey",
description =
Some("Admin API Key Authentication. The header `x-admin-api-key` must be set with the Admin API key."),
name = Some("x-admin-api-key"),
in = Some("header"),
scheme = None,
bearerFormat = None,
flows = None,
openIdConnectUrl = None
)

private val jwtSecurityScheme = SecurityScheme(
`type` = "http",
description =
Some("JWT Authentication. The header `Authorization` must be set with the JWT token using `Bearer` scheme"),
name = Some("Authorization"),
in = Some("header"),
scheme = Some(AuthenticationScheme.Bearer.name),
bearerFormat = None,
flows = None,
openIdConnectUrl = None
)
private val redocUIOptions = RedocUIOptions.default
.copy(pathPrefix = List("redoc"))

def swaggerEndpoints[F[_]](apiEndpoints: List[ServerEndpoint[Any, F]]): List[ServerEndpoint[Any, F]] =
SwaggerInterpreter(swaggerUIOptions = swaggerUIOptions, customiseDocsModel = customiseDocsModel)
SwaggerInterpreter(swaggerUIOptions = swaggerUIOptions, customiseDocsModel = DocModels.customiseDocsModel)
.fromServerEndpoints[F](apiEndpoints, "Prism Agent", "1.0.0")

def redocEndpoints[F[_]](apiEndpoints: List[ServerEndpoint[Any, F]]): List[ServerEndpoint[Any, F]] =
RedocInterpreter(redocUIOptions = RedocUIOptions.default.copy(pathPrefix = List("redoc")))
.fromServerEndpoints[F](apiEndpoints, title = "Prism Agent", version = "1.0.0")
RedocInterpreter(redocUIOptions = redocUIOptions, customiseDocsModel = DocModels.customiseDocsModel)
.fromServerEndpoints[F](apiEndpoints, "Prism Agent", "1.0.0")

def withDocumentations[F[_]](apiEndpoints: List[ServerEndpoint[Any, F]]): List[ServerEndpoint[Any, F]] = {
apiEndpoints ++ swaggerEndpoints[F](apiEndpoints) ++ redocEndpoints[F](apiEndpoints)
Expand Down
@@ -1,17 +1,16 @@
package io.iohk.atala.api.http.model

import sttp.tapir.EndpointIO.annotations.query
import sttp.tapir.EndpointIO.annotations.{description, query}
import sttp.tapir.Schema.annotations.validateEach
import sttp.tapir.Validator
import sttp.tapir.Schema

import io.iohk.atala.api.http.Annotation
import sttp.tapir.{Schema, Validator}
case class PaginationInput(
@query
@validateEach(Validator.positiveOrZero[Int])
@description("The number of items to skip before returning results. Default is 0 if not specified.")
offset: Option[Int] = None,
@query
@validateEach(Validator.positive[Int])
@description("The maximum number of items to return. Defaults to 100 if not specified.")
limit: Option[Int] = None
) {
def toPagination = Pagination.apply(this)
Expand All @@ -23,21 +22,6 @@ case class Pagination(offset: Int, limit: Int) {
}

object Pagination {

object annotations {
object offset
extends Annotation[Int](
description = "The number of items to skip before returning results. Default is 0 if not specified",
example = 0
)

object limit
extends Annotation[Int](
description = "The maximum number of items to return. Defaults to 100 if not specified.",
example = 100
)
}

def apply(in: PaginationInput): Pagination =
Pagination(in.offset.getOrElse(0), in.limit.getOrElse(100))
}
Expand Up @@ -21,6 +21,8 @@ import java.util.UUID

object ConnectionEndpoints {

val TAG: String = "Connections Management"

private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput]

val createConnection: Endpoint[
Expand All @@ -47,16 +49,19 @@ object ConnectionEndpoints {
)
)
.out(jsonBody[Connection])
.description("The created connection record.")
.description("The newly created connection record.")
.errorOut(basicFailuresAndForbidden)
.name("createConnection")
.summary("Creates a new connection record and returns an Out of Band invitation.")
.summary("Create a new connection invitation that can be delivered out-of-band to a peer agent.")
.description("""
|Generates a new Peer DID and creates an [Out of Band 2.0](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) invitation.
|It returns a new connection record in `InvitationGenerated` state.
|The request body may contain a `label` that can be used as a human readable alias for the connection, for example `{'label': "Bob"}`
|Create a new connection invitation that can be delivered out-of-band to a peer agent, regardless of whether it resides in cloud or edge environment.
|The generated invitation adheres to the DIDComm Messaging v2.0 - [Out of Band Messages](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) specification [section 9.5.4](https://identity.foundation/didcomm-messaging/spec/v2.0/#invitation).
|The <b>from</b> field of the out-of-band invitation message contains a freshly generated Peer DID that complies with the [did:peer:2](https://identity.foundation/peer-did-method-spec/#generating-a-didpeer2) specification.
|This Peer DID includes the 'uri' location of the DIDComm messaging service, essential for the invitee's subsequent execution of the connection flow.
|In the agent database, the created connection record has an initial state set to `InvitationGenerated`.
|The request body may contain a `label` that can be used as a human readable alias for the connection, for example `{'label': "Connection with Bob"}`
|""".stripMargin)
.tag("Connections Management")
.tag(TAG)

val getConnection
: Endpoint[(ApiKeyCredentials, JwtCredentials), (RequestContext, UUID), ErrorResponse, Connection, Any] =
Expand All @@ -66,15 +71,21 @@ object ConnectionEndpoints {
.in(extractFromRequest[RequestContext](RequestContext.apply))
.in(
"connections" / path[UUID]("connectionId").description(
"The unique identifier of the connection record."
"The `connectionId` uniquely identifying the connection flow record."
)
)
.out(jsonBody[Connection].description("The connection record."))
.out(jsonBody[Connection].description("The specific connection flow record."))
.errorOut(basicFailureAndNotFoundAndForbidden)
.name("getConnection")
.summary("Gets an existing connection record by its unique identifier.")
.description("Gets an existing connection record by its unique identifier")
.tag("Connections Management")
.summary(
"Retrieves a specific connection flow record from the agent's database based on its unique `connectionId`."
)
.description("""
|Retrieve a specific connection flow record from the agent's database based in its unique `connectionId`.
|The API returns a comprehensive collection of connection flow records within the system, regardless of their state.
|The returned connection item includes essential metadata such as connection ID, thread ID, state, role, participant information, and other relevant details.
|""".stripMargin)
.tag(TAG)

val getConnections: Endpoint[
(ApiKeyCredentials, JwtCredentials),
Expand All @@ -89,13 +100,24 @@ object ConnectionEndpoints {
.in(extractFromRequest[RequestContext](RequestContext.apply))
.in("connections")
.in(paginationInput)
.in(query[Option[String]]("thid").description("The thid of a DIDComm communication."))
.out(jsonBody[ConnectionsPage].description("The list of connection records."))
.in(
query[Option[String]]("thid").description(
"The `thid`, shared between the inviter and the invitee, that uniquely identifies a connection flow."
)
)
.out(
jsonBody[ConnectionsPage].description("The list of connection flow records available from the agent's database")
)
.errorOut(basicFailuresAndForbidden)
.name("getConnections")
.summary("Gets the list of connection records.")
.description("Get the list of connection records paginated")
.tag("Connections Management")
.summary("Retrieves the list of connection flow records available from the agent's database.")
.description("""
|Retrieve of a list containing connections available from the agent's database.
|The API returns a comprehensive collection of connection flow records within the system, regardless of their state.
|Each connection item includes essential metadata such as connection ID, thread ID, state, role, participant information, and other relevant details.
|Pagination support is available, allowing for efficient handling of large datasets.
|""".stripMargin)
.tag(TAG)

val acceptConnectionInvitation: Endpoint[
(ApiKeyCredentials, JwtCredentials),
Expand All @@ -121,15 +143,17 @@ object ConnectionEndpoints {
)
)
.out(jsonBody[Connection])
.description("The created connection record.")
.description("The newly connection record.")
.errorOut(basicFailuresAndForbidden)
.name("acceptConnectionInvitation")
.summary("Accepts an Out of Band invitation.")
.summary("Accept a new connection invitation received out-of-band from another peer agent.")
.description("""
|Accepts an [Out of Band 2.0](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) invitation, generates a new Peer DID,
|and submits a Connection Request to the inviter.
|It returns a connection object in `ConnectionRequestPending` state, until the Connection Request is eventually sent to the inviter by the prism-agent's background process. The connection object state will then automatically move to `ConnectionRequestSent`.
|Accept an new connection invitation received out-of-band from another peer agent.
|The invitation must be compliant with the DIDComm Messaging v2.0 - [Out of Band Messages](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) specification [section 9.5.4](https://identity.foundation/didcomm-messaging/spec/v2.0/#invitation).
|A new connection record with state `ConnectionRequestPending` will be created in the agent database and later processed by a background job to send a connection request to the peer agent.
|The created record will contain a newly generated pairwise Peer DID used for that connection.
|A connection request will then be sent to the peer agent to actually establish the connection, moving the record state to `ConnectionRequestSent`, and waiting the connection response from the peer agent.
|""".stripMargin)
.tag("Connections Management")
.tag(TAG)

}

0 comments on commit 996a406

Please sign in to comment.