From d75b36bd472c1cc2f15d775596a2675cda469754 Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev - IOHK <102033808+yshyn-iohk@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:51:20 +0700 Subject: [PATCH] feat(pollux): schema registry lookup and verification policies REST API ATL-1334 (#168) * feat(pollux): implement schema-registry in-memory using Tapir * feat(pollux) Add pagination request for VC Schema + Specs * feat(pollux) Add VerificationPolicy entity and REST API --- infrastructure/local/docker-compose.yml | 5 +- prism-agent/service/build.sbt | 12 +- .../service/project/Dependencies.scala | 2 + .../io/iohk/atala/agent/server/Main.scala | 11 +- .../io/iohk/atala/agent/server/Modules.scala | 25 ++- .../iohk/atala/api/http/FailureResponse.scala | 5 + .../atala/api/http/codec/OrderCodec.scala | 37 ++++ .../io/iohk/atala/api/http/model/Order.scala | 20 ++ .../atala/api/http/model/Pagination.scala | 15 ++ .../io/iohk/atala/pollux/schema/Models.scala | 66 ------- .../schema/SchemaRegistryEndpoints.scala | 82 ++++++-- .../SchemaRegistryServerEndpoints.scala | 54 +++-- .../schema/VerificationPolicyEndpoints.scala | 186 ++++++++++++++++++ .../VerificationPolicyServerEndpoints.scala | 114 +++++++++++ .../atala/pollux/schema/model/Proof.scala | 22 +++ .../model/VerifiableCredentialSchema.scala | 89 +++++++++ .../schema/model/VerificationPolicy.scala | 102 ++++++++++ .../service/SchemaRegistryService.scala | 32 ++- .../SchemaRegistryServiceInMemory.scala | 48 ++++- .../service/VerificationPolicyService.scala | 28 +++ .../VerificationPolicyServiceInMemory.scala | 90 +++++++++ .../schema/SchemaRegistryEndpointsSpec.scala | 108 ++++++++++ 22 files changed, 1032 insertions(+), 121 deletions(-) create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/codec/OrderCodec.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/Order.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/Pagination.scala delete mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/Models.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/VerificationPolicyEndpoints.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/VerificationPolicyServerEndpoints.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/Proof.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/VerifiableCredentialSchema.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/VerificationPolicy.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/VerificationPolicyService.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/VerificationPolicyServiceInMemory.scala create mode 100644 prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpointsSpec.scala diff --git a/infrastructure/local/docker-compose.yml b/infrastructure/local/docker-compose.yml index 53c43218e3..c48c0f7194 100644 --- a/infrastructure/local/docker-compose.yml +++ b/infrastructure/local/docker-compose.yml @@ -105,7 +105,10 @@ services: POLLUX_DB_NAME: pollux POLLUX_DB_USER: postgres POLLUX_DB_PASSWORD: postgres - + ports: + - "8085:8085" + - "8080:8080" + swagger-ui: image: swaggerapi/swagger-ui:v4.14.0 environment: diff --git a/prism-agent/service/build.sbt b/prism-agent/service/build.sbt index 94045c3f38..9d5847cca7 100644 --- a/prism-agent/service/build.sbt +++ b/prism-agent/service/build.sbt @@ -3,7 +3,8 @@ import sbtghpackages.GitHubPackagesPlugin.autoImport._ import sbtrelease.ReleasePlugin.autoImport.ReleaseTransformations._ // Custom keys -val apiBaseDirectory = settingKey[File]("The base directory for PrismAgent API specifications") +val apiBaseDirectory = + settingKey[File]("The base directory for PrismAgent API specifications") inThisBuild( Seq( @@ -53,8 +54,13 @@ lazy val server = project Compile / sourceGenerators += openApiGenerateClasses, openApiGeneratorSpec := apiBaseDirectory.value / "http/prism-agent-openapi-spec.yaml", openApiGeneratorConfig := baseDirectory.value / "openapi/generator-config/config.yaml", - openApiGeneratorImportMapping := Seq("DidOperationType", "DidOperationStatus") - .map(model => (model, s"io.iohk.atala.agent.server.http.model.OASModelPatches.$model")) + openApiGeneratorImportMapping := Seq( + "DidOperationType", + "DidOperationStatus" + ) + .map(model => + (model, s"io.iohk.atala.agent.server.http.model.OASModelPatches.$model") + ) .toMap, Docker / maintainer := "atala-coredid@iohk.io", Docker / dockerUsername := Some("input-output-hk"), diff --git a/prism-agent/service/project/Dependencies.scala b/prism-agent/service/project/Dependencies.scala index dced7ac5f2..76fb26c3d7 100644 --- a/prism-agent/service/project/Dependencies.scala +++ b/prism-agent/service/project/Dependencies.scala @@ -63,6 +63,7 @@ object Dependencies { private lazy val tapirRedocBundle = "com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % Versions.tapir private lazy val tapirSttpStubServer = "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % Versions.tapir % Test + private lazy val sttpClient3ZioJson = "com.softwaremill.sttp.client3" %% "zio-json" % "3.8.0" % Test // Dependency Modules @@ -80,6 +81,7 @@ object Dependencies { tapirJsonZio, tapirRedocBundle, tapirSttpStubServer, + sttpClient3ZioJson, tapirZioHttpServer, tapirHttp4sServerZio, http4sBlazeServer) 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 5b7d5581d9..aaf2023af8 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 @@ -9,12 +9,13 @@ 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( - io.iohk.atala.mercury.AgentServiceAny( - new DIDComm(UniversalDidResolver, peer.getSecretResolverInMemory), - peer.did + def agentLayer(peer: PeerDID): ZLayer[Any, Nothing, AgentServiceAny] = + ZLayer.succeed( + io.iohk.atala.mercury.AgentServiceAny( + new DIDComm(UniversalDidResolver, peer.getSecretResolverInMemory), + peer.did + ) ) - ) override def run: ZIO[Any, Throwable, Unit] = for { 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 d7345da8cf..f13d3f3d78 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 @@ -51,7 +51,7 @@ import io.iohk.atala.castor.core.service.{DIDService, DIDServiceImpl} import io.iohk.atala.pollux.core.service.CredentialServiceImpl import io.iohk.atala.castor.core.util.DIDOperationValidator import io.iohk.atala.castor.sql.repository.{JdbcDIDOperationRepository, TransactorLayer} -import io.iohk.atala.castor.sql.repository.{DbConfig => CastorDbConfig} +import io.iohk.atala.castor.sql.repository.DbConfig as CastorDbConfig import io.iohk.atala.iris.proto.service.IrisServiceGrpc import io.iohk.atala.iris.proto.service.IrisServiceGrpc.IrisServiceStub import io.iohk.atala.pollux.core.repository.CredentialRepository @@ -65,17 +65,17 @@ import zio.config.typesafe.TypesafeConfigSource import zio.config.{ReadError, read} import zio.interop.catz.* import zio.stream.ZStream -import zhttp.http._ +import zhttp.http.* import zhttp.service.Server import java.util.concurrent.Executors - -import io.iohk.atala.mercury._ -import io.iohk.atala.mercury.model._ -import io.iohk.atala.mercury.model.error._ -import io.iohk.atala.mercury.protocol.issuecredential._ +import io.iohk.atala.mercury.* +import io.iohk.atala.mercury.model.* +import io.iohk.atala.mercury.model.error.* +import io.iohk.atala.mercury.protocol.issuecredential.* import io.iohk.atala.pollux.core.model.error.IssueCredentialError import io.iohk.atala.pollux.core.model.error.IssueCredentialError.RepositoryError + import java.io.IOException import cats.implicits.* import io.iohk.atala.pollux.schema.SchemaRegistryServerEndpoints @@ -87,6 +87,8 @@ 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 +import io.iohk.atala.pollux.schema.{SchemaRegistryServerEndpoints, VerificationPolicyServerEndpoints} +import io.iohk.atala.pollux.service.{SchemaRegistryServiceInMemory, VerificationPolicyServiceInMemory} object Modules { @@ -101,13 +103,18 @@ object Modules { lazy val zioApp = { val zioHttpServerApp = for { allSchemaRegistryEndpoints <- SchemaRegistryServerEndpoints.all - allEndpoints = ZHttpEndpoints.withDocumentations[Task](allSchemaRegistryEndpoints) + allVerificationPolicyEndpoints <- VerificationPolicyServerEndpoints.all + allEndpoints = ZHttpEndpoints.withDocumentations[Task]( + allSchemaRegistryEndpoints ++ allVerificationPolicyEndpoints + ) appConfig <- ZIO.service[AppConfig] httpServer <- ZHttp4sBlazeServer.start(allEndpoints, port = appConfig.agent.httpEndpoint.http.port) } yield httpServer zioHttpServerApp - .provideLayer(SchemaRegistryServiceInMemory.layer ++ SystemModule.configLayer) + .provideLayer( + SchemaRegistryServiceInMemory.layer ++ VerificationPolicyServiceInMemory.layer ++ SystemModule.configLayer + ) .unit } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/FailureResponse.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/FailureResponse.scala index 2b9279c6b6..00c776617c 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/FailureResponse.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/FailureResponse.scala @@ -2,6 +2,7 @@ package io.iohk.atala.api.http import sttp.model.StatusCode import sttp.tapir.EndpointOutput.OneOf +import sttp.tapir.Schema import sttp.tapir.generic.auto.* import sttp.tapir.json.zio.jsonBody import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} @@ -13,6 +14,7 @@ case class NotFoundResponse(msg: String) extends FailureResponse object NotFoundResponse { given encoder: zio.json.JsonEncoder[NotFoundResponse] = DeriveJsonEncoder.gen[NotFoundResponse] given decoder: zio.json.JsonDecoder[NotFoundResponse] = DeriveJsonDecoder.gen[NotFoundResponse] + given schema: Schema[NotFoundResponse] = Schema.derived } case class BadRequest(msg: String, errors: List[String] = List.empty) extends FailureResponse @@ -20,6 +22,7 @@ case class BadRequest(msg: String, errors: List[String] = List.empty) extends Fa object BadRequest { given encoder: zio.json.JsonEncoder[BadRequest] = DeriveJsonEncoder.gen[BadRequest] given decoder: zio.json.JsonDecoder[BadRequest] = DeriveJsonDecoder.gen[BadRequest] + given schema: Schema[BadRequest] = Schema.derived } case class InternalServerError(msg: String) extends FailureResponse @@ -27,6 +30,7 @@ case class InternalServerError(msg: String) extends FailureResponse object InternalServerError { given encoder: zio.json.JsonEncoder[InternalServerError] = DeriveJsonEncoder.gen[InternalServerError] given decoder: zio.json.JsonDecoder[InternalServerError] = DeriveJsonDecoder.gen[InternalServerError] + given schema: Schema[InternalServerError] = Schema.derived } //An RFC-7807 compliant data structure for reporting errors to the client @@ -36,4 +40,5 @@ case class ErrorResponse(`type`: String, title: String, status: Int, instance: S object ErrorResponse { given encoder: zio.json.JsonEncoder[ErrorResponse] = DeriveJsonEncoder.gen[ErrorResponse] given decoder: zio.json.JsonDecoder[ErrorResponse] = DeriveJsonDecoder.gen[ErrorResponse] + given schema: Schema[ErrorResponse] = Schema.derived } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/codec/OrderCodec.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/codec/OrderCodec.scala new file mode 100644 index 0000000000..882ded5de1 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/codec/OrderCodec.scala @@ -0,0 +1,37 @@ +package io.iohk.atala.api.http.codec + +import io.iohk.atala.api.http.model.Order +import io.iohk.atala.api.http.model.Order.Direction +import sttp.tapir.Codec.PlainCodec +import sttp.tapir.{Codec, DecodeResult} + +import java.util.Base64 + +object OrderCodec { + implicit def orderCodec: PlainCodec[Order] = { + def decode(s: String): DecodeResult[Order] = + try { + val s2 = new String(Base64.getDecoder.decode(s)) + val order = s2.split(".", 2) match { + case Array() => Order.empty + case Array(field) => Order(field) + case Array(field, "") => Order(field) + case Array(field, direction) => + Order(field, Some(Direction.valueOf(direction))) + case _ => sys.error("impossible") + } + DecodeResult.Value(order) + } catch { + case e: Exception => DecodeResult.Error(s, e) + } + + def encode(order: Order): String = + Base64.getEncoder + .encodeToString( + s"${order.field}.${order.direction.getOrElse(Order.DefaultDirection).toString}" + .getBytes("UTF-8") + ) + + Codec.string.mapDecode(decode)(encode) + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/Order.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/Order.scala new file mode 100644 index 0000000000..4af6016507 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/Order.scala @@ -0,0 +1,20 @@ +package io.iohk.atala.api.http.model + +import sttp.tapir.Codec.PlainCodec +import sttp.tapir.generic.auto.* +import sttp.tapir.{Codec, DecodeResult, Schema} + +import java.util.Base64 + +case class Order(field: String, direction: Option[Order.Direction] = None) + +object Order { + val DefaultDirection = Direction.Ascending + val empty = Order("") + + enum Direction(kind: String): + case Ascending extends Direction("asc") + case Descending extends Direction("desc") + + import io.iohk.atala.api.http.codec.OrderCodec.orderCodec +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/Pagination.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/Pagination.scala new file mode 100644 index 0000000000..82a37243a0 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/Pagination.scala @@ -0,0 +1,15 @@ +package io.iohk.atala.api.http.model + +import sttp.tapir.Codec.PlainCodec +import sttp.tapir.generic.auto.* +import sttp.tapir.{Codec, DecodeResult, Schema} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} + +import java.time.ZonedDateTime +import java.util.{Base64, UUID} +import scala.util.Try + +case class Pagination( + offset: Option[Int] = Some(0), + limit: Option[Int] = Some(10) +) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/Models.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/Models.scala deleted file mode 100644 index 268ed98063..0000000000 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/Models.scala +++ /dev/null @@ -1,66 +0,0 @@ -package io.iohk.atala.pollux.schema - -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} - -import java.time.ZonedDateTime -import java.util.UUID - -case class VerifiableCredentialsSchemaInput( - id: Option[UUID], - name: String, - version: String, - description: Option[String], - attributes: List[String], - authored: Option[ZonedDateTime], - tags: List[String] -) -object VerifiableCredentialsSchemaInput { - given encoder: zio.json.JsonEncoder[VerifiableCredentialsSchemaInput] = - DeriveJsonEncoder.gen[VerifiableCredentialsSchemaInput] - given decoder: zio.json.JsonDecoder[VerifiableCredentialsSchemaInput] = - DeriveJsonDecoder.gen[VerifiableCredentialsSchemaInput] -} - -case class VerifiableCredentialsSchema( - id: UUID, - name: String, - version: String, - tags: List[String], - description: Option[String], - attributes: List[String], - author: String, - authored: ZonedDateTime, - proof: Option[Proof] -) - -object VerifiableCredentialsSchema { - def apply(in: VerifiableCredentialsSchemaInput): VerifiableCredentialsSchema = - VerifiableCredentialsSchema( - id = in.id.getOrElse(UUID.randomUUID()), - name = in.name, - version = in.version, - tags = in.tags, - description = in.description, - attributes = in.attributes, - author = "Prism Agent", - authored = in.authored.getOrElse(ZonedDateTime.now()), - proof = None - ) - - given encoder: zio.json.JsonEncoder[VerifiableCredentialsSchema] = DeriveJsonEncoder.gen[VerifiableCredentialsSchema] - given decoder: zio.json.JsonDecoder[VerifiableCredentialsSchema] = DeriveJsonDecoder.gen[VerifiableCredentialsSchema] -} - -case class Proof( - `type`: String, - created: ZonedDateTime, - verificationMethod: String, - proofPurpose: String, - proofValue: String, - domain: Option[String] -) - -object Proof { - given encoder: zio.json.JsonEncoder[Proof] = DeriveJsonEncoder.gen[Proof] - given decoder: zio.json.JsonDecoder[Proof] = DeriveJsonDecoder.gen[Proof] -} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpoints.scala index 4b65b4d61c..0e1ae8c2c3 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpoints.scala @@ -1,6 +1,10 @@ package io.iohk.atala.pollux.schema +import io.iohk.atala.api.http.model.{Order, Pagination} import io.iohk.atala.api.http.{BadRequest, FailureResponse, InternalServerError, NotFoundResponse} +import io.iohk.atala.pollux.schema.model.VerifiableCredentialSchema +import io.iohk.atala.pollux.schema.model.VerifiableCredentialSchema.{Input, Page} +import io.iohk.atala.api.http.codec.OrderCodec._ import sttp.tapir.EndpointIO.Info import sttp.tapir.json.zio.jsonBody import sttp.tapir.{ @@ -12,30 +16,41 @@ import sttp.tapir.{ oneOfDefaultVariant, oneOfVariant, path, + query, + statusCode, stringToPath } -import sttp.tapir.generic.auto.* import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} import sttp.model.StatusCode -import sttp.tapir.statusCode import java.util.UUID object SchemaRegistryEndpoints { - val createSchemaEndpoint - : PublicEndpoint[VerifiableCredentialsSchemaInput, FailureResponse, VerifiableCredentialsSchema, Any] = + val createSchemaEndpoint: PublicEndpoint[ + VerifiableCredentialSchema.Input, + FailureResponse, + VerifiableCredentialSchema, + Any + ] = endpoint.post .in("schema-registry" / "schemas") .in( - jsonBody[VerifiableCredentialsSchemaInput] - .copy(info = Info.empty.description("Create schema input object with the metadata and attributes")) + jsonBody[VerifiableCredentialSchema.Input] + .copy(info = + Info.empty.description( + "Create schema input object with the metadata and attributes" + ) + ) ) .out(statusCode(StatusCode.Created)) - .out(jsonBody[VerifiableCredentialsSchema]) + .out(jsonBody[VerifiableCredentialSchema]) .errorOut( oneOf[FailureResponse]( - oneOfVariant(StatusCode.InternalServerError, jsonBody[InternalServerError]) + oneOfVariant( + StatusCode.InternalServerError, + jsonBody[InternalServerError] + ) ) ) .name("createSchema") @@ -45,13 +60,18 @@ object SchemaRegistryEndpoints { ) .tag("Schema Registry") - val getSchemaByIdEndpoint: PublicEndpoint[UUID, FailureResponse, VerifiableCredentialsSchema, Any] = + val getSchemaByIdEndpoint: PublicEndpoint[ + UUID, + FailureResponse, + VerifiableCredentialSchema, + Any + ] = endpoint.get .in( "schema-registry" / "schemas" / path[UUID]("id") - .copy(info = Info.empty.description("Schema Id")) + .copy(info = Info.empty.description("Get the schema by id")) ) - .out(jsonBody[VerifiableCredentialsSchema]) + .out(jsonBody[VerifiableCredentialSchema]) .errorOut( oneOf[FailureResponse]( oneOfVariant(StatusCode.NotFound, jsonBody[NotFoundResponse]) @@ -63,4 +83,44 @@ object SchemaRegistryEndpoints { "Fetch the schema by the unique identifier. Verifiable Credential Schema in json format is returned." ) .tag("Schema Registry") + + val lookupSchemasByQueryEndpoint: PublicEndpoint[ + (VerifiableCredentialSchema.Filter, Pagination, Option[Order]), + FailureResponse, + VerifiableCredentialSchema.Page, + Any + ] = + endpoint.get + .in("schema-registry" / "schemas".description("Lookup schemas by query")) + .in( + query[Option[String]]("author") + .and( + query[Option[String]]("name") + .and( + query[Option[String]]("tags") + ) + ) + .mapTo[VerifiableCredentialSchema.Filter] + ) + .in( + query[Option[Int]]("offset") + .and(query[Option[Int]]("limit")) + .mapTo[Pagination] + ) + .in(query[Option[Order]]("order")) + .out(jsonBody[VerifiableCredentialSchema.Page]) + .errorOut( + oneOf[FailureResponse]( + oneOfVariant( + StatusCode.InternalServerError, + jsonBody[InternalServerError] + ) + ) + ) + .name("lookupSchemasByQuery") + .summary("Lookup schemas by indexed fields") + .description( + "Lookup schemas by `author`, `name`, `tags` parameters and control the pagination by `offset` and `limit` parameters " + ) + .tag("Schema Registry") } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryServerEndpoints.scala index 5991ea7397..15b5fe4bb2 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryServerEndpoints.scala @@ -1,30 +1,36 @@ package io.iohk.atala.pollux.schema -import io.iohk.atala.pollux.service.SchemaRegistryService.{createSchema, getSchemaById} +import io.iohk.atala.api.http.model.{Order, Pagination} +import io.iohk.atala.api.http.{FailureResponse, InternalServerError, NotFoundResponse} +import io.iohk.atala.pollux.schema.SchemaRegistryEndpoints.{ + createSchemaEndpoint, + getSchemaByIdEndpoint, + lookupSchemasByQueryEndpoint +} +import io.iohk.atala.pollux.schema.model.VerifiableCredentialSchema import io.iohk.atala.pollux.service.SchemaRegistryService +import io.iohk.atala.pollux.service.SchemaRegistryService.{createSchema, getSchemaById, lookupSchemas} import sttp.tapir.redoc.RedocUIOptions import sttp.tapir.redoc.bundle.RedocInterpreter -import sttp.tapir.swagger.bundle.SwaggerInterpreter -import zio.{Task, URIO, ZIO, ZLayer} -import io.iohk.atala.api.http.{FailureResponse, InternalServerError, NotFoundResponse} -import SchemaRegistryEndpoints.{createSchemaEndpoint, getSchemaByIdEndpoint} import sttp.tapir.server.ServerEndpoint -import sttp.tapir.ztapir.ZServerEndpoint +import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.tapir.ztapir.* +import zio.{Task, URIO, ZIO, ZLayer} import java.util.UUID class SchemaRegistryServerEndpoints( schemaRegistryService: SchemaRegistryService ) { + def throwableToInternalServerError(throwable: Throwable) = + ZIO.fail[FailureResponse](InternalServerError(throwable.getMessage)) + + // TODO: make the endpoint typed ZServerEndpoint[SchemaRegistryService, Any] val createSchemaServerEndpoint: ZServerEndpoint[Any, Any] = createSchemaEndpoint.zServerLogic(schemaInput => schemaRegistryService .createSchema(schemaInput) - .foldZIO( - throwable => ZIO.fail[FailureResponse](InternalServerError(throwable.getMessage)), - schema => ZIO.succeed(schema) - ) + .foldZIO(throwableToInternalServerError, schema => ZIO.succeed(schema)) ) val getSchemaByIdServerEndpoint: ZServerEndpoint[Any, Any] = @@ -32,16 +38,38 @@ class SchemaRegistryServerEndpoints( schemaRegistryService .getSchemaById(id) .foldZIO( - throwable => ZIO.fail[FailureResponse](InternalServerError(throwable.getMessage)), + throwableToInternalServerError, { case Some(schema) => ZIO.succeed(schema) - case None => ZIO.fail[FailureResponse](NotFoundResponse(s"Schema is not found by $id")) + case None => + ZIO.fail[FailureResponse]( + NotFoundResponse(s"Schema is not found by $id") + ) } ) ) + val lookupSchemasByQueryServerEndpoint: ZServerEndpoint[Any, Any] = + lookupSchemasByQueryEndpoint.zServerLogic { + case ( + filter: VerifiableCredentialSchema.Filter, + page: Pagination, + order: Option[Order] + ) => + schemaRegistryService + .lookupSchemas(filter, page, order) + .foldZIO( + throwableToInternalServerError, + pageOfVCS => ZIO.succeed(pageOfVCS) + ) + } + val all: List[ZServerEndpoint[Any, Any]] = - List(createSchemaServerEndpoint, getSchemaByIdServerEndpoint) + List( + createSchemaServerEndpoint, + getSchemaByIdServerEndpoint, + lookupSchemasByQueryServerEndpoint + ) } object SchemaRegistryServerEndpoints { diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/VerificationPolicyEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/VerificationPolicyEndpoints.scala new file mode 100644 index 0000000000..e822018899 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/VerificationPolicyEndpoints.scala @@ -0,0 +1,186 @@ +package io.iohk.atala.pollux.schema + +import io.iohk.atala.api.http.codec.OrderCodec.* +import io.iohk.atala.api.http.model.{Order, Pagination} +import io.iohk.atala.api.http.{BadRequest, FailureResponse, InternalServerError, NotFoundResponse} +import io.iohk.atala.pollux.schema.model.VerifiableCredentialSchema.{Input, Page} +import io.iohk.atala.pollux.schema.model.{ + VerifiableCredentialSchema, + VerificationPolicy, + VerificationPolicyInput, + VerificationPolicyPage +} +import sttp.model.StatusCode +import sttp.tapir.EndpointIO.Info +import sttp.tapir.json.zio.jsonBody +import sttp.tapir.{ + Endpoint, + EndpointInfo, + PublicEndpoint, + endpoint, + oneOf, + oneOfDefaultVariant, + oneOfVariant, + path, + query, + statusCode, + stringToPath +} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} + +import java.util.UUID + +object VerificationPolicyEndpoints { + + val createVerificationPolicyEndpoint: PublicEndpoint[ + VerificationPolicyInput, + FailureResponse, + VerificationPolicy, + Any + ] = + endpoint.post + .in("verification" / "policies") + .in( + jsonBody[VerificationPolicyInput].description( + "Create verification policy object" + ) + ) + .out(statusCode(StatusCode.Created)) + .out(jsonBody[VerificationPolicy]) + .errorOut( + oneOf[FailureResponse]( + oneOfVariant( + StatusCode.InternalServerError, + jsonBody[InternalServerError] + ) + ) + ) + .name("createVerificationPolicy") + .summary("Create the new verification policy") + .description("Create the new verification policy") + .tag("Verification") + + val updateVerificationPolicyEndpoint: PublicEndpoint[ + (String, VerificationPolicyInput), + FailureResponse, + VerificationPolicy, + Any + ] = + endpoint.put + .in("verification" / "policies" / path[String]("id")) + .in( + jsonBody[VerificationPolicyInput].description( + "Update verification policy object" + ) + ) + .out(statusCode(StatusCode.Ok)) + .out(jsonBody[VerificationPolicy]) + .errorOut( + oneOf[FailureResponse]( + oneOfVariant( + StatusCode.InternalServerError, + jsonBody[InternalServerError] + ) + ) + ) + .name("updateVerificationPolicy") + .summary("Update the verification policy object by id") + .description( + "Update the fields of the verification policy entry: `attributes`, `issuerDIDs`, `name`, `credentialTypes`, " + ) + .tag("Verification") + + val getVerificationPolicyByIdEndpoint: PublicEndpoint[ + String, + FailureResponse, + VerificationPolicy, + Any + ] = + endpoint.get + .in( + "verification" / "policies" / path[String]("id") + .description("Get the verification policy by id") + ) + .out(jsonBody[VerificationPolicy]) + .errorOut( + oneOf[FailureResponse]( + oneOfVariant(StatusCode.NotFound, jsonBody[NotFoundResponse]) + ) + ) + .name("getVerificationPolicyById") + .summary("Fetch the verification policy by id") + .description( + "Get the verification policy by id" + ) + .tag("Verification") + + val deleteVerificationPolicyByIdEndpoint: PublicEndpoint[ + String, + FailureResponse, + Unit, + Any + ] = + endpoint.delete + .in( + "verification" / "policies" / path[String]("id") + .description("Delete the verification policy by id") + ) + .out(statusCode(StatusCode.Ok)) + .errorOut( + oneOf[FailureResponse]( + oneOfVariant(StatusCode.NotFound, jsonBody[NotFoundResponse]) + ) + ) + .name("deleteVerificationPolicyById") + .summary("Deleted the verification policy by id") + .description( + "Delete the verification policy by id" + ) + .tag("Verification") + + val lookupVerificationPoliciesByQueryEndpoint: PublicEndpoint[ + (VerificationPolicy.Filter, Pagination, Option[Order]), + FailureResponse, + VerificationPolicyPage, + Any + ] = + endpoint.get + .in( + "verification" / "policies" + .description("Lookup verification policy by query") + ) + .in( + query[Option[String]]("name") + .and( + query[Option[String]]("attributes") + .and( + query[Option[String]]("issuerDIDs") + .and( + query[Option[String]]("credentialTypes") + ) + ) + ) + .mapTo[VerificationPolicy.Filter] + ) + .in( + query[Option[Int]]("offset") + .and(query[Option[Int]]("limit")) + .mapTo[Pagination] + ) + .in(query[Option[Order]]("order")) + .out(jsonBody[VerificationPolicyPage]) + .errorOut( + oneOf[FailureResponse]( + oneOfVariant( + StatusCode.InternalServerError, + jsonBody[InternalServerError] + ) + ) + ) + .name("lookupVerificationPoliciesByQuery") + .summary("Lookup verification policies by query") + .description( + "Lookup verification policies by `name`, `attributes`, `issuerDIDs`, and `credentialTypes` and control the pagination by `offset` and `limit` parameters" + ) + .tag("Verification") +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/VerificationPolicyServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/VerificationPolicyServerEndpoints.scala new file mode 100644 index 0000000000..2ba69e577c --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/VerificationPolicyServerEndpoints.scala @@ -0,0 +1,114 @@ +package io.iohk.atala.pollux.schema + +import io.iohk.atala.api.http.model.{Order, Pagination} +import io.iohk.atala.api.http.{FailureResponse, InternalServerError, NotFoundResponse} +import io.iohk.atala.pollux.schema.VerificationPolicyEndpoints.* +import io.iohk.atala.pollux.schema.model.{VerificationPolicy, VerificationPolicyInput} +import io.iohk.atala.pollux.service.VerificationPolicyService +import sttp.tapir.redoc.RedocUIOptions +import sttp.tapir.redoc.bundle.RedocInterpreter +import sttp.tapir.server.ServerEndpoint +import sttp.tapir.swagger.bundle.SwaggerInterpreter +import sttp.tapir.ztapir.* +import zio.{Task, URIO, ZIO, ZLayer} + +import java.util.UUID + +class VerificationPolicyServerEndpoints( + service: VerificationPolicyService +) { + def throwableToInternalServerError(throwable: Throwable) = + ZIO.fail[FailureResponse](InternalServerError(throwable.getMessage)) + + // TODO: make the endpoint typed ZServerEndpoint[SchemaRegistryService, Any] + val createVerificationPolicyServerEndpoint: ZServerEndpoint[Any, Any] = + createVerificationPolicyEndpoint.zServerLogic(input => + service + .createVerificationPolicy(input) + .foldZIO(throwableToInternalServerError, vp => ZIO.succeed(vp)) + ) + + val updateVerificationPolicyServerEndpoint: ZServerEndpoint[Any, Any] = { + updateVerificationPolicyEndpoint.zServerLogic { case (id: String, update: VerificationPolicyInput) => + service + .updateVerificationPolicyById(id, update) + .foldZIO( + throwableToInternalServerError, + { + case Some(pv) => ZIO.succeed(pv) + case None => + ZIO.fail[FailureResponse]( + NotFoundResponse(s"Verification policy is not found by $id") + ) + } + ) + } + } + + val getVerificationPolicyByIdServerEndpoint: ZServerEndpoint[Any, Any] = + getVerificationPolicyByIdEndpoint.zServerLogic(id => + service + .getVerificationPolicyById(id) + .foldZIO( + throwableToInternalServerError, + { + case Some(pv) => ZIO.succeed(pv) + case None => + ZIO.fail[FailureResponse]( + NotFoundResponse(s"Verification policy is not found by $id") + ) + } + ) + ) + + val deleteVerificationPolicyByIdServerEndpoint: ZServerEndpoint[Any, Any] = + deleteVerificationPolicyByIdEndpoint.zServerLogic(id => + service + .deleteVerificationPolicyById(id) + .foldZIO( + throwableToInternalServerError, + { + case Some(_) => ZIO.succeed(()) + case None => + ZIO.fail[FailureResponse]( + NotFoundResponse(s"Verification policy is not found by $id") + ) + } + ) + ) + + val lookupVerificationPoliciesByQueryServerEndpoint: ZServerEndpoint[Any, Any] = + lookupVerificationPoliciesByQueryEndpoint.zServerLogic { + case ( + filter: VerificationPolicy.Filter, + page: Pagination, + order: Option[Order] + ) => + service + .lookupVerificationPolicies(filter, page, order) + .foldZIO( + throwableToInternalServerError, + pageOfVCS => ZIO.succeed(pageOfVCS) + ) + } + + val all: List[ZServerEndpoint[Any, Any]] = + List( + createVerificationPolicyServerEndpoint, + getVerificationPolicyByIdServerEndpoint, + updateVerificationPolicyServerEndpoint, + deleteVerificationPolicyByIdServerEndpoint, + lookupVerificationPoliciesByQueryServerEndpoint + ) +} + +object VerificationPolicyServerEndpoints { + def all: URIO[VerificationPolicyService, List[ZServerEndpoint[Any, Any]]] = { + for { + service <- ZIO.service[VerificationPolicyService] + endpoints = new VerificationPolicyServerEndpoints( + service + ) + } yield endpoints.all + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/Proof.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/Proof.scala new file mode 100644 index 0000000000..7be8a72619 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/Proof.scala @@ -0,0 +1,22 @@ +package io.iohk.atala.pollux.schema.model + +import sttp.tapir.Schema +import sttp.tapir.generic.auto.* +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} + +import java.time.ZonedDateTime + +case class Proof( + `type`: String, + created: ZonedDateTime, + verificationMethod: String, + proofPurpose: String, + proofValue: String, + domain: Option[String] +) + +object Proof { + given encoder: zio.json.JsonEncoder[Proof] = DeriveJsonEncoder.gen[Proof] + given decoder: zio.json.JsonDecoder[Proof] = DeriveJsonDecoder.gen[Proof] + given schema: Schema[Proof] = Schema.derived +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/VerifiableCredentialSchema.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/VerifiableCredentialSchema.scala new file mode 100644 index 0000000000..551bef16b7 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/VerifiableCredentialSchema.scala @@ -0,0 +1,89 @@ +package io.iohk.atala.pollux.schema.model + +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{description, encodedName} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} + +import java.time.ZonedDateTime +import java.util.UUID + +case class VerifiableCredentialSchema( + id: UUID, + name: String, + version: String, + tags: List[String], + description: Option[String], + attributes: List[String], + author: String, + authored: ZonedDateTime, + proof: Option[Proof] +) + +object VerifiableCredentialSchema { + def apply(in: VerifiableCredentialSchema.Input): VerifiableCredentialSchema = + VerifiableCredentialSchema( + id = in.id.getOrElse(UUID.randomUUID()), + name = in.name, + version = in.version, + tags = in.tags, + description = in.description, + attributes = in.attributes, + author = "Prism Agent", + authored = in.authored.getOrElse(ZonedDateTime.now()), + proof = None + ) + + given encoder: zio.json.JsonEncoder[VerifiableCredentialSchema] = + DeriveJsonEncoder.gen[VerifiableCredentialSchema] + + given decoder: zio.json.JsonDecoder[VerifiableCredentialSchema] = + DeriveJsonDecoder.gen[VerifiableCredentialSchema] + + given schema: Schema[VerifiableCredentialSchema] = Schema.derived + + case class Input( + id: Option[UUID], + name: String, + version: String, + description: Option[String], + attributes: List[String], + authored: Option[ZonedDateTime], + tags: List[String] + ) + + object Input { + given encoder: zio.json.JsonEncoder[Input] = + DeriveJsonEncoder.gen[Input] + + given decoder: zio.json.JsonDecoder[Input] = + DeriveJsonDecoder.gen[Input] + + given schema: Schema[Input] = Schema.derived + } + + case class Filter( + author: Option[String], + name: Option[String], + tags: Option[String] + ) { + def predicate(vcs: VerifiableCredentialSchema): Boolean = + name.forall(_ == vcs.name) && + author.forall(_ == vcs.author) && + tags.map(_.split(',')).forall(vcs.tags.intersect(_).nonEmpty) + } + + case class Page( + self: String, + kind: String, + pageOf: String, + next: Option[String], + previous: Option[String], + contents: List[VerifiableCredentialSchema] + ) + + object Page { + given encoder: zio.json.JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given decoder: zio.json.JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + given schema: sttp.tapir.Schema[Page] = Schema.derived + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/VerificationPolicy.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/VerificationPolicy.scala new file mode 100644 index 0000000000..903bb5f4f3 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/model/VerificationPolicy.scala @@ -0,0 +1,102 @@ +package io.iohk.atala.pollux.schema.model + +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{description, encodedName} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} + +import java.time.ZonedDateTime +import java.util.UUID + +case class VerificationPolicy( + self: String, + kind: String, + id: String, + name: String, + attributes: List[String], + issuerDIDs: List[String], + credentialTypes: List[String], + createdAt: ZonedDateTime, + updatedAt: ZonedDateTime +) { + def update(in: VerificationPolicyInput): VerificationPolicy = { + copy( + name = in.name, + attributes = in.attributes, + issuerDIDs = in.issuerDIDs, + credentialTypes = in.credentialTypes, + updatedAt = ZonedDateTime.now() + ) + } +} + +object VerificationPolicy { + def apply(in: VerificationPolicyInput): VerificationPolicy = + VerificationPolicy( + self = "to be defined", + kind = "VerificationPolicy", + id = in.id.getOrElse(UUID.randomUUID().toString), + name = in.name, + attributes = in.attributes, + issuerDIDs = in.issuerDIDs, + credentialTypes = in.credentialTypes, + createdAt = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now() + ) + + given encoder: zio.json.JsonEncoder[VerificationPolicy] = DeriveJsonEncoder.gen[VerificationPolicy] + given decoder: zio.json.JsonDecoder[VerificationPolicy] = DeriveJsonDecoder.gen[VerificationPolicy] + given schema: Schema[VerificationPolicy] = Schema.derived + + case class Filter( + name: Option[String], + attributes: Option[String], + issuerDIDs: Option[String], + credentialTypes: Option[String] + ) { + def predicate(vp: VerificationPolicy): Boolean = { + name.forall(vp.name == _) && + attributes.map(_.split(',')).forall(vp.attributes.intersect(_).nonEmpty) && + issuerDIDs.map(_.split(',')).forall(vp.issuerDIDs.intersect(_).nonEmpty) && + credentialTypes.map(_.split(',')).forall(vp.credentialTypes.intersect(_).nonEmpty) + } + } +} + +case class VerificationPolicyPage( + self: String, + kind: String, + pageOf: String, + next: Option[String], + previous: Option[String], + contents: List[VerificationPolicy] +) + +object VerificationPolicyPage { + given encoder: zio.json.JsonEncoder[VerificationPolicyPage] = + DeriveJsonEncoder.gen[VerificationPolicyPage] + + given decoder: zio.json.JsonDecoder[VerificationPolicyPage] = + DeriveJsonDecoder.gen[VerificationPolicyPage] + + given schema: Schema[VerificationPolicyPage] = + Schema.derived +} + +case class VerificationPolicyInput( + id: Option[String], + kind: String, + name: String, + attributes: List[String], + issuerDIDs: List[String], + credentialTypes: List[String] +) + +object VerificationPolicyInput { + given encoder: zio.json.JsonEncoder[VerificationPolicyInput] = + DeriveJsonEncoder.gen[VerificationPolicyInput] + + given decoder: zio.json.JsonDecoder[VerificationPolicyInput] = + DeriveJsonDecoder.gen[VerificationPolicyInput] + + given schema: Schema[VerificationPolicyInput] = Schema.derived +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryService.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryService.scala index f18732d62b..72b7d11656 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryService.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryService.scala @@ -1,21 +1,41 @@ package io.iohk.atala.pollux.service +import io.iohk.atala.api.http.model.{Order, Pagination} import zio.{Task, ZIO, ZLayer} -import io.iohk.atala.pollux.schema.{VerifiableCredentialsSchemaInput, VerifiableCredentialsSchema} +import io.iohk.atala.pollux.schema.model.VerifiableCredentialSchema import java.util.UUID trait SchemaRegistryService { - def createSchema(in: VerifiableCredentialsSchemaInput): Task[VerifiableCredentialsSchema] - def getSchemaById(id: UUID): Task[Option[VerifiableCredentialsSchema]] + def createSchema( + in: VerifiableCredentialSchema.Input + ): Task[VerifiableCredentialSchema] + def getSchemaById(id: UUID): Task[Option[VerifiableCredentialSchema]] + + def lookupSchemas( + filter: VerifiableCredentialSchema.Filter, + pagination: Pagination, + order: Option[Order] + ): Task[VerifiableCredentialSchema.Page] } object SchemaRegistryService { def createSchema( - in: VerifiableCredentialsSchemaInput - ): ZIO[SchemaRegistryService, Throwable, VerifiableCredentialsSchema] = + in: VerifiableCredentialSchema.Input + ): ZIO[SchemaRegistryService, Throwable, VerifiableCredentialSchema] = ZIO.serviceWithZIO[SchemaRegistryService](_.createSchema(in)) - def getSchemaById(id: UUID): ZIO[SchemaRegistryService, Throwable, Option[VerifiableCredentialsSchema]] = + def getSchemaById(id: UUID): ZIO[SchemaRegistryService, Throwable, Option[ + VerifiableCredentialSchema + ]] = ZIO.serviceWithZIO[SchemaRegistryService](_.getSchemaById(id)) + + def lookupSchemas( + filter: VerifiableCredentialSchema.Filter, + pagination: Pagination, + order: Option[Order] + ): ZIO[SchemaRegistryService, Throwable, VerifiableCredentialSchema.Page] = + ZIO.serviceWithZIO[SchemaRegistryService]( + _.lookupSchemas(filter, pagination, order) + ) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryServiceInMemory.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryServiceInMemory.scala index 691350b09e..1a3fb92540 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryServiceInMemory.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryServiceInMemory.scala @@ -1,31 +1,65 @@ package io.iohk.atala.pollux.service -import zio.{Task, ZIO, ZLayer, Ref, UIO} -import io.iohk.atala.pollux.schema.{VerifiableCredentialsSchemaInput, VerifiableCredentialsSchema} +import io.iohk.atala.api.http.model.{Order, Pagination} +import io.iohk.atala.pollux.schema.model.VerifiableCredentialSchema +import zio.{Ref, Task, UIO, ZIO, ZLayer} + import java.util.UUID import scala.collection.mutable -class SchemaRegistryServiceInMemory(ref: Ref[Map[UUID, VerifiableCredentialsSchema]]) extends SchemaRegistryService { +class SchemaRegistryServiceInMemory( + ref: Ref[Map[UUID, VerifiableCredentialSchema]] +) extends SchemaRegistryService { // TODO: Figure out what is the logic for trying to overwrite the schema with the same id (409 Conflict) // TODO: Other validations (same [schema_name, version], list of the attributes is not empty, etc) - override def createSchema(in: VerifiableCredentialsSchemaInput): Task[VerifiableCredentialsSchema] = { - val schema = VerifiableCredentialsSchema(in) + override def createSchema( + in: VerifiableCredentialSchema.Input + ): Task[VerifiableCredentialSchema] = { + val schema = VerifiableCredentialSchema(in) for { _ <- ref.update(s => s + (schema.id -> schema)) } yield schema } - override def getSchemaById(id: UUID): Task[Option[VerifiableCredentialsSchema]] = { + override def getSchemaById( + id: UUID + ): Task[Option[VerifiableCredentialSchema]] = { for { storage <- ref.get schema = storage.get(id) } yield schema } + + // TODO: this is naive implementation for demo purposes, sorting doesn't work + override def lookupSchemas( + filter: VerifiableCredentialSchema.Filter, + pagination: Pagination, + order: Option[Order] + ): Task[VerifiableCredentialSchema.Page] = { + for { + storage: Map[UUID, VerifiableCredentialSchema] <- ref.get + filtered = storage.values.filter(filter.predicate) + paginated = filtered.toList + .slice( + pagination.offset.getOrElse(0), + pagination.offset.getOrElse(0) + pagination.limit.getOrElse(10) + ) + } yield VerifiableCredentialSchema.Page( + self = "sss", + kind = "VerifiableCredentialSchema", + pageOf = "ppp", + next = None, + previous = None, + contents = paginated + ) + } } object SchemaRegistryServiceInMemory { val layer = ZLayer.fromZIO( - Ref.make(Map.empty[UUID, VerifiableCredentialsSchema]).map(SchemaRegistryServiceInMemory(_)) + Ref + .make(Map.empty[UUID, VerifiableCredentialSchema]) + .map(SchemaRegistryServiceInMemory(_)) ) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/VerificationPolicyService.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/VerificationPolicyService.scala new file mode 100644 index 0000000000..996ceb46e2 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/VerificationPolicyService.scala @@ -0,0 +1,28 @@ +package io.iohk.atala.pollux.service + +import io.iohk.atala.api.http.model.{Order, Pagination} +import io.iohk.atala.pollux.schema.model.{VerificationPolicy, VerificationPolicyInput, VerificationPolicyPage} +import zio.{Task, ZIO, ZLayer} + +import java.util.UUID + +trait VerificationPolicyService { + def createVerificationPolicy( + in: VerificationPolicyInput + ): Task[VerificationPolicy] + + def getVerificationPolicyById(id: String): Task[Option[VerificationPolicy]] + + def updateVerificationPolicyById( + id: String, + update: VerificationPolicyInput + ): Task[Option[VerificationPolicy]] + + def deleteVerificationPolicyById(id: String): Task[Option[VerificationPolicy]] + + def lookupVerificationPolicies( + filter: VerificationPolicy.Filter, + pagination: Pagination, + order: Option[Order] + ): Task[VerificationPolicyPage] +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/VerificationPolicyServiceInMemory.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/VerificationPolicyServiceInMemory.scala new file mode 100644 index 0000000000..b3f212492a --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/VerificationPolicyServiceInMemory.scala @@ -0,0 +1,90 @@ +package io.iohk.atala.pollux.service + +import io.iohk.atala.api.http.model.{Order, Pagination} +import io.iohk.atala.pollux.schema.model.{VerificationPolicy, VerificationPolicyInput, VerificationPolicyPage} +import zio.{Ref, Task, UIO, ZIO, ZLayer} + +import java.time.ZonedDateTime +import scala.collection.mutable + +class VerificationPolicyServiceInMemory( + ref: Ref[Map[String, VerificationPolicy]] +) extends VerificationPolicyService { + + // TODO: Figure out what is the logic for trying to overwrite the schema with the same id (409 Conflict) + // TODO: Other validations (same [schema_name, version], list of the attributes is not empty, etc) + override def createVerificationPolicy( + in: VerificationPolicyInput + ): Task[VerificationPolicy] = { + val vp = VerificationPolicy(in) + for { + _ <- ref.update(s => s + (vp.id -> vp)) + } yield vp + } + + override def getVerificationPolicyById( + id: String + ): Task[Option[VerificationPolicy]] = { + for { + storage <- ref.get + vp = storage.get(id) + } yield vp + } + + override def updateVerificationPolicyById( + id: String, + in: VerificationPolicyInput + ): Task[Option[VerificationPolicy]] = { + for { + storage: Map[String, VerificationPolicy] <- ref.updateAndGet(kv => + kv.get(id) + .fold(kv)(oldVp => kv + (id -> oldVp.update(in))) + ) + vp = storage.get(id) + } yield vp + } + + override def deleteVerificationPolicyById( + id: String + ): Task[Option[VerificationPolicy]] = { + for { + storage: Map[String, VerificationPolicy] <- ref.getAndUpdate(kv => + kv.get(id) + .fold(kv)(_ => kv - id) + ) + vp = storage.get(id) + } yield vp + } + + // TODO: this is naive implementation for demo purposes, sorting doesn't work + override def lookupVerificationPolicies( + filter: VerificationPolicy.Filter, + pagination: Pagination, + order: Option[Order] + ): Task[VerificationPolicyPage] = { + for { + storage: Map[String, VerificationPolicy] <- ref.get + filtered = storage.values.filter(filter.predicate) + paginated = filtered.toList + .slice( + pagination.offset.getOrElse(0), + pagination.offset.getOrElse(0) + pagination.limit.getOrElse(10) + ) + } yield VerificationPolicyPage( + self = "to be defined", + kind = "VerifiableCredentialSchema", + pageOf = "to be defined", + next = None, + previous = None, + contents = paginated + ) + } +} + +object VerificationPolicyServiceInMemory { + val layer = ZLayer.fromZIO( + Ref + .make(Map.empty[String, VerificationPolicy]) + .map(VerificationPolicyServiceInMemory(_)) + ) +} diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpointsSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpointsSpec.scala new file mode 100644 index 0000000000..3099a6d834 --- /dev/null +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpointsSpec.scala @@ -0,0 +1,108 @@ +package io.iohk.atala.pollux.schema + +import io.iohk.atala.api.http.NotFoundResponse +import io.iohk.atala.pollux.service.SchemaRegistryService +import sttp.client3.testing.SttpBackendStub +import sttp.client3.ziojson.* +import sttp.client3.{ResponseException, UriContext, basicRequest} +import sttp.tapir.server.interceptor.RequestResult.Response +import sttp.tapir.server.stub.TapirStubInterpreter +import sttp.tapir.ztapir.RIOMonadError +import zio.ZIO +import zio.test.Assertion.* +import zio.test.{Assertion, ZIOSpecDefault, assertZIO} +import io.iohk.atala.pollux.schema.* +import io.iohk.atala.pollux.schema.model.VerifiableCredentialSchema +import io.iohk.atala.pollux.service.SchemaRegistryServiceInMemory +import sttp.model.StatusCode +import zio.ZLayer +import zio.json.{DecoderOps, EncoderOps} + +import java.time.ZonedDateTime +import java.util.UUID + +object SchemaRegistryEndpointsSpec extends ZIOSpecDefault: + + private val schemaId = UUID.randomUUID() + + private val schemaInput = VerifiableCredentialSchema.Input( + Option(schemaId), + name = "test schema", + version = "1.0", + description = Option("schema description"), + attributes = List("first_name", "dob"), + authored = Option(ZonedDateTime.now()), + tags = List("test") + ) + + private val schema = VerifiableCredentialSchema(schemaInput) + + def httpBackend(schemaRegistryService: SchemaRegistryService) = { + val schemaRegistryEndpoints = SchemaRegistryServerEndpoints(schemaRegistryService) + val backend = TapirStubInterpreter(SttpBackendStub(new RIOMonadError[Any])) + .whenServerEndpoint(schemaRegistryEndpoints.createSchemaServerEndpoint) + .thenRunLogic() + .whenServerEndpoint(schemaRegistryEndpoints.getSchemaByIdServerEndpoint) + .thenRunLogic() + .backend() + backend + } + + def spec = suite("Schema Endpoints spec")( + createSchemaSpec // , updateSchemaSpec + ).provideLayer(SchemaRegistryServiceInMemory.layer) + + private val createSchemaSpec = suite("create schema")( + test("create new schema") { + for { + schemaRegistryService <- ZIO.service[SchemaRegistryService] + backend = httpBackend(schemaRegistryService) + + response = basicRequest + .post(uri"http://test.com/schema-registry/schemas") + .body(schemaInput.toJsonPretty) + .response(asJson[VerifiableCredentialSchema]) + .send(backend) + + assertion <- assertZIO(response.map(_.body))(isRight(equalTo(schema))) + } yield assertion + }, + test("create and get a schema by id") { + for { + schemaRegistryService <- ZIO.service[SchemaRegistryService] + backend = httpBackend(schemaRegistryService) + + _ <- basicRequest + .post(uri"http://test.com/schema-registry/schemas") + .body(schemaInput.toJsonPretty) + .send(backend) + + response = basicRequest + .get(uri"http://test.com/schema-registry/schemas/$schemaId") + .response(asJson[VerifiableCredentialSchema]) + .send(backend) + + assertion <- assertZIO(response.map(_.body))(isRight(equalTo(schema))) + } yield assertion + }, + test("create and get a schema by random id") { + for { + schemaRegistryService <- ZIO.service[SchemaRegistryService] + backend = httpBackend(schemaRegistryService) + + _ <- basicRequest + .post(uri"http://test.com/schema-registry/schemas") + .body(schemaInput.toJsonPretty) + .send(backend) + + uuid = UUID.randomUUID() + + response = basicRequest + .get(uri"http://test.com/schema-registry/schemas/$uuid") + .response(asJson[NotFoundResponse]) + .send(backend) + + assertion <- assertZIO(response.map(_.code))(equalTo(StatusCode.NotFound)) + } yield assertion + } + )