From 996a406c6405d19a440a3aef79552e6bf9ba8b6c Mon Sep 17 00:00:00 2001 From: bvoiturier Date: Tue, 9 Jan 2024 14:37:03 +0100 Subject: [PATCH] docs(prism-agent): ATL-6269 improve connections OpenAPI doc (#833) Signed-off-by: Benjamin Voiturier Signed-off-by: Shota Jolbordi --- .../atala/agent/server/http/DocModels.scala | 126 ++++++++++++++++++ .../agent/server/http/ZHttpEndpoints.scala | 77 +---------- .../api/http/model/PaginationInput.scala | 24 +--- .../controller/ConnectionEndpoints.scala | 68 +++++++--- .../iohk/atala/api/util/Tapir2StaticOAS.scala | 9 +- 5 files changed, 190 insertions(+), 114 deletions(-) create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/DocModels.scala diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/DocModels.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/DocModels.scala new file mode 100644 index 0000000000..a45080eca0 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/DocModels.scala @@ -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. + |
+ |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 from 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. + |
+ |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 + ) + ) + ) + ) + + } + +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala index 0cec3ca389..721dcae0a0 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala @@ -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) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/PaginationInput.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/PaginationInput.scala index ed20e9b1e0..ed6d17dba4 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/PaginationInput.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/PaginationInput.scala @@ -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) @@ -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)) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala index 2c557c56e3..91b611f0a4 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala @@ -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[ @@ -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 from 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] = @@ -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), @@ -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), @@ -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) } diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala index d57e531d7d..a7c42f720b 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala @@ -1,9 +1,14 @@ package io.iohk.atala.api.util import io.iohk.atala.agent.server.AgentHttpServer +import io.iohk.atala.agent.server.http.DocModels import io.iohk.atala.castor.controller.{DIDController, DIDRegistrarController} +<<<<<<< HEAD import io.iohk.atala.connect.controller.ConnectionController import io.iohk.atala.credentialstatus.controller.CredentialStatusController +======= +import io.iohk.atala.connect.controller.{ConnectionController, ConnectionEndpoints} +>>>>>>> 614e811b (docs(prism-agent): ATL-6269 improve connections OpenAPI doc (#833)) import io.iohk.atala.event.controller.EventController import io.iohk.atala.iam.authentication.DefaultAuthenticator import io.iohk.atala.iam.entity.http.controller.EntityController @@ -14,6 +19,7 @@ import io.iohk.atala.pollux.credentialschema.controller.{CredentialSchemaControl import io.iohk.atala.presentproof.controller.PresentProofController import io.iohk.atala.system.controller.SystemController import org.scalatestplus.mockito.MockitoSugar.* +import sttp.apispec.Tag import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter import zio.{Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer} @@ -29,7 +35,8 @@ object Tapir2StaticOAS extends ZIOAppDefault { allEndpoints <- AgentHttpServer.agentRESTServiceEndpoints } yield { import sttp.apispec.openapi.circe.yaml.* - val yaml = OpenAPIDocsInterpreter().toOpenAPI(allEndpoints.map(_.endpoint), "Prism Agent", args(1)).toYaml + val model = DocModels.customiseDocsModel(OpenAPIDocsInterpreter().toOpenAPI(allEndpoints.map(_.endpoint), "", "")) + val yaml = model.info(model.info.copy(version = args(1))).toYaml val path = Path.of(args.head) Using(Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { writer => writer.write(yaml) } }