Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(prism-agent): ATL-6269 improve connections OpenAPI doc #833

Merged
merged 7 commits into from Jan 9, 2024
@@ -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)

}