From 95d328e3420d4731817a1f91c720e2833e9de362 Mon Sep 17 00:00:00 2001 From: patlo-iog Date: Mon, 23 Sep 2024 21:47:33 +0700 Subject: [PATCH 01/13] fix: oas to use any schema for json ast node (#1372) Signed-off-by: Pat Losoponkul --- .../api/http/codec/CirceJsonInterop.scala | 15 --------------- .../castor/controller/http/Service.scala | 18 +++++++++--------- .../PresentationExchangeTapirSchemas.scala | 4 +--- 3 files changed, 10 insertions(+), 27 deletions(-) delete mode 100644 cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala deleted file mode 100644 index 64269a575d..0000000000 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.hyperledger.identus.api.http.codec - -import io.circe.Json as CirceJson -import org.hyperledger.identus.shared.json.JsonInterop -import sttp.tapir.json.zio.* -import sttp.tapir.Schema -import zio.json.* -import zio.json.ast.Json as ZioJson - -object CirceJsonInterop { - given encodeJson: JsonEncoder[CirceJson] = JsonEncoder[ZioJson].contramap(JsonInterop.toZioJsonAst) - given decodeJson: JsonDecoder[CirceJson] = JsonDecoder[ZioJson].map(JsonInterop.toCirceJsonAst) - given schemaJson: Schema[CirceJson] = - Schema.derived[ZioJson].map[CirceJson](js => Some(JsonInterop.toCirceJsonAst(js)))(JsonInterop.toZioJsonAst) -} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala index 7229e911fc..44a1bfd788 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala @@ -1,15 +1,15 @@ package org.hyperledger.identus.castor.controller.http -import io.circe.Json -import org.hyperledger.identus.api.http.codec.CirceJsonInterop import org.hyperledger.identus.api.http.Annotation import org.hyperledger.identus.castor.controller.http.Service.annotations import org.hyperledger.identus.castor.core.model.{did as castorDomain, ProtoModelHelper} import org.hyperledger.identus.castor.core.model.did.w3c +import org.hyperledger.identus.shared.json.JsonInterop import org.hyperledger.identus.shared.utils.Traverse.* import sttp.tapir.Schema import sttp.tapir.Schema.annotations.{description, encodedExample} -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} +import zio.json.ast.Json import scala.language.implicitConversions @@ -48,7 +48,7 @@ object Service { extends Annotation[Json]( description = "The service endpoint. Can contain multiple possible values as described in the [Create DID operation](https://github.com/input-output-hk/prism-did-method-spec/blob/main/w3c-spec/PRISM-method.md#create-did)", - example = Json.fromString("https://example.com") + example = Json.Str("https://example.com") ) } @@ -62,7 +62,7 @@ object Service { Service( id = service.id, `type` = service.`type`, - serviceEndpoint = ServiceEndpoint.fromJson(service.serviceEndpoint) + serviceEndpoint = ServiceEndpoint.fromJson(JsonInterop.toZioJsonAst(service.serviceEndpoint)) ) extension (service: Service) { @@ -139,9 +139,9 @@ object ServiceType { opaque type ServiceEndpoint = Json object ServiceEndpoint { - given encoder: JsonEncoder[ServiceEndpoint] = CirceJsonInterop.encodeJson - given decoder: JsonDecoder[ServiceEndpoint] = CirceJsonInterop.decodeJson - given schema: Schema[ServiceEndpoint] = CirceJsonInterop.schemaJson + given encoder: JsonEncoder[ServiceEndpoint] = Json.encoder + given decoder: JsonDecoder[ServiceEndpoint] = Json.decoder + given schema: Schema[ServiceEndpoint] = Schema.any[ServiceEndpoint] def fromJson(json: Json): ServiceEndpoint = json @@ -149,7 +149,7 @@ object ServiceEndpoint { def toDomain: Either[String, castorDomain.ServiceEndpoint] = { val stringEncoded = serviceEndpoint.asString match { case Some(s) => s - case None => serviceEndpoint.noSpaces + case None => serviceEndpoint.toJson } ProtoModelHelper.parseServiceEndpoint(stringEncoded) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/prex/http/PresentationExchangeTapirSchemas.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/prex/http/PresentationExchangeTapirSchemas.scala index 91b129f5f2..e42fe2dcf9 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/prex/http/PresentationExchangeTapirSchemas.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/prex/http/PresentationExchangeTapirSchemas.scala @@ -1,9 +1,7 @@ package org.hyperledger.identus.pollux.prex.http import org.hyperledger.identus.pollux.prex.* -import sttp.tapir.json.zio.* import sttp.tapir.Schema -import zio.json.ast.Json import scala.language.implicitConversions @@ -20,5 +18,5 @@ object PresentationExchangeTapirSchemas { given Schema[Ldp] = Schema.derived given Schema[Field] = Schema.derived given Schema[JsonPathValue] = Schema.schemaForString.map[JsonPathValue](Some(_))(_.value) - given Schema[FieldFilter] = Schema.derived[Json].map[FieldFilter](Some(_))(_.asJsonZio) + given Schema[FieldFilter] = Schema.any[FieldFilter] } From 8fc2fe3dbed8856d21c18b7fedf89454661b34d6 Mon Sep 17 00:00:00 2001 From: Bassam Date: Tue, 24 Sep 2024 10:21:43 -0400 Subject: [PATCH 02/13] feat: Issuer Replace Either By Union Type (#1374) Signed-off-by: Bassam Riman --- .../http/StatusListCredential.scala | 27 +++++++++++--- .../service/OIDCCredentialIssuerService.scala | 2 +- .../VcVerificationControllerImplSpec.scala | 2 +- .../core/service/CredentialServiceImpl.scala | 2 +- .../service/CredentialServiceImplSpec.scala | 8 ++--- .../VcVerificationServiceImplSpec.scala | 24 ++++++------- ...esentationSubmissionVerificationSpec.scala | 2 +- .../vc/jwt/VerifiableCredentialPayload.scala | 36 ++++++++++++------- .../vc/jwt/revocation/VCStatusList2021.scala | 2 +- .../pollux/vc/jwt/JWTVerificationTest.scala | 14 +++++--- 10 files changed, 75 insertions(+), 44 deletions(-) diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala index 8d59f50530..7627d75bb1 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala @@ -22,7 +22,7 @@ case class StatusListCredential( `type`: Set[String], @description(annotations.issuer.description) @encodedExample(annotations.issuer.example) - issuer: Either[String, CredentialIssuer], + issuer: String | CredentialIssuer, @description(annotations.id.description) @encodedExample(annotations.id.example) id: String, @@ -156,11 +156,18 @@ object StatusListCredential { given credentialIssuerDecoder: JsonDecoder[CredentialIssuer] = DeriveJsonDecoder.gen[CredentialIssuer] - given eitherStringOrCredentialIssuerEncoder: JsonEncoder[Either[String, CredentialIssuer]] = - JsonEncoder[String].orElseEither(JsonEncoder[CredentialIssuer]) + given stringOrCredentialIssuerEncoder: JsonEncoder[String | CredentialIssuer] = + JsonEncoder[String] + .orElseEither(JsonEncoder[CredentialIssuer]) + .contramap[String | CredentialIssuer] { + case string: String => Left(string) + case credentialIssuer: CredentialIssuer => Right(credentialIssuer) + } - given eitherStringOrCredentialIssuerDecoder: JsonDecoder[Either[String, CredentialIssuer]] = - JsonDecoder[CredentialIssuer].map(Right(_)).orElse(JsonDecoder[String].map(Left(_))) + given stringOrCredentialIssuerDecoder: JsonDecoder[String | CredentialIssuer] = + JsonDecoder[CredentialIssuer] + .map(issuer => issuer: String | CredentialIssuer) + .orElse(JsonDecoder[String].map(schemaId => schemaId: String | CredentialIssuer)) given statusListCredentialEncoder: JsonEncoder[StatusListCredential] = DeriveJsonEncoder.gen[StatusListCredential] @@ -180,6 +187,16 @@ object StatusListCredential { given credentialIssuerSchema: Schema[CredentialIssuer] = Schema.derived + given schemaIssuer: Schema[String | CredentialIssuer] = Schema + .schemaForEither(Schema.schemaForString, Schema.derived[CredentialIssuer]) + .map[String | CredentialIssuer] { + case Left(string) => Some(string) + case Right(credentialIssuer) => Some(credentialIssuer) + } { + case string: String => Left(string) + case credentialIssuer: CredentialIssuer => Right(credentialIssuer) + } + given statusListCredentialSchema: Schema[StatusListCredential] = Schema.derived } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala index 839f1a4d8e..d57bb12493 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala @@ -199,7 +199,7 @@ case class OIDCCredentialIssuerServiceImpl( `type` = Set( "VerifiableCredential" ) ++ credentialDefinition.`type`, // TODO: This information should come from Schema registry by record.schemaId - issuer = Left(issuerDid.toString), + issuer = issuerDid.toString, issuanceDate = Instant.now(), maybeExpirationDate = None, // TODO: Add expiration date maybeCredentialSchema = None, // TODO: Add schema from schema registry diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala index b2e0fcf012..210390472c 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala @@ -36,7 +36,7 @@ object VcVerificationControllerImplSpec extends ZIOSpecDefault with VcVerificati `@context` = Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala index ec86a7fa87..cfb7c3c04e 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala @@ -1140,7 +1140,7 @@ class CredentialServiceImpl( maybeId = None, `type` = Set("VerifiableCredential"), // TODO: This information should come from Schema registry by record.schemaId - issuer = Right(CredentialIssuer(jwtIssuer.did.toString, `type` = "Profile")), + issuer = CredentialIssuer(jwtIssuer.did.toString, `type` = "Profile"), issuanceDate = issuanceDate, maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)), maybeCredentialSchema = record.schemaUri.map(id => diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala index a63890f44a..32711c1445 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala @@ -538,11 +538,9 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS _ <- holderSvc.receiveCredentialIssue(issueCredential) } yield assertTrue( decodedJWT.issuer == - Right( - CredentialIssuer( - id = decodedJWT.iss, - `type` = "Profile" - ) + CredentialIssuer( + id = decodedJWT.iss, + `type` = "Profile" ) ) }.provideSomeLayer( diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala index 62be6e2e87..e7e5c01f35 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala @@ -27,7 +27,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -94,7 +94,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -161,7 +161,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -228,7 +228,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -302,7 +302,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -373,7 +373,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -444,7 +444,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -524,7 +524,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -605,7 +605,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -673,7 +673,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -741,7 +741,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), @@ -809,7 +809,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), maybeId = Some("http://example.edu/credentials/3732"), `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), - issuer = Left(issuer.did.toString), + issuer = issuer.did.toString, issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), diff --git a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala index 60b025826e..90f1c40507 100644 --- a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala +++ b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala @@ -74,7 +74,7 @@ object PresentationSubmissionVerificationSpec extends ZIOSpecDefault { maybeTermsOfUse = None, maybeValidFrom = None, maybeValidUntil = None, - maybeIssuer = Some(Left(iss)) + maybeIssuer = Some(iss) ), nbf = jwtCredentialNbf, aud = Set.empty, diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala index cb7d84dd0b..4cb35fb17f 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala @@ -77,7 +77,7 @@ sealed trait CredentialPayload { def maybeValidUntil: Option[Instant] - def issuer: Either[String, CredentialIssuer] + def issuer: String | CredentialIssuer def maybeCredentialStatus: Option[CredentialStatus] @@ -93,7 +93,10 @@ sealed trait CredentialPayload { def toJwtCredentialPayload: JwtCredentialPayload = JwtCredentialPayload( - iss = issuer.fold(identity, _.id), + iss = issuer match { + case string: String => string + case credentialIssuer: CredentialIssuer => credentialIssuer.id + }, maybeSub = maybeSub, vc = JwtVc( `@context` = `@context`, @@ -141,7 +144,7 @@ case class JwtVc( credentialSubject: Json, maybeValidFrom: Option[Instant], maybeValidUntil: Option[Instant], - maybeIssuer: Option[Either[String, CredentialIssuer]], + maybeIssuer: Option[String | CredentialIssuer], maybeCredentialStatus: Option[CredentialStatus], maybeRefreshService: Option[RefreshService], maybeEvidence: Option[Json], @@ -167,14 +170,14 @@ case class JwtCredentialPayload( override val credentialSubject = vc.credentialSubject override val maybeValidFrom = vc.maybeValidFrom override val maybeValidUntil = vc.maybeValidUntil - override val issuer = vc.maybeIssuer.getOrElse(Left(iss)) + override val issuer = vc.maybeIssuer.getOrElse(iss) } case class W3cCredentialPayload( override val `@context`: Set[String], override val `type`: Set[String], maybeId: Option[String], - issuer: Either[String, CredentialIssuer], + issuer: String | CredentialIssuer, issuanceDate: Instant, maybeExpirationDate: Option[Instant], override val maybeCredentialSchema: Option[Either[CredentialSchema, List[CredentialSchema]]], @@ -236,9 +239,9 @@ object CredentialPayload { ("statusListCredential", credentialStatus.statusListCredential.asJson) ) - implicit val eitherStringOrCredentialIssuerEncoder: Encoder[Either[String, CredentialIssuer]] = { - case Left(value) => Json.fromString(value) - case Right(issuer) => issuer.asJson + implicit val stringOrCredentialIssuerEncoder: Encoder[String | CredentialIssuer] = Encoder.instance { + case string: String => Encoder[String].apply(string) + case credentialIssuer: CredentialIssuer => Encoder[CredentialIssuer].apply(credentialIssuer) } implicit val eitherCredentialSchemaOrListEncoder: Encoder[Either[CredentialSchema, List[CredentialSchema]]] = { @@ -370,8 +373,10 @@ object CredentialPayload { ) } - implicit val eitherStringOrCredentialIssuerDecoder: Decoder[Either[String, CredentialIssuer]] = - Decoder[String].map(Left(_)).or(Decoder[CredentialIssuer].map(Right(_))) + implicit val stringOrCredentialIssuerDecoder: Decoder[String | CredentialIssuer] = + Decoder[String] + .map(schema => schema: String | CredentialIssuer) + .or(Decoder[CredentialIssuer].map(schema => schema: String | CredentialIssuer)) implicit val eitherCredentialSchemaOrListDecoder: Decoder[Either[CredentialSchema, List[CredentialSchema]]] = Decoder[CredentialSchema] @@ -390,7 +395,7 @@ object CredentialPayload { .as[Set[String]] .orElse(c.downField("type").as[String].map(Set(_))) maybeId <- c.downField("id").as[Option[String]] - issuer <- c.downField("issuer").as[Either[String, CredentialIssuer]] + issuer <- c.downField("issuer").as[String | CredentialIssuer] issuanceDate <- c.downField("issuanceDate").as[Instant] maybeExpirationDate <- c.downField("expirationDate").as[Option[Instant]] maybeValidFrom <- c.downField("validFrom").as[Option[Instant]] @@ -444,7 +449,7 @@ object CredentialPayload { maybeTermsOfUse <- c.downField("termsOfUse").as[Option[Json]] maybeValidFrom <- c.downField("validFrom").as[Option[Instant]] maybeValidUntil <- c.downField("validUntil").as[Option[Instant]] - maybeIssuer <- c.downField("issuer").as[Option[Either[String, CredentialIssuer]]] + maybeIssuer <- c.downField("issuer").as[Option[String | CredentialIssuer]] } yield { JwtVc( `@context` = `@context`, @@ -888,7 +893,12 @@ object W3CCredential { )(didResolver: DidResolver): IO[String, Validation[String, Unit]] = { JWTVerification.validateEncodedJwt(payload.proof.jwt, proofPurpose)(didResolver: DidResolver)(claim => Validation.fromEither(decode[W3cCredentialPayload](claim).left.map(_.toString)) - )(_.issuer.fold(identity, _.id)) + )(vc => + vc.issuer match { + case string: String => string + case credentialIssuer: CredentialIssuer => credentialIssuer.id + } + ) } def verifyDates(w3cPayload: W3cVerifiableCredentialPayload, leeway: TemporalAmount)(implicit diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala index 8629b9cc1d..dbaf540498 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala @@ -61,7 +61,7 @@ object VCStatusList2021 { ), maybeId = Some(vcId), `type` = Set("VerifiableCredential", "StatusList2021Credential"), - issuer = Left(jwtIssuer.did.toString), + issuer = jwtIssuer.did.toString, issuanceDate = Instant.now, maybeExpirationDate = None, maybeCredentialSchema = None, diff --git a/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala b/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala index 7572353859..e09f9e6e96 100644 --- a/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala +++ b/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala @@ -82,8 +82,8 @@ object JWTVerificationTest extends ZIOSpecDefault { maybeValidFrom = Some(validFrom), maybeValidUntil = Some(validUntil), maybeIssuer = Some( - if (issuerAsObject) Right(CredentialIssuer(issuer.issuer.did.toString, "Profile")) - else Left(issuer.issuer.did.toString) + if (issuerAsObject) CredentialIssuer(issuer.issuer.did.toString, "Profile") + else issuer.issuer.did.toString ) ), nbf = jwtCredentialNbf, // ISSUANCE DATE @@ -211,8 +211,14 @@ object JWTVerificationTest extends ZIOSpecDefault { .decodeJwt(jwtCredential) jwtWithObjectIssuer <- JwtCredential .decodeJwt(jwtCredentialWithObjectIssuer) - jwtWithObjectIssuerIssuer = jwtWithObjectIssuer.vc.maybeIssuer.get.toOption.get.id - jwtIssuer = jwt.vc.maybeIssuer.get.left.toOption.get + jwtWithObjectIssuerIssuer = jwtWithObjectIssuer.vc.maybeIssuer.get match { + case string: String => string + case credentialIssuer: CredentialIssuer => credentialIssuer.id + } + jwtIssuer = jwt.vc.maybeIssuer.get match { + case string: String => string + case credentialIssuer: CredentialIssuer => credentialIssuer.id + } } yield assertTrue( jwtWithObjectIssuerIssuer.equals(jwtIssuer) ) From e532ba604c4e8e820345226d842d3b27813f5e66 Mon Sep 17 00:00:00 2001 From: patlo-iog Date: Thu, 26 Sep 2024 15:54:17 +0700 Subject: [PATCH 03/13] fix: return 404 when create credConfig on non-existing issuer (#1379) Signed-off-by: Pat Losoponkul --- .../core/service/OID4VCIIssuerMetadataService.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala index 9fe6e171fa..c0db44819d 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala @@ -67,7 +67,7 @@ trait OID4VCIIssuerMetadataService { format: CredentialFormat, configurationId: String, schemaId: String - ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] + ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat | IssuerIdNotFound, CredentialConfiguration] def getCredentialConfigurations( issuerId: UUID ): IO[IssuerIdNotFound, Seq[CredentialConfiguration]] @@ -127,8 +127,13 @@ class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataReposito format: CredentialFormat, configurationId: String, schemaId: String - ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] = { + ): ZIO[ + WalletAccessContext, + InvalidSchemaId | UnsupportedCredentialFormat | IssuerIdNotFound, + CredentialConfiguration + ] = { for { + _ <- getCredentialIssuer(issuerId) _ <- format match { case CredentialFormat.JWT => ZIO.unit case f => ZIO.fail(UnsupportedCredentialFormat(f)) From 693dcc45274044ac9bebffe2a8dbe0b85b45b452 Mon Sep 17 00:00:00 2001 From: Bassam Date: Thu, 26 Sep 2024 13:07:33 -0400 Subject: [PATCH 04/13] feat: Default Backend API to Array Of Credential Schema (#1366) Signed-off-by: Bassam Riman --- .../controller/IssueControllerImpl.scala | 19 +-- .../CreateIssueCredentialRecordRequest.scala | 27 +++- .../VcVerificationControllerImplSpec.scala | 8 +- .../issuecredential/CredentialPreview.scala | 10 +- .../core/model/IssueCredentialRecord.scala | 4 +- .../repository/CredentialRepository.scala | 2 +- .../core/service/CredentialService.scala | 4 +- .../core/service/CredentialServiceImpl.scala | 57 +++++---- .../service/CredentialServiceNotifier.scala | 8 +- .../service/PresentationServiceImpl.scala | 2 +- .../VcVerificationServiceImpl.scala | 19 ++- .../CredentialRepositoryInMemory.scala | 8 +- .../CredentialRepositorySpecSuite.scala | 6 +- .../service/CredentialServiceImplSpec.scala | 12 +- .../service/CredentialServiceSpecHelper.scala | 4 +- .../core/service/MockCredentialService.scala | 12 +- .../service/PresentationServiceSpec.scala | 4 +- .../PresentationServiceSpecHelper.scala | 2 +- .../VcVerificationServiceImplSpec.scala | 116 +++++++----------- ...28__support_multiple_credential_schema.sql | 9 ++ .../repository/JdbcCredentialRepository.scala | 20 +-- .../vc/jwt/VerifiableCredentialPayload.scala | 22 ++-- 22 files changed, 205 insertions(+), 170 deletions(-) create mode 100644 pollux/sql-doobie/src/main/resources/sql/pollux/V28__support_multiple_credential_schema.sql diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala index 587b3376da..2624e36d11 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala @@ -11,13 +11,7 @@ import org.hyperledger.identus.api.util.PaginationUtils import org.hyperledger.identus.castor.core.model.did.{PrismDID, VerificationRelationship} import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.connect.core.service.ConnectionService -import org.hyperledger.identus.issue.controller.http.{ - AcceptCredentialOfferInvitation, - AcceptCredentialOfferRequest, - CreateIssueCredentialRecordRequest, - IssueCredentialRecord, - IssueCredentialRecordPage -} +import org.hyperledger.identus.issue.controller.http.* import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.pollux.core.model.{CredentialFormat, DidCommID} import org.hyperledger.identus.pollux.core.model.CredentialFormat.{AnonCreds, JWT, SDJWT} @@ -48,6 +42,7 @@ class IssueControllerImpl( request: CreateIssueCredentialRecordRequest, offerContext: OfferContext ): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecord] = { + for { jsonClaims <- ZIO .fromEither(io.circe.parser.parse(request.claims.toString())) @@ -69,7 +64,10 @@ class IssueControllerImpl( pairwiseHolderDID = offerContext.pairwiseHolderDID, kidIssuer = request.issuingKid, thid = DidCommID(), - maybeSchemaId = request.schemaId, + maybeSchemaIds = request.schemaId.map { + case schemaId: String => List(schemaId) + case schemaIds: List[String] => schemaIds + }, claims = jsonClaims, validityPeriod = request.validityPeriod, automaticIssuance = request.automaticIssuance.orElse(Some(true)), @@ -94,7 +92,10 @@ class IssueControllerImpl( pairwiseHolderDID = offerContext.pairwiseHolderDID, kidIssuer = request.issuingKid, thid = DidCommID(), - maybeSchemaId = request.schemaId, + maybeSchemaIds = request.schemaId.map { + case schemaId: String => List(schemaId) + case schemaIds: List[String] => schemaIds + }, claims = jsonClaims, validityPeriod = request.validityPeriod, automaticIssuance = request.automaticIssuance.orElse(Some(true)), diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala index 405b302409..a19ce368b0 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala @@ -9,6 +9,7 @@ import sttp.tapir.Schema.annotations.{description, encodedExample} import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} import java.util.UUID +import scala.language.implicitConversions /** A class to represent an incoming request to create a new credential offer. * @@ -33,7 +34,7 @@ final case class CreateIssueCredentialRecordRequest( validityPeriod: Option[Double] = None, @description(annotations.schemaId.description) @encodedExample(annotations.schemaId.example) - schemaId: Option[String], + schemaId: Option[String | List[String]] = None, @description(annotations.credentialDefinitionId.description) @encodedExample(annotations.credentialDefinitionId.example) credentialDefinitionId: Option[UUID], @@ -178,6 +179,19 @@ object CreateIssueCredentialRecordRequest { ) } + given schemaIdEncoder: JsonEncoder[String | List[String]] = + JsonEncoder[String] + .orElseEither(JsonEncoder[List[String]]) + .contramap[String | List[String]] { + case schemaId: String => Left(schemaId) + case schemaIds: List[String] => Right(schemaIds) + } + + given schemaIdDecoder: JsonDecoder[String | List[String]] = + JsonDecoder[List[String]] + .map(schemaId => schemaId: String | List[String]) + .orElse(JsonDecoder[String].map(schemaId => schemaId: String | List[String])) + given encoder: JsonEncoder[CreateIssueCredentialRecordRequest] = DeriveJsonEncoder.gen[CreateIssueCredentialRecordRequest] @@ -185,6 +199,17 @@ object CreateIssueCredentialRecordRequest { DeriveJsonDecoder.gen[CreateIssueCredentialRecordRequest] given schemaJson: Schema[KeyId] = Schema.schemaForString.map[KeyId](v => Some(KeyId(v)))(KeyId.value) + + given schemaId: Schema[String | List[String]] = Schema + .schemaForEither(Schema.schemaForString, Schema.schemaForArray[String]) + .map[String | List[String]] { + case Left(value) => Some(value) + case Right(values) => Some(values.toList) + } { + case value: String => Left(value) + case values: List[String] => Right(values.toArray) + } + given schema: Schema[CreateIssueCredentialRecordRequest] = Schema.derived } diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala index 210390472c..2e75ce25a2 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala @@ -42,11 +42,9 @@ object VcVerificationControllerImplSpec extends ZIOSpecDefault with VcVerificati maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( diff --git a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/CredentialPreview.scala b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/CredentialPreview.scala index 9997acdaf1..4351ca6367 100644 --- a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/CredentialPreview.scala +++ b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/CredentialPreview.scala @@ -27,14 +27,20 @@ import io.circe.generic.semiauto.* */ final case class CredentialPreview( `type`: String = "https://didcomm.org/issue-credential/3.0/credential-credential", + schema_ids: Option[List[String]] = None, schema_id: Option[String] = None, body: CredentialPreviewBody, ) object CredentialPreview { def apply(attributes: Seq[Attribute]) = new CredentialPreview(body = CredentialPreviewBody(attributes)) - def apply(schema_id: Option[String], attributes: Seq[Attribute]) = - new CredentialPreview(schema_id = schema_id, body = CredentialPreviewBody(attributes)) + def apply(schema_ids: Option[List[String]], attributes: Seq[Attribute]) = + new CredentialPreview( + schema_ids = schema_ids, + // Done for backward compatibility + schema_id = schema_ids.flatMap(s => s.headOption), + body = CredentialPreviewBody(attributes) + ) given Encoder[CredentialPreview] = deriveEncoder[CredentialPreview] given Decoder[CredentialPreview] = deriveDecoder[CredentialPreview] diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/IssueCredentialRecord.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/IssueCredentialRecord.scala index 2a9dfce4d6..62829323d3 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/IssueCredentialRecord.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/IssueCredentialRecord.scala @@ -23,7 +23,7 @@ final case class IssueCredentialRecord( createdAt: Instant, updatedAt: Option[Instant], thid: DidCommID, - schemaUri: Option[String], + schemaUris: Option[List[String]], credentialDefinitionId: Option[UUID], credentialDefinitionUri: Option[String], credentialFormat: CredentialFormat, @@ -86,7 +86,7 @@ final case class ValidFullIssuedCredentialRecord( id: DidCommID, issuedCredential: Option[IssueCredential], credentialFormat: CredentialFormat, - schemaUri: Option[String], + schemaUris: Option[List[String]], credentialDefinitionUri: Option[String], subjectId: Option[String], keyId: Option[KeyId], diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepository.scala index 2ac6c75c7a..896ef7b971 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepository.scala @@ -79,7 +79,7 @@ trait CredentialRepository { recordId: DidCommID, issue: IssueCredential, issuedRawCredential: String, - schemaUri: Option[String], + schemaUris: Option[List[String]], credentialDefinitionUri: Option[String], protocolState: ProtocolState ): URIO[WalletAccessContext, Unit] diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala index e1341a3dbb..1e7fb43a4b 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala @@ -27,7 +27,7 @@ trait CredentialService { pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: io.circe.Json, validityPeriod: Option[Double] = None, automaticIssuance: Option[Boolean], @@ -43,7 +43,7 @@ trait CredentialService { pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: io.circe.Json, validityPeriod: Option[Double] = None, automaticIssuance: Option[Boolean], diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala index cfb7c3c04e..0063b8715a 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala @@ -127,7 +127,7 @@ class CredentialServiceImpl( pairwiseIssuerDID: DidId, kidIssuer: Option[KeyId], thid: DidCommID, - schemaUri: Option[String], + schemaUris: Option[List[String]], validityPeriod: Option[Double], automaticIssuance: Option[Boolean], issuingDID: Option[CanonicalPrismDID], @@ -161,7 +161,7 @@ class CredentialServiceImpl( createdAt = Instant.now, updatedAt = None, thid = thid, - schemaUri = schemaUri, + schemaUris = schemaUris, credentialDefinitionId = credentialDefinitionGUID, credentialDefinitionUri = credentialDefinitionId, credentialFormat = credentialFormat, @@ -196,7 +196,7 @@ class CredentialServiceImpl( pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: Json, validityPeriod: Option[Double], automaticIssuance: Option[Boolean], @@ -207,12 +207,12 @@ class CredentialServiceImpl( connectionId: Option[UUID], ): URIO[WalletAccessContext, IssueCredentialRecord] = { for { - _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaId) + _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaIds) attributes <- CredentialService.convertJsonClaimsToAttributes(claims) offer <- createDidCommOfferCredential( pairwiseIssuerDID = pairwiseIssuerDID, pairwiseHolderDID = pairwiseHolderDID, - maybeSchemaId = maybeSchemaId, + maybeSchemaIds = maybeSchemaIds, claims = attributes, thid = thid, UUID.randomUUID().toString, @@ -223,7 +223,7 @@ class CredentialServiceImpl( pairwiseIssuerDID = pairwiseIssuerDID, kidIssuer = kidIssuer, thid = thid, - schemaUri = maybeSchemaId, + schemaUris = maybeSchemaIds, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance, issuingDID = Some(issuingDID), @@ -244,7 +244,7 @@ class CredentialServiceImpl( pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: io.circe.Json, validityPeriod: Option[Double] = None, automaticIssuance: Option[Boolean], @@ -255,12 +255,12 @@ class CredentialServiceImpl( connectionId: Option[UUID], ): URIO[WalletAccessContext, IssueCredentialRecord] = { for { - _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaId) + _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaIds) attributes <- CredentialService.convertJsonClaimsToAttributes(claims) offer <- createDidCommOfferCredential( pairwiseIssuerDID = pairwiseIssuerDID, pairwiseHolderDID = pairwiseHolderDID, - maybeSchemaId = maybeSchemaId, + maybeSchemaIds = maybeSchemaIds, claims = attributes, thid = thid, UUID.randomUUID().toString, @@ -271,7 +271,7 @@ class CredentialServiceImpl( pairwiseIssuerDID = pairwiseIssuerDID, kidIssuer = kidIssuer, thid = thid, - schemaUri = maybeSchemaId, + schemaUris = maybeSchemaIds, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance, issuingDID = Some(issuingDID), @@ -320,7 +320,7 @@ class CredentialServiceImpl( pairwiseIssuerDID = pairwiseIssuerDID, kidIssuer = None, thid = thid, - schemaUri = Some(credentialDefinition.schemaId), + schemaUris = Some(List(credentialDefinition.schemaId)), validityPeriod = validityPeriod, automaticIssuance = automaticIssuance, issuingDID = None, @@ -375,7 +375,7 @@ class CredentialServiceImpl( createdAt = Instant.now, updatedAt = None, thid = DidCommID(offer.thid.getOrElse(offer.id)), - schemaUri = None, + schemaUris = None, credentialDefinitionId = None, credentialDefinitionUri = None, credentialFormat = credentialFormat, @@ -438,12 +438,19 @@ class CredentialServiceImpl( private[this] def validateClaimsAgainstSchemaIfAny( claims: Json, - maybeSchemaId: Option[String] - ): UIO[Unit] = maybeSchemaId match - case Some(schemaId) => - CredentialSchema - .validateJWTCredentialSubject(schemaId, claims.noSpaces, uriDereferencer) - .orDieAsUnmanagedFailure + maybeSchemaIds: Option[List[String]] + ): UIO[Unit] = maybeSchemaIds match + case Some(schemaIds) => + for { + _ <- ZIO + .collectAll( + schemaIds.map(schemaId => + CredentialSchema + .validateJWTCredentialSubject(schemaId, claims.noSpaces, uriDereferencer) + ) + ) + .orDieAsUnmanagedFailure + } yield ZIO.unit case None => ZIO.unit @@ -806,7 +813,7 @@ class CredentialServiceImpl( processedIssuedCredential, record, attachment, - Some(processedCredential.getSchemaId), + Some(List(processedCredential.getSchemaId)), Some(processedCredential.getCredDefId) ) } yield result @@ -822,7 +829,7 @@ class CredentialServiceImpl( issueCredential: IssueCredential, record: IssueCredentialRecord, attachment: AttachmentDescriptor, - schemaId: Option[String], + schemaId: Option[List[String]], credDefId: Option[String] ) = { credentialRepository @@ -957,7 +964,7 @@ class CredentialServiceImpl( private def createDidCommOfferCredential( pairwiseIssuerDID: DidId, pairwiseHolderDID: Option[DidId], - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: Seq[Attribute], thid: DidCommID, challenge: String, @@ -965,7 +972,7 @@ class CredentialServiceImpl( offerFormat: IssueCredentialOfferFormat ): UIO[OfferCredential] = { for { - credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = maybeSchemaId, attributes = claims)) + credentialPreview <- ZIO.succeed(CredentialPreview(schema_ids = maybeSchemaIds, attributes = claims)) body = OfferCredential.Body( goal_code = Some("Offer Credential"), credential_preview = credentialPreview, @@ -1001,7 +1008,7 @@ class CredentialServiceImpl( thid: DidCommID ): URIO[WalletAccessContext, OfferCredential] = { for { - credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = Some(schemaUri), attributes = claims)) + credentialPreview <- ZIO.succeed(CredentialPreview(schema_ids = Some(List(schemaUri)), attributes = claims)) body = OfferCredential.Body( goal_code = Some("Offer Credential"), credential_preview = credentialPreview, @@ -1143,8 +1150,8 @@ class CredentialServiceImpl( issuer = CredentialIssuer(jwtIssuer.did.toString, `type` = "Profile"), issuanceDate = issuanceDate, maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)), - maybeCredentialSchema = record.schemaUri.map(id => - Left(org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE)) + maybeCredentialSchema = record.schemaUris.map(ids => + ids.map(id => org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE)) ), maybeCredentialStatus = Some(credentialStatus), credentialSubject = claims.add("id", jwtPresentation.iss.asJson).asJson, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala index 5046688d45..500fdf4c29 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala @@ -28,7 +28,7 @@ class CredentialServiceNotifier( pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: Json, validityPeriod: Option[Double], automaticIssuance: Option[Boolean], @@ -44,7 +44,7 @@ class CredentialServiceNotifier( pairwiseHolderDID, kidIssuer, thid, - maybeSchemaId, + maybeSchemaIds, claims, validityPeriod, automaticIssuance, @@ -61,7 +61,7 @@ class CredentialServiceNotifier( pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: io.circe.Json, validityPeriod: Option[Double] = None, automaticIssuance: Option[Boolean], @@ -77,7 +77,7 @@ class CredentialServiceNotifier( pairwiseHolderDID, kidIssuer, thid, - maybeSchemaId, + maybeSchemaIds, claims, validityPeriod, automaticIssuance, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala index 67d68b928c..55c41fe35a 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala @@ -239,7 +239,7 @@ private class PresentationServiceImpl( ) presentationPayload <- createAnoncredPresentationPayloadFromCredential( issuedCredentials, - issuedValidCredentials.flatMap(_.schemaUri), + issuedValidCredentials.flatMap(_.schemaUris.getOrElse(List())), issuedValidCredentials.flatMap(_.credentialDefinitionUri), requestPresentation, anoncredCredentialProof.credentialProofs diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala index 7ad3c9588d..5cb0fbc27a 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala @@ -2,7 +2,14 @@ package org.hyperledger.identus.pollux.core.service.verification import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.service.URIDereferencer -import org.hyperledger.identus.pollux.vc.jwt.{CredentialPayload, DidResolver, JWT, JWTVerification, JwtCredential} +import org.hyperledger.identus.pollux.vc.jwt.{ + CredentialPayload, + CredentialSchema as JwtCredentialSchema, + DidResolver, + JWT, + JWTVerification, + JwtCredential +} import org.hyperledger.identus.pollux.vc.jwt.CredentialPayload.Implicits import zio.* @@ -54,7 +61,10 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe ZIO .fromOption(decodedJwt.maybeCredentialSchema) .mapError(error => VcVerificationServiceError.UnexpectedError(s"Missing Credential Schema: $error")) - credentialSchemas = credentialSchema.fold(List(_), identity) + credentialSchemas = credentialSchema match { + case schema: JwtCredentialSchema => List(schema) + case schemaList: List[JwtCredentialSchema] => schemaList + } result <- ZIO.collectAll( credentialSchemas.map(credentialSchema => @@ -98,7 +108,10 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe ZIO .fromOption(decodedJwt.maybeCredentialSchema) .mapError(error => VcVerificationServiceError.UnexpectedError(s"Missing Credential Schema: $error")) - credentialSchemas = credentialSchema.fold(List(_), identity) + credentialSchemas = credentialSchema match { + case schema: JwtCredentialSchema => List(schema) + case schemaList: List[JwtCredentialSchema] => schemaList + } result <- ZIO.collectAll( credentialSchemas.map(credentialSchema => diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala index aa7d5390f2..d59c0cd30d 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala @@ -104,7 +104,7 @@ class CredentialRepositoryInMemory( recordId: DidCommID, issue: IssueCredential, issuedRawCredential: String, - schemaUri: Option[String], + schemaUris: Option[List[String]], credentialDefinitionUri: Option[String], protocolState: ProtocolState ): URIO[WalletAccessContext, Unit] = { @@ -117,7 +117,7 @@ class CredentialRepositoryInMemory( recordId, record.copy( updatedAt = Some(Instant.now), - schemaUri = schemaUri, + schemaUris = schemaUris, credentialDefinitionUri = credentialDefinitionUri, issueCredentialData = Some(issue), issuedCredentialRaw = Some(issuedRawCredential), @@ -155,7 +155,7 @@ class CredentialRepositoryInMemory( recordId.contains( rec.id ) && rec.issueCredentialData.isDefined - && rec.schemaUri.isDefined + && rec.schemaUris.isDefined && rec.credentialDefinitionUri.isDefined && rec.credentialFormat == CredentialFormat.AnonCreds ) @@ -164,7 +164,7 @@ class CredentialRepositoryInMemory( rec.id, rec.issueCredentialData, rec.credentialFormat, - rec.schemaUri, + rec.schemaUris, rec.credentialDefinitionUri, rec.subjectId, rec.keyId, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositorySpecSuite.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositorySpecSuite.scala index fc61ba2f73..c598c4c876 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositorySpecSuite.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositorySpecSuite.scala @@ -21,7 +21,7 @@ object CredentialRepositorySpecSuite { createdAt = Instant.now, updatedAt = None, thid = DidCommID(), - schemaUri = None, + schemaUris = None, credentialDefinitionId = None, credentialDefinitionUri = None, credentialFormat = credentialFormat, @@ -373,7 +373,7 @@ object CredentialRepositorySpecSuite { aRecord.id, issueCredential, "RAW_CREDENTIAL_DATA", - Some("schemaUri"), + Some(List("schemaUri")), Some("credentialDefinitionUri"), ProtocolState.CredentialReceived ) @@ -383,7 +383,7 @@ object CredentialRepositorySpecSuite { assertTrue(updatedRecord.get.issueCredentialData.contains(issueCredential)) && assertTrue(updatedRecord.get.issuedCredentialRaw.contains("RAW_CREDENTIAL_DATA")) assertTrue(updatedRecord.get.credentialDefinitionUri.contains("credentialDefinitionUri")) - assertTrue(updatedRecord.get.schemaUri.contains("schemaUri")) + assertTrue(updatedRecord.get.schemaUris.getOrElse(List.empty).contains("schemaUri")) } }, test("updateFail (fail one retry) updates record") { diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala index 32711c1445..5e35878bb4 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala @@ -75,14 +75,14 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS thid = thid, pairwiseIssuerDID = pairwiseIssuerDid, pairwiseHolderDID = pairwiseHolderDid, - maybeSchemaId = None, + maybeSchemaIds = None, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance ) } yield { assertTrue(record.thid == thid) && assertTrue(record.updatedAt.isEmpty) && - assertTrue(record.schemaUri.isEmpty) && + assertTrue(record.schemaUris.getOrElse(List.empty).isEmpty) && assertTrue(record.validityPeriod == validityPeriod) && assertTrue(record.automaticIssuance == automaticIssuance) && assertTrue(record.role == Role.Issuer) && @@ -148,7 +148,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS thid = thid, pairwiseIssuerDID = pairwiseIssuerDid, pairwiseHolderDID = pairwiseHolderDid, - maybeSchemaId = Some("resource:///vc-schema-example.json"), + maybeSchemaIds = Some(List("resource:///vc-schema-example.json")), claims = claims, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance @@ -158,7 +158,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS assertTrue(record.thid == thid) && assertTrue(record.updatedAt.isEmpty) && assertTrue( - record.schemaUri.contains("resource:///vc-schema-example.json") + record.schemaUris.getOrElse(List.empty).contains("resource:///vc-schema-example.json") ) && assertTrue(record.validityPeriod == validityPeriod) && assertTrue(record.automaticIssuance == automaticIssuance) && @@ -208,7 +208,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS thid = thid, pairwiseIssuerDID = pairwiseIssuerDid, pairwiseHolderDID = pairwiseHolderDid, - maybeSchemaId = Some("resource:///vc-schema-example.json"), + maybeSchemaIds = Some(List("resource:///vc-schema-example.json")), claims = claims, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance @@ -287,7 +287,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS } yield { assertTrue(holderRecord.thid.toString == offer.thid.get) && assertTrue(holderRecord.updatedAt.isEmpty) && - assertTrue(holderRecord.schemaUri.isEmpty) && + assertTrue(holderRecord.schemaUris.getOrElse(List.empty).isEmpty) && assertTrue(holderRecord.validityPeriod.isEmpty) && assertTrue(holderRecord.automaticIssuance.isEmpty) && assertTrue(holderRecord.role == Role.Holder) && diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala index 4f61c5e123..ed0641edb4 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala @@ -107,7 +107,7 @@ trait CredentialServiceSpecHelper { pairwiseIssuerDID: DidId = DidId("did:prism:issuer"), pairwiseHolderDID: Option[DidId] = Some(DidId("did:prism:holder-pairwise")), thid: DidCommID = DidCommID(), - maybeSchemaId: Option[String] = None, + maybeSchemaIds: Option[List[String]] = None, claims: Json = io.circe.parser .parse(""" |{ @@ -130,7 +130,7 @@ trait CredentialServiceSpecHelper { pairwiseHolderDID = pairwiseHolderDID, kidIssuer = None, thid = thid, - maybeSchemaId = maybeSchemaId, + maybeSchemaIds = maybeSchemaIds, claims = claims, validityPeriod = validityPeriod, automaticIssuance = automaticIssuance, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala index 41b7b472bc..55a4e82d55 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala @@ -22,7 +22,7 @@ object MockCredentialService extends Mock[CredentialService] { DidId, Option[DidId], DidCommID, - Option[String], + Option[List[String]], Json, Option[Double], Option[Boolean], @@ -41,7 +41,7 @@ object MockCredentialService extends Mock[CredentialService] { DidId, Option[DidId], DidCommID, - Option[String], + Option[List[String]], Json, Option[Double], Option[Boolean], @@ -130,7 +130,7 @@ object MockCredentialService extends Mock[CredentialService] { pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: Json, validityPeriod: Option[Double], automaticIssuance: Option[Boolean], @@ -145,7 +145,7 @@ object MockCredentialService extends Mock[CredentialService] { pairwiseIssuerDID, pairwiseHolderDID, thid, - maybeSchemaId, + maybeSchemaIds, claims, validityPeriod, automaticIssuance, @@ -161,7 +161,7 @@ object MockCredentialService extends Mock[CredentialService] { pairwiseHolderDID: Option[DidId], kidIssuer: Option[KeyId], thid: DidCommID, - maybeSchemaId: Option[String], + maybeSchemaIds: Option[List[String]], claims: Json, validityPeriod: Option[Double], automaticIssuance: Option[Boolean], @@ -176,7 +176,7 @@ object MockCredentialService extends Mock[CredentialService] { pairwiseIssuerDID, pairwiseHolderDID, thid, - maybeSchemaId, + maybeSchemaIds, claims, validityPeriod, automaticIssuance, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala index b1feb03cd2..981a187601 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala @@ -521,7 +521,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp aIssueCredentialRecord.id, issueCredential, rawCredentialData, - Some("SchemaId"), + Some(List("SchemaId")), Some("CredDefId"), IssueCredentialRecord.ProtocolState.CredentialReceived ) @@ -865,7 +865,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp createdAt = Instant.now, updatedAt = None, thid = DidCommID(), - schemaUri = Some(schemaId), + schemaUris = Some(List(schemaId)), credentialDefinitionId = Some(credentialDefinitionDb.guid), credentialDefinitionUri = Some(credentialDefinitionId), credentialFormat = CredentialFormat.AnonCreds, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala index 8e08c6445f..b92b00d6d4 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala @@ -133,7 +133,7 @@ trait PresentationServiceSpecHelper { createdAt = Instant.now, updatedAt = None, thid = DidCommID(), - schemaUri = None, + schemaUris = None, credentialDefinitionId = None, credentialDefinitionUri = None, credentialFormat = credentialFormat, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala index e7e5c01f35..3b458cc6df 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala @@ -33,11 +33,9 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -100,11 +98,9 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -167,11 +163,9 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -234,11 +228,9 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -308,11 +300,9 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -379,11 +369,9 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "resource:///vc-schema-personal.json", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "resource:///vc-schema-personal.json", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -450,16 +438,14 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Right( - List( - CredentialSchema( - id = "resource:///vc-schema-personal.json", - `type` = "JsonSchemaValidator2018" - ), - CredentialSchema( - id = "resource:///vc-schema-driver-license.json", - `type` = "JsonSchemaValidator2018" - ) + List( + CredentialSchema( + id = "resource:///vc-schema-personal.json", + `type` = "JsonSchemaValidator2018" + ), + CredentialSchema( + id = "resource:///vc-schema-driver-license.json", + `type` = "JsonSchemaValidator2018" ) ) ), @@ -530,16 +516,14 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Right( - List( - CredentialSchema( - id = "resource:///vc-schema-personal.json", - `type` = "JsonSchemaValidator2018" - ), - CredentialSchema( - id = "resource:///vc-schema-driver-license.json", - `type` = "JsonSchemaValidator2018" - ) + List( + CredentialSchema( + id = "resource:///vc-schema-personal.json", + `type` = "JsonSchemaValidator2018" + ), + CredentialSchema( + id = "resource:///vc-schema-driver-license.json", + `type` = "JsonSchemaValidator2018" ) ) ), @@ -611,11 +595,9 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -679,11 +661,9 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -747,11 +727,9 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( @@ -815,11 +793,9 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - Left( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" - ) + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" ) ), credentialSubject = Json.obj( diff --git a/pollux/sql-doobie/src/main/resources/sql/pollux/V28__support_multiple_credential_schema.sql b/pollux/sql-doobie/src/main/resources/sql/pollux/V28__support_multiple_credential_schema.sql new file mode 100644 index 0000000000..5dd92dedb2 --- /dev/null +++ b/pollux/sql-doobie/src/main/resources/sql/pollux/V28__support_multiple_credential_schema.sql @@ -0,0 +1,9 @@ +ALTER TABLE public.issue_credential_records + ADD COLUMN schema_uris VARCHAR(500)[]; + +UPDATE public.issue_credential_records +SET schema_uris = ARRAY[schema_uri] +WHERE schema_uri IS NOT NULL; + +ALTER TABLE public.issue_credential_records + DROP COLUMN schema_uri; \ No newline at end of file diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialRepository.scala index 339e2946ad..151ebd9e3f 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialRepository.scala @@ -72,7 +72,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_uri, + | schema_uris, | credential_definition_id, | credential_definition_uri, | credential_format, @@ -98,7 +98,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | ${record.createdAt}, | ${record.updatedAt}, | ${record.thid}, - | ${record.schemaUri}, + | ${record.schemaUris}, | ${record.credentialDefinitionId}, | ${record.credentialDefinitionUri}, | ${record.credentialFormat}, @@ -142,7 +142,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_uri, + | schema_uris, | credential_definition_id, | credential_definition_uri, | credential_format, @@ -214,7 +214,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_uri, + | schema_uris, | credential_definition_id, | credential_definition_uri, | credential_format, @@ -278,7 +278,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_uri, + | schema_uris, | credential_definition_id, | credential_definition_uri, | credential_format, @@ -323,7 +323,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_uri, + | schema_uris, | credential_definition_id, | credential_definition_uri, | credential_format, @@ -502,13 +502,13 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | id, | issue_credential_data, | credential_format, - | schema_uri, + | schema_uris, | credential_definition_uri, | subject_id | FROM public.issue_credential_records | WHERE 1=1 | AND issue_credential_data IS NOT NULL - | AND schema_uri IS NOT NULL + | AND schema_uris IS NOT NULL | AND credential_definition_uri IS NOT NULL | AND credential_format = 'AnonCreds' | AND $inClauseFragment @@ -538,14 +538,14 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ recordId: DidCommID, issue: IssueCredential, issuedRawCredential: String, - schemaUri: Option[String], + schemaUris: Option[List[String]], credentialDefinitionUri: Option[String], protocolState: ProtocolState ): URIO[WalletAccessContext, Unit] = { val cxnIO = sql""" | UPDATE public.issue_credential_records | SET - | schema_uri = $schemaUri, + | schema_uris = $schemaUris, | credential_definition_uri = $credentialDefinitionUri, | issue_credential_data = $issue, | issued_credential_raw = $issuedRawCredential, diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala index 4cb35fb17f..6fb3c7387a 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala @@ -87,7 +87,7 @@ sealed trait CredentialPayload { def maybeTermsOfUse: Option[Json] - def maybeCredentialSchema: Option[Either[CredentialSchema, List[CredentialSchema]]] + def maybeCredentialSchema: Option[CredentialSchema | List[CredentialSchema]] def credentialSubject: Json @@ -140,7 +140,7 @@ sealed trait CredentialPayload { case class JwtVc( `@context`: Set[String], `type`: Set[String], - maybeCredentialSchema: Option[Either[CredentialSchema, List[CredentialSchema]]], + maybeCredentialSchema: Option[CredentialSchema | List[CredentialSchema]], credentialSubject: Json, maybeValidFrom: Option[Instant], maybeValidUntil: Option[Instant], @@ -180,7 +180,7 @@ case class W3cCredentialPayload( issuer: String | CredentialIssuer, issuanceDate: Instant, maybeExpirationDate: Option[Instant], - override val maybeCredentialSchema: Option[Either[CredentialSchema, List[CredentialSchema]]], + override val maybeCredentialSchema: Option[CredentialSchema | List[CredentialSchema]], override val credentialSubject: Json, override val maybeCredentialStatus: Option[CredentialStatus], override val maybeRefreshService: Option[RefreshService], @@ -244,9 +244,9 @@ object CredentialPayload { case credentialIssuer: CredentialIssuer => Encoder[CredentialIssuer].apply(credentialIssuer) } - implicit val eitherCredentialSchemaOrListEncoder: Encoder[Either[CredentialSchema, List[CredentialSchema]]] = { - case Left(credentialSchema) => credentialSchema.asJson - case Right(credentialSchemas) => credentialSchemas.asJson + implicit val credentialSchemaOrListEncoder: Encoder[CredentialSchema | List[CredentialSchema]] = Encoder.instance { + case schema: CredentialSchema => Encoder[CredentialSchema].apply(schema) + case schemaList: List[CredentialSchema] => Encoder[List[CredentialSchema]].apply(schemaList) } implicit val w3cCredentialPayloadEncoder: Encoder[W3cCredentialPayload] = @@ -378,10 +378,10 @@ object CredentialPayload { .map(schema => schema: String | CredentialIssuer) .or(Decoder[CredentialIssuer].map(schema => schema: String | CredentialIssuer)) - implicit val eitherCredentialSchemaOrListDecoder: Decoder[Either[CredentialSchema, List[CredentialSchema]]] = + implicit val credentialSchemaOrListDecoder: Decoder[CredentialSchema | List[CredentialSchema]] = Decoder[CredentialSchema] - .map(Left(_)) - .or(Decoder[List[CredentialSchema]].map(Right(_))) + .map(schema => schema: CredentialSchema | List[CredentialSchema]) + .or(Decoder[List[CredentialSchema]].map(schema => schema: CredentialSchema | List[CredentialSchema])) implicit val w3cCredentialPayloadDecoder: Decoder[W3cCredentialPayload] = (c: HCursor) => @@ -402,7 +402,7 @@ object CredentialPayload { maybeValidUntil <- c.downField("validUntil").as[Option[Instant]] maybeCredentialSchema <- c .downField("credentialSchema") - .as[Option[Either[CredentialSchema, List[CredentialSchema]]]] + .as[Option[CredentialSchema | List[CredentialSchema]]] credentialSubject <- c.downField("credentialSubject").as[Json] maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus]] maybeRefreshService <- c.downField("refreshService").as[Option[RefreshService]] @@ -441,7 +441,7 @@ object CredentialPayload { .orElse(c.downField("type").as[String].map(Set(_))) maybeCredentialSchema <- c .downField("credentialSchema") - .as[Option[Either[CredentialSchema, List[CredentialSchema]]]] + .as[Option[CredentialSchema | List[CredentialSchema]]] credentialSubject <- c.downField("credentialSubject").as[Json] maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus]] maybeRefreshService <- c.downField("refreshService").as[Option[RefreshService]] From fbee0554bf424acf8007c9b7088cdb0654f0d6b2 Mon Sep 17 00:00:00 2001 From: Shota Jolbordi Date: Fri, 27 Sep 2024 15:39:05 +0400 Subject: [PATCH 05/13] feat: Implement prism anoncreds method for schemas and credential definitions (#1385) Signed-off-by: Shota Jolbordi --- .sbtopts | 2 + .../client/kotlin/.openapi-generator-ignore | 12 +- .../identus/client/models/Service.kt | 49 ++-- .../models/UpdateManagedDIDServiceAction.kt | 15 +- .../client/typescript/models/Service.ts | 16 +- .../models/UpdateManagedDIDServiceAction.ts | 16 +- .../src/main/resources/application.conf | 1 + .../identus/agent/server/CloudAgentApp.scala | 5 +- .../agent/server/ControllerHelper.scala | 2 +- .../identus/agent/server/MainApp.scala | 27 ++- .../agent/server/config/AppConfig.scala | 4 +- .../server/jobs/PresentBackgroundJobs.scala | 28 +-- .../castor/controller/http/Service.scala | 13 +- .../http/StatusListCredential.scala | 10 +- .../controller/IssueControllerImpl.scala | 229 ++++++++++-------- .../CreateIssueCredentialRecordRequest.scala | 9 +- .../service/OIDCCredentialIssuerService.scala | 10 +- .../pollux/PrismEnvelopeResponse.scala | 42 ++++ ...redentialDefinitionRegistryEndpoints.scala | 142 ++++++++++- ...ialDefinitionRegistryServerEndpoints.scala | 102 ++++++-- .../CredentialDefinitionController.scala | 25 +- .../CredentialDefinitionControllerImpl.scala | 105 ++++++-- .../CredentialDefinitionControllerLogic.scala | 37 ++- .../CredentialDefinitionDidUrlResponse.scala | 69 ++++++ ...edentialDefinitionDidUrlResponsePage.scala | 93 +++++++ .../http/FilterInput.scala | 4 +- .../SchemaRegistryEndpoints.scala | 200 +++++++++++++-- .../SchemaRegistryServerEndpoints.scala | 127 +++++++--- .../CredentialSchemaController.scala | 31 ++- .../CredentialSchemaControllerImpl.scala | 126 ++++++++-- .../CredentialSchemaControllerLogic.scala | 36 ++- .../http/CredentialSchemaDidUrlResponse.scala | 68 ++++++ .../CredentialSchemaDidUrlResponsePage.scala | 91 +++++++ .../http/CredentialSchemaResponse.scala | 17 +- .../http/CredentialSchemaResponsePage.scala | 3 +- .../credentialschema/http/FilterInput.scala | 6 +- .../identus/api/util/Tapir2StaticOAS.scala | 11 +- .../controller/IssueControllerImplSpec.scala | 17 +- .../controller/IssueControllerTestTools.scala | 2 +- .../OIDCCredentialIssuerServiceSpec.scala | 3 +- .../CredentialDefinitionBasicSpec.scala | 4 +- .../CredentialDefinitionFailureSpec.scala | 4 +- ...ialDefinitionLookupAndPaginationSpec.scala | 10 +- .../CredentialDefinitionTestTools.scala | 38 +-- .../schema/CredentialSchemaAnoncredSpec.scala | 11 +- .../schema/CredentialSchemaBasicSpec.scala | 4 +- .../schema/CredentialSchemaFailureSpec.scala | 4 +- ...dentialSchemaLookupAndPaginationSpec.scala | 9 +- .../CredentialSchemaMultiTenancySpec.scala | 6 +- .../schema/CredentialSchemaTestTools.scala | 37 ++- .../VcVerificationControllerTestTools.scala | 3 +- .../walletapi/service/ManagedDIDService.scala | 2 + .../service/ManagedDIDServiceImpl.scala | 22 +- ...dDIDServiceWithEventNotificationImpl.scala | 9 +- .../util/ManagedDIDTemplateValidator.scala | 18 +- .../service/ManagedDIDServiceSpec.scala | 61 +++-- docs/docusaurus/schemas/credential-schema.md | 6 +- .../core/model/ResourceResolutionMethod.scala | 17 ++ .../error/CredentialSchemaServiceError.scala | 6 + .../core/model/error/PresentationError.scala | 18 +- .../model/schema/CredentialDefinition.scala | 21 +- .../core/model/schema/CredentialSchema.scala | 31 +-- .../CredentialDefinitionRepository.scala | 3 +- .../CredentialSchemaRepository.scala | 9 +- .../CredentialStatusListRepository.scala | 2 +- .../service/CredentialDefinitionService.scala | 13 +- .../CredentialDefinitionServiceImpl.scala | 39 ++- .../service/CredentialSchemaService.scala | 19 +- .../service/CredentialSchemaServiceImpl.scala | 62 +++-- .../core/service/CredentialService.scala | 2 +- .../core/service/CredentialServiceImpl.scala | 36 ++- .../core/service/GenericUriResolverImpl.scala | 30 +++ .../service/HttpURIDereferencerImpl.scala | 41 ---- .../OID4VCIIssuerMetadataService.scala | 7 +- .../service/PresentationServiceImpl.scala | 16 +- .../service/ResourceURIDereferencerImpl.scala | 37 --- .../pollux/core/service/URIDereferencer.scala | 49 ---- .../service/uriResolvers/DidUrlResolver.scala | 132 ++++++++++ .../uriResolvers/HttpUrlResolver.scala | 75 ++++++ .../uriResolvers/ResourceUrlResolver.scala | 57 +++++ .../VcVerificationServiceImpl.scala | 14 +- .../model/schema/CredentialSchemaSpec.scala | 3 + ...edentialDefinitionRepositoryInMemory.scala | 11 +- ...edentialStatusListRepositoryInMemory.scala | 2 + ...redentialDefinitionServiceSpecHelper.scala | 3 +- .../service/CredentialServiceImplSpec.scala | 7 +- .../CredentialServiceNotifierSpec.scala | 2 +- .../service/CredentialServiceSpecHelper.scala | 5 +- .../core/service/MockCredentialService.scala | 4 +- .../PresentationServiceSpecHelper.scala | 10 +- .../uriResolvers/DidUrlResolverSpec.scala | 192 +++++++++++++++ .../VcVerificationServiceImplSpec.scala | 26 +- .../VcVerificationServiceSpecHelper.scala | 9 +- ...n_method_to_schema_and_cred_definition.sql | 10 + .../sql/model/db/CredentialDefinition.scala | 29 ++- .../sql/model/db/CredentialSchema.scala | 46 +++- .../identus/pollux/sql/model/db/package.scala | 38 +++ .../pollux/sql/repository/Implicits.scala | 5 +- .../JdbcCredentialDefinitionRepository.scala | 11 +- .../JdbcCredentialSchemaRepository.scala | 22 +- .../JdbcCredentialStatusListRepository.scala | 2 + .../OID4VCIIssuerMetadataServiceSpec.scala | 3 +- ...edentialDefinitionSqlIntegrationSpec.scala | 22 +- .../CredentialSchemaSqlIntegrationSpec.scala | 26 +- .../identus/pollux/vc/jwt/DidResolver.scala | 2 +- .../vc/jwt/VerifiableCredentialPayload.scala | 6 +- .../vc/jwt/revocation/VCStatusList2021.scala | 2 +- .../identus/shared/http/DataUrlResolver.scala | 17 ++ .../shared/http/GenericUriResolver.scala | 70 ++++-- .../identus/shared/models/PrismEnvelope.scala | 16 ++ tests/integration-tests/build.gradle.kts | 4 +- .../test/kotlin/abilities/ListenToEvents.kt | 42 +++- .../test/kotlin/steps/did/ManageDidSteps.kt | 3 +- .../test/kotlin/steps/did/UpdateDidSteps.kt | 20 +- .../steps/oid4vci/IssueCredentialSteps.kt | 4 +- .../kotlin/steps/oid4vci/ManageIssuerSteps.kt | 6 +- 116 files changed, 2644 insertions(+), 825 deletions(-) create mode 100644 cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/PrismEnvelopeResponse.scala create mode 100644 cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponse.scala create mode 100644 cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponsePage.scala create mode 100644 cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponse.scala create mode 100644 cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponsePage.scala create mode 100644 pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/ResourceResolutionMethod.scala create mode 100644 pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala delete mode 100644 pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/HttpURIDereferencerImpl.scala delete mode 100644 pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/ResourceURIDereferencerImpl.scala delete mode 100644 pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/URIDereferencer.scala create mode 100644 pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala create mode 100644 pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/HttpUrlResolver.scala create mode 100644 pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/ResourceUrlResolver.scala create mode 100644 pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolverSpec.scala create mode 100644 pollux/sql-doobie/src/main/resources/sql/pollux/V29__add_resolution_method_to_schema_and_cred_definition.sql create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/http/DataUrlResolver.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/models/PrismEnvelope.scala diff --git a/.sbtopts b/.sbtopts index 2872000dd8..398fe87e6a 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1,3 @@ -Dquill.macro.log=false +-J-Xmx4G +-J-XX:+UseG1GC diff --git a/cloud-agent/client/kotlin/.openapi-generator-ignore b/cloud-agent/client/kotlin/.openapi-generator-ignore index 2f78a69926..d9ad2d6500 100644 --- a/cloud-agent/client/kotlin/.openapi-generator-ignore +++ b/cloud-agent/client/kotlin/.openapi-generator-ignore @@ -2,7 +2,8 @@ settings.gradle build.gradle docs -# igore broken files +# ignore broken files + src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceAction.kt src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceActionType.kt @@ -15,3 +16,12 @@ src/main/kotlin/org/hyperledger/identus/client/models/CredentialSubject.kt src/main/kotlin/org/hyperledger/identus/client/models/DateTimeParameter.kt src/main/kotlin/org/hyperledger/identus/client/models/DidParameter.kt src/main/kotlin/org/hyperledger/identus/client/models/VcVerificationParameter.kt + +src/test/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceActionTest.kt +src/test/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceActionTypeTest.kt + +src/test/kotlin/org/hyperledger/identus/client/models/ServiceTest.kt +src/test/kotlin/org/hyperledger/identus/client/models/ServiceTypeTest.kt + +src/test/kotlin/org/hyperledger/identus/client/models/StatusPurposeTest.kt +src/test/kotlin/org/hyperledger/identus/client/models/CredentialSubjectTest.kt diff --git a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt index 9245b77e67..a331caee07 100644 --- a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt +++ b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt @@ -10,35 +10,52 @@ "ArrayInDataClass", "EnumEntryName", "RemoveRedundantQualifierName", - "UnusedImport" + "UnusedImport", ) package org.hyperledger.identus.client.models -import org.hyperledger.identus.client.models.Json - +import com.google.gson.* +import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName - -/** - * A service expressed in the DID document. https://www.w3.org/TR/did-core/#services - * - * @param id The id of the service. Requires a URI fragment when use in create / update DID. Returns the full ID (with DID prefix) when resolving DID - * @param type - * @param serviceEndpoint - */ - - -data class Service ( +import java.lang.reflect.Type + +class StringOrStringArrayAdapter : JsonSerializer>, JsonDeserializer> { + + // Deserialize logic: String or Array of Strings to List + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): List { + return when { + json?.isJsonArray == true -> { + context!!.deserialize(json, typeOfT) + } + json?.isJsonPrimitive == true -> { + listOf(json.asString) + } + else -> throw JsonParseException("Unexpected type for field") + } + } + + // Serialize logic: List to String or Array of Strings + override fun serialize(src: List?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { + return when { + src == null -> JsonNull.INSTANCE + src.size == 1 -> JsonPrimitive(src[0]) // If only one string, serialize as a single string + else -> context!!.serialize(src) // Otherwise, serialize as a list + } + } +} + +data class Service( /* The id of the service. Requires a URI fragment when use in create / update DID. Returns the full ID (with DID prefix) when resolving DID */ @SerializedName("id") val id: kotlin.String, @SerializedName("type") + @JsonAdapter(StringOrStringArrayAdapter::class) val type: kotlin.collections.List? = null, @SerializedName("serviceEndpoint") - val serviceEndpoint: Json + val serviceEndpoint: JsonElement? = null, ) - diff --git a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceAction.kt b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceAction.kt index 819766835e..5c8aa24016 100644 --- a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceAction.kt +++ b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceAction.kt @@ -10,25 +10,23 @@ "ArrayInDataClass", "EnumEntryName", "RemoveRedundantQualifierName", - "UnusedImport" + "UnusedImport", ) package org.hyperledger.identus.client.models -import org.hyperledger.identus.client.models.Json - +import com.google.gson.JsonElement import com.google.gson.annotations.SerializedName /** * A patch to existing Service. 'type' and 'serviceEndpoint' cannot both be empty. * * @param id The id of the service to update - * @param type - * @param serviceEndpoint + * @param type + * @param serviceEndpoint */ - -data class UpdateManagedDIDServiceAction ( +data class UpdateManagedDIDServiceAction( /* The id of the service to update */ @SerializedName("id") @@ -38,7 +36,6 @@ data class UpdateManagedDIDServiceAction ( val type: kotlin.collections.List? = null, @SerializedName("serviceEndpoint") - val serviceEndpoint: Json? = null + val serviceEndpoint: JsonElement? = null, ) - diff --git a/cloud-agent/client/typescript/models/Service.ts b/cloud-agent/client/typescript/models/Service.ts index 5446e9d0e9..e8a2e6c8bd 100644 --- a/cloud-agent/client/typescript/models/Service.ts +++ b/cloud-agent/client/typescript/models/Service.ts @@ -1,17 +1,3 @@ -/** - * Prism Agent - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * OpenAPI spec version: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { Json } from '../models/Json'; - /** * A service expressed in the DID document. https://www.w3.org/TR/did-core/#services */ @@ -21,7 +7,7 @@ export class Service { */ 'id': string; 'type': Array; - 'serviceEndpoint': Json; + 'serviceEndpoint': string | Array | object; static readonly discriminator: string | undefined = undefined; diff --git a/cloud-agent/client/typescript/models/UpdateManagedDIDServiceAction.ts b/cloud-agent/client/typescript/models/UpdateManagedDIDServiceAction.ts index 987d8434fd..d7c6c9fbc2 100644 --- a/cloud-agent/client/typescript/models/UpdateManagedDIDServiceAction.ts +++ b/cloud-agent/client/typescript/models/UpdateManagedDIDServiceAction.ts @@ -1,17 +1,3 @@ -/** - * Prism Agent - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * OpenAPI spec version: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { Json } from '../models/Json'; - /** * A patch to existing Service. \'type\' and \'serviceEndpoint\' cannot both be empty. */ @@ -21,7 +7,7 @@ export class UpdateManagedDIDServiceAction { */ 'id': string; 'type'?: Array; - 'serviceEndpoint'?: Json; + 'serviceEndpoint'?: string | Array | object; static readonly discriminator: string | undefined = undefined; diff --git a/cloud-agent/service/server/src/main/resources/application.conf b/cloud-agent/service/server/src/main/resources/application.conf index e1632125f5..13f2a0b4bb 100644 --- a/cloud-agent/service/server/src/main/resources/application.conf +++ b/cloud-agent/service/server/src/main/resources/application.conf @@ -93,6 +93,7 @@ agent { port = 8085 port =${?AGENT_HTTP_PORT} } + serviceName = "agent-base-url" publicEndpointUrl = "https://host.docker.internal:8080/cloud-agent" publicEndpointUrl = ${?REST_SERVICE_URL} } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala index ced294305e..d3af09cf2b 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala @@ -30,6 +30,7 @@ import org.hyperledger.identus.pollux.prex.PresentationExchangeServerEndpoints import org.hyperledger.identus.pollux.vc.jwt.DidResolver as JwtDidResolver import org.hyperledger.identus.presentproof.controller.PresentProofServerEndpoints import org.hyperledger.identus.resolvers.DIDResolver +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.{HexString, WalletAccessContext, WalletAdministrationContext, WalletId} import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import org.hyperledger.identus.system.controller.SystemServerEndpoints @@ -68,8 +69,8 @@ object CloudAgentApp { } yield () private val presentProofExchangeJob: RIO[ - AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & PresentationService & CredentialService & - DIDNonSecretStorage & DIDService & ManagedDIDService, + AppConfig & DidOps & UriResolver & DIDResolver & JwtDidResolver & HttpClient & PresentationService & + CredentialService & DIDNonSecretStorage & DIDService & ManagedDIDService, Unit ] = for { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala index a8d2f0ddad..e02a3522c0 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala @@ -61,7 +61,7 @@ trait ControllerHelper { protected def extractPrismDIDFromString(maybeDid: String): IO[ErrorResponse, PrismDID] = ZIO .fromEither(PrismDID.fromString(maybeDid)) - .mapError(e => ErrorResponse.badRequest(detail = Some(s"Error parsing string as PrismDID: ${e}"))) + .mapError(e => ErrorResponse.badRequest(detail = Some(s"Error parsing string as PrismDID: $e"))) protected def getLongFormPrismDID( did: PrismDID, diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala index 10b4928d25..9f163a0a11 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala @@ -18,6 +18,11 @@ import org.hyperledger.identus.agent.walletapi.sql.{ } import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage import org.hyperledger.identus.castor.controller.{DIDControllerImpl, DIDRegistrarControllerImpl} +import org.hyperledger.identus.castor.core.model.did.{ + Service as DidDocumentService, + ServiceEndpoint as DidDocumentServiceEndpoint, + ServiceType as DidDocumentServiceType +} import org.hyperledger.identus.castor.core.service.DIDServiceImpl import org.hyperledger.identus.castor.core.util.DIDOperationValidator import org.hyperledger.identus.connect.controller.ConnectionControllerImpl @@ -142,6 +147,24 @@ object MainApp extends ZIOAppDefault { |""".stripMargin) .ignore + appConfig <- ZIO.service[AppConfig].provide(SystemModule.configLayer) + // these services are added to any DID document by default when they are created. + defaultDidDocumentServices = Set( + DidDocumentService( + id = appConfig.agent.httpEndpoint.serviceName, + serviceEndpoint = DidDocumentServiceEndpoint + .Single( + DidDocumentServiceEndpoint.UriOrJsonEndpoint + .Uri( + DidDocumentServiceEndpoint.UriValue + .fromString(appConfig.agent.httpEndpoint.publicEndpointUrl.toString) + .toOption + .get // This will fail if URL is invalid, which will prevent app from starting since public endpoint in config is invalid + ) + ), + `type` = DidDocumentServiceType.Single(DidDocumentServiceType.Name.fromStringUnsafe("LinkedResourceV1")) + ) + ) _ <- preMigrations _ <- migrations @@ -178,7 +201,7 @@ object MainApp extends ZIOAppDefault { AppModule.didJwtResolverLayer, DIDOperationValidator.layer(), DIDResolver.layer, - HttpURIDereferencerImpl.layer, + GenericUriResolverImpl.layer, PresentationDefinitionValidatorImpl.layer, // service ConnectionServiceImpl.layer >>> ConnectionServiceNotifier.layer, @@ -188,7 +211,7 @@ object MainApp extends ZIOAppDefault { LinkSecretServiceImpl.layer >>> CredentialServiceImpl.layer >>> CredentialServiceNotifier.layer, DIDServiceImpl.layer, EntityServiceImpl.layer, - ManagedDIDServiceWithEventNotificationImpl.layer, + ZLayer.succeed(defaultDidDocumentServices) >>> ManagedDIDServiceWithEventNotificationImpl.layer, LinkSecretServiceImpl.layer >>> PresentationServiceImpl.layer >>> PresentationServiceNotifier.layer, VerificationPolicyServiceImpl.layer, WalletManagementServiceImpl.layer, diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala index 191a3b02cf..0f75561812 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala @@ -109,7 +109,7 @@ final case class DatabaseConfig( DbConfig( username = if (appUser) appUsername else username, password = if (appUser) appPassword else password, - jdbcUrl = s"jdbc:postgresql://${host}:${port}/${databaseName}", + jdbcUrl = s"jdbc:postgresql://$host:$port/${databaseName}", awaitConnectionThreads = awaitConnectionThreads ) } @@ -187,7 +187,7 @@ final case class AgentConfig( } -final case class HttpEndpointConfig(http: HttpConfig, publicEndpointUrl: java.net.URL) +final case class HttpEndpointConfig(http: HttpConfig, serviceName: String, publicEndpointUrl: java.net.URL) final case class DidCommEndpointConfig(http: HttpConfig, publicEndpointUrl: java.net.URL) diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala index 9c35fa6449..9938b6b50b 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala @@ -48,8 +48,8 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { /*DIDSecretStorageError | PresentationError | CredentialServiceError | BackgroundJobError | TransportError | */ CastorDIDResolutionError | GetManagedDIDError | Failure - private type RESOURCES = COMMON_RESOURCES & CredentialService & JwtDidResolver & DIDService & AppConfig & - MESSAGING_RESOURCES + private type RESOURCES = COMMON_RESOURCES & CredentialService & JwtDidResolver & UriResolver & DIDService & + AppConfig & MESSAGING_RESOURCES private type COMMON_RESOURCES = PresentationService & DIDNonSecretStorage & ManagedDIDService @@ -1035,7 +1035,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { credentialFormat: CredentialFormat, invitation: Option[Invitation] ): ZIO[ - AppConfig & JwtDidResolver & COMMON_RESOURCES & MESSAGING_RESOURCES, + AppConfig & JwtDidResolver & UriResolver & COMMON_RESOURCES & MESSAGING_RESOURCES, Failure, Unit ] = { @@ -1069,7 +1069,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { presentation: Presentation, invitation: Option[Invitation] ): ZIO[ - AppConfig & JwtDidResolver & COMMON_RESOURCES & MESSAGING_RESOURCES, + AppConfig & JwtDidResolver & UriResolver & COMMON_RESOURCES & MESSAGING_RESOURCES, Failure, Unit ] = { @@ -1114,28 +1114,12 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { // https://www.w3.org/TR/vc-data-model/#proofs-signatures-0 // A proof is typically attached to a verifiable presentation for authentication purposes // and to a verifiable credential as a method of assertion. - httpLayer <- ZIO.service[HttpClient] - httpUrlResolver = new UriResolver { - override def resolve(uri: String): IO[GenericUriResolverError, String] = { - val res = HttpClient - .get(uri) - .map(x => x.bodyAsString) - .provideSomeLayer(ZLayer.succeed(httpLayer)) - res.mapError(err => SchemaSpecificResolutionError("http", err)) - } - } - genericUriResolver = GenericUriResolver( - Map( - "data" -> DataUrlResolver(), - "http" -> httpUrlResolver, - "https" -> httpUrlResolver - ) - ) + uriResolver <- ZIO.service[UriResolver] result <- JwtPresentation .verify( JWT(base64Decoded), verificationConfig.toPresentationVerificationOptions() - )(didResolverService, genericUriResolver)(clock) + )(didResolverService, uriResolver)(clock) .mapError(error => PresentationError.PresentationVerificationError(error.mkString)) } yield result diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala index 44a1bfd788..52b566f436 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/castor/controller/http/Service.scala @@ -93,12 +93,13 @@ object ServiceType { case Single(value) => Left(value) case Multiple(values) => Right(values.toArray) } - given decoder: JsonDecoder[ServiceType] = JsonDecoder.string - .orElseEither(JsonDecoder.array[String]) - .map[ServiceType] { - case Left(value) => Single(value) - case Right(values) => Multiple(values.toSeq) - } + + given decoder: JsonDecoder[ServiceType] = JsonDecoder[String] + .map(Single.apply) + .orElse( + JsonDecoder[Seq[String]].map(Multiple.apply) + ) + given schema: Schema[ServiceType] = Schema .schemaForEither(Schema.schemaForString, Schema.schemaForArray[String]) .map[ServiceType] { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala index 7627d75bb1..0206b60827 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala @@ -136,11 +136,9 @@ object StatusListCredential { |""".stripMargin given StatusPurposeCodec: JsonCodec[StatusPurpose] = JsonCodec[StatusPurpose]( - JsonEncoder[String].contramap[StatusPurpose](_.str), - JsonDecoder[String].mapOrFail { - case StatusPurpose.Revocation.str => Right(StatusPurpose.Revocation) - case StatusPurpose.Suspension.str => Right(StatusPurpose.Suspension) - case str => Left(s"no enum value matched for \"$str\"") + JsonEncoder[String].contramap[StatusPurpose](_.toString), + JsonDecoder[String].mapOrFail { input => + StatusPurpose.values.find(_.toString.compareToIgnoreCase(input) == 0).toRight("Unknown StatusPurpose") }, ) @@ -183,7 +181,7 @@ object StatusListCredential { given credentialSubjectSchema: Schema[CredentialSubject] = Schema.derived - given statusPurposeSchema: Schema[StatusPurpose] = Schema.derived + given statusPurposeSchema: Schema[StatusPurpose] = Schema.derivedEnumeration.defaultStringBased given credentialIssuerSchema: Schema[CredentialIssuer] = Schema.derived diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala index 2624e36d11..69cfc710bf 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/IssueControllerImpl.scala @@ -8,21 +8,28 @@ import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext} import org.hyperledger.identus.api.http.model.{CollectionStats, PaginationInput} import org.hyperledger.identus.api.util.PaginationUtils -import org.hyperledger.identus.castor.core.model.did.{PrismDID, VerificationRelationship} +import org.hyperledger.identus.castor.core.model.did.{DIDUrl, PrismDID, VerificationRelationship} import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.connect.core.service.ConnectionService import org.hyperledger.identus.issue.controller.http.* import org.hyperledger.identus.mercury.model.DidId -import org.hyperledger.identus.pollux.core.model.{CredentialFormat, DidCommID} +import org.hyperledger.identus.pollux.core.model.{CredentialFormat, DidCommID, ResourceResolutionMethod} import org.hyperledger.identus.pollux.core.model.CredentialFormat.{AnonCreds, JWT, SDJWT} import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.Role -import org.hyperledger.identus.pollux.core.service.CredentialService +import org.hyperledger.identus.pollux.core.service.{CredentialDefinitionService, CredentialService} +import org.hyperledger.identus.shared.crypto.Sha256Hash +import org.hyperledger.identus.shared.json.Json as JsonUtils import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext} -import zio.{Duration, URLayer, ZIO, ZLayer} +import org.hyperledger.identus.shared.utils.Base64Utils +import zio.* +import zio.json.given +import scala.collection.immutable.ListMap import scala.language.implicitConversions + class IssueControllerImpl( credentialService: CredentialService, + credentialDefinitionService: CredentialDefinitionService, connectionService: ConnectionService, didService: DIDService, managedDIDService: ManagedDIDService, @@ -43,106 +50,133 @@ class IssueControllerImpl( offerContext: OfferContext ): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecord] = { + def getIssuingDidFromRequest(request: CreateIssueCredentialRecordRequest) = extractPrismDIDFromString( + request.issuingDID + ) + for { - jsonClaims <- ZIO + jsonClaims <- ZIO // TODO: Get read of Circe and use zio-json all the way down .fromEither(io.circe.parser.parse(request.claims.toString())) - .mapError(e => ErrorResponse.badRequest(detail = Some(s"Invalid claims JSON: ${e.getMessage}"))) - + .mapError(e => ErrorResponse.badRequest(detail = Some(e.getMessage))) credentialFormat = request.credentialFormat.map(CredentialFormat.valueOf).getOrElse(CredentialFormat.JWT) + outcome <- + credentialFormat match + case JWT => + for { + issuingDID <- getIssuingDidFromRequest(request) + _ <- validatePrismDID(issuingDID, allowUnpublished = true, Role.Issuer) + record <- credentialService + .createJWTIssueCredentialRecord( + pairwiseIssuerDID = offerContext.pairwiseIssuerDID, + pairwiseHolderDID = offerContext.pairwiseHolderDID, + kidIssuer = request.issuingKid, + thid = DidCommID(), + maybeSchemaIds = request.schemaId.map { + case schemaId: String => List(schemaId) + case schemaIds: List[String] => schemaIds + }, + claims = jsonClaims, + validityPeriod = request.validityPeriod, + automaticIssuance = request.automaticIssuance.orElse(Some(true)), + issuingDID = issuingDID.asCanonical, + goalCode = offerContext.goalCode, + goal = offerContext.goal, + expirationDuration = offerContext.expirationDuration, + connectionId = request.connectionId + ) + } yield record + case SDJWT => + for { + issuingDID <- getIssuingDidFromRequest(request) + _ <- validatePrismDID(issuingDID, allowUnpublished = true, Role.Issuer) + record <- credentialService + .createSDJWTIssueCredentialRecord( + pairwiseIssuerDID = offerContext.pairwiseIssuerDID, + pairwiseHolderDID = offerContext.pairwiseHolderDID, + kidIssuer = request.issuingKid, + thid = DidCommID(), + maybeSchemaIds = request.schemaId.map { + case schemaId: String => List(schemaId) + case schemaIds: List[String] => schemaIds + }, + claims = jsonClaims, + validityPeriod = request.validityPeriod, + automaticIssuance = request.automaticIssuance.orElse(Some(true)), + issuingDID = issuingDID.asCanonical, + goalCode = offerContext.goalCode, + goal = offerContext.goal, + expirationDuration = offerContext.expirationDuration, + connectionId = request.connectionId + ) + } yield record + case AnonCreds => + for { + issuingDID <- getIssuingDidFromRequest(request) + credentialDefinitionGUID <- ZIO + .fromOption(request.credentialDefinitionId) + .mapError(_ => + ErrorResponse.badRequest(detail = Some("Missing request parameter: credentialDefinitionId")) + ) + credentialDefinition <- credentialDefinitionService.getByGUID(credentialDefinitionGUID) + credentialDefinitionId <- { - outcome <- credentialFormat match { - case JWT => - for { - issuingDID <- ZIO - .fromOption(request.issuingDID) - .mapError(_ => ErrorResponse.badRequest(detail = Some("Missing request parameter: issuingDID"))) - .flatMap(extractPrismDIDFromString) - _ <- validatePrismDID(issuingDID, allowUnpublished = true, Role.Issuer) - record <- credentialService - .createJWTIssueCredentialRecord( - pairwiseIssuerDID = offerContext.pairwiseIssuerDID, - pairwiseHolderDID = offerContext.pairwiseHolderDID, - kidIssuer = request.issuingKid, - thid = DidCommID(), - maybeSchemaIds = request.schemaId.map { - case schemaId: String => List(schemaId) - case schemaIds: List[String] => schemaIds - }, - claims = jsonClaims, - validityPeriod = request.validityPeriod, - automaticIssuance = request.automaticIssuance.orElse(Some(true)), - issuingDID = issuingDID.asCanonical, - goalCode = offerContext.goalCode, - goal = offerContext.goal, - expirationDuration = offerContext.expirationDuration, - connectionId = request.connectionId - ) - } yield record + credentialDefinition.resolutionMethod match + case ResourceResolutionMethod.did => + val publicEndpointServiceName = appConfig.agent.httpEndpoint.serviceName + val didUrlResourcePath = + s"credential-definition-registry/definitions/did-url/${credentialDefinitionGUID.toString}/definition" + val didUrl = for { + canonicalized <- JsonUtils.canonicalizeToJcs(credentialDefinition.definition.toJson) + encoded = Base64Utils.encodeURL(canonicalized.getBytes) + hash = Sha256Hash.compute(encoded.getBytes).hexEncoded + didUrl = DIDUrl( + issuingDID.did, + Seq(), + ListMap( + "resourceService" -> Seq(publicEndpointServiceName), + "resourcePath" -> Seq( + s"$didUrlResourcePath?resourceHash=$hash" + ), + ), + None + ).toString + } yield didUrl - case SDJWT => - for { - issuingDID <- ZIO - .fromOption(request.issuingDID) - .mapError(_ => ErrorResponse.badRequest(detail = Some("Missing request parameter: issuingDID"))) - .flatMap(extractPrismDIDFromString) - _ <- validatePrismDID(issuingDID, allowUnpublished = true, Role.Issuer) - record <- credentialService - .createSDJWTIssueCredentialRecord( - pairwiseIssuerDID = offerContext.pairwiseIssuerDID, - pairwiseHolderDID = offerContext.pairwiseHolderDID, - kidIssuer = request.issuingKid, - thid = DidCommID(), - maybeSchemaIds = request.schemaId.map { - case schemaId: String => List(schemaId) - case schemaIds: List[String] => schemaIds - }, - claims = jsonClaims, - validityPeriod = request.validityPeriod, - automaticIssuance = request.automaticIssuance.orElse(Some(true)), - issuingDID = issuingDID.asCanonical, - goalCode = offerContext.goalCode, - goal = offerContext.goal, - expirationDuration = offerContext.expirationDuration, - connectionId = request.connectionId - ) - } yield record + ZIO + .fromEither(didUrl) + .mapError(_ => ErrorResponse.badRequest(detail = Some("Could not parse credential definition"))) + + case ResourceResolutionMethod.http => + val publicEndpointUrl = appConfig.agent.httpEndpoint.publicEndpointUrl.toExternalForm + val httpUrlSuffix = + s"credential-definition-registry/definitions/${credentialDefinitionGUID.toString}/definition" + val urlPrefix = if (publicEndpointUrl.endsWith("/")) publicEndpointUrl else publicEndpointUrl + "/" + ZIO.succeed(s"$urlPrefix$httpUrlSuffix") + } + record <- credentialService + .createAnonCredsIssueCredentialRecord( + pairwiseIssuerDID = offerContext.pairwiseIssuerDID, + pairwiseHolderDID = offerContext.pairwiseHolderDID, + thid = DidCommID(), + credentialDefinitionGUID = credentialDefinitionGUID, + credentialDefinitionId = credentialDefinitionId, + claims = jsonClaims, + validityPeriod = request.validityPeriod, + automaticIssuance = request.automaticIssuance.orElse(Some(true)), + goalCode = offerContext.goalCode, + goal = offerContext.goal, + expirationDuration = offerContext.expirationDuration, + connectionId = request.connectionId + ) + } yield record - case AnonCreds => - for { - credentialDefinitionGUID <- ZIO - .fromOption(request.credentialDefinitionId) - .mapError(_ => - ErrorResponse.badRequest(detail = Some("Missing request parameter: credentialDefinitionId")) - ) - credentialDefinitionId = { - val publicEndpointUrl = appConfig.agent.httpEndpoint.publicEndpointUrl.toExternalForm - val urlSuffix = - s"credential-definition-registry/definitions/${credentialDefinitionGUID.toString}/definition" - val urlPrefix = if (publicEndpointUrl.endsWith("/")) publicEndpointUrl else publicEndpointUrl + "/" - s"$urlPrefix$urlSuffix" - } - record <- credentialService - .createAnonCredsIssueCredentialRecord( - pairwiseIssuerDID = offerContext.pairwiseIssuerDID, - pairwiseHolderDID = offerContext.pairwiseHolderDID, - thid = DidCommID(), - credentialDefinitionGUID = credentialDefinitionGUID, - credentialDefinitionId = credentialDefinitionId, - claims = jsonClaims, - validityPeriod = request.validityPeriod, - automaticIssuance = request.automaticIssuance.orElse(Some(true)), - goalCode = offerContext.goalCode, - goal = offerContext.goal, - expirationDuration = offerContext.expirationDuration, - connectionId = request.connectionId - ) - } yield record - } } yield IssueCredentialRecord.fromDomain(outcome) } + override def createCredentialOffer( request: CreateIssueCredentialRecordRequest )(implicit rc: RequestContext): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecord] = { + for { connectionId <- ZIO .fromOption(request.connectionId) @@ -174,6 +208,7 @@ class IssueControllerImpl( result <- createCredentialOfferRecord(request, offerContext) } yield result } + def acceptCredentialOfferInvitation( request: AcceptCredentialOfferInvitation )(implicit @@ -315,7 +350,9 @@ class IssueControllerImpl( } object IssueControllerImpl { - val layer - : URLayer[CredentialService & ConnectionService & DIDService & ManagedDIDService & AppConfig, IssueController] = - ZLayer.fromFunction(IssueControllerImpl(_, _, _, _, _)) + val layer: URLayer[ + CredentialService & CredentialDefinitionService & ConnectionService & DIDService & ManagedDIDService & AppConfig, + IssueController + ] = + ZLayer.fromFunction(IssueControllerImpl(_, _, _, _, _, _)) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala index a19ce368b0..5c530b194d 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/issue/controller/http/CreateIssueCredentialRecordRequest.scala @@ -49,7 +49,7 @@ final case class CreateIssueCredentialRecordRequest( automaticIssuance: Option[Boolean] = None, @description(annotations.issuingDID.description) @encodedExample(annotations.issuingDID.example) - issuingDID: Option[String], + issuingDID: String, @description(annotations.issuingKid.description) @encodedExample(annotations.issuingKid.example) issuingKid: Option[KeyId], @@ -130,12 +130,11 @@ object CreateIssueCredentialRecordRequest { ) object issuingDID - extends Annotation[Option[String]]( + extends Annotation[String]( description = """ - |The short-form issuer Prism DID by which the JWT verifiable credential will be issued. - |Note that this parameter only applies when the offer is type 'JWT'. + |The issuer Prism DID by which the verifiable credential will be issued. DID can be short for or long form. |""".stripMargin, - example = Some("did:prism:3bb0505d13fcb04d28a48234edb27b0d4e6d7e18a81e2c1abab58f3bbc21ce6f") + example = "did:prism:3bb0505d13fcb04d28a48234edb27b0d4e6d7e18a81e2c1abab58f3bbc21ce6f" ) object issuingKid diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala index d57bb12493..340dda1622 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala @@ -11,8 +11,7 @@ import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.service.{ CredentialService, OID4VCIIssuerMetadataService, - OID4VCIIssuerMetadataServiceError, - URIDereferencer + OID4VCIIssuerMetadataServiceError } import org.hyperledger.identus.pollux.vc.jwt.{ DidResolver, @@ -23,6 +22,7 @@ import org.hyperledger.identus.pollux.vc.jwt.{ W3cCredentialPayload, * } +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.* import zio.* @@ -111,7 +111,7 @@ case class OIDCCredentialIssuerServiceImpl( issuerMetadataService: OID4VCIIssuerMetadataService, issuanceSessionStorage: IssuanceSessionStorage, didResolver: DidResolver, - uriDereferencer: URIDereferencer, + uriResolver: UriResolver, ) extends OIDCCredentialIssuerService with Openid4VCIProofJwtOps { @@ -256,7 +256,7 @@ case class OIDCCredentialIssuerServiceImpl( } .map(_.schemaId) _ <- CredentialSchema - .validateJWTCredentialSubject(schemaId.toString(), simpleZioToCirce(claims).noSpaces, uriDereferencer) + .validateJWTCredentialSubject(schemaId.toString(), simpleZioToCirce(claims).noSpaces, uriResolver) .mapError(e => CredentialSchemaError(e)) session <- buildNewIssuanceSession(issuerId, issuingDID, claims, schemaId) _ <- issuanceSessionStorage @@ -320,7 +320,7 @@ case class OIDCCredentialIssuerServiceImpl( object OIDCCredentialIssuerServiceImpl { val layer: URLayer[ - DIDNonSecretStorage & CredentialService & IssuanceSessionStorage & DidResolver & URIDereferencer & + DIDNonSecretStorage & CredentialService & IssuanceSessionStorage & DidResolver & UriResolver & OID4VCIIssuerMetadataService, OIDCCredentialIssuerService ] = diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/PrismEnvelopeResponse.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/PrismEnvelopeResponse.scala new file mode 100644 index 0000000000..d32219ef52 --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/PrismEnvelopeResponse.scala @@ -0,0 +1,42 @@ +package org.hyperledger.identus.pollux + +import org.hyperledger.identus.api.http.* +import org.hyperledger.identus.pollux.PrismEnvelopeResponse.annotations +import org.hyperledger.identus.shared.models.PrismEnvelope +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{default, description, encodedExample, encodedName} +import zio.json.* + +case class PrismEnvelopeResponse( + @description(annotations.resource.description) + @encodedExample(annotations.resource.example) + resource: String, + @description(annotations.resource.description) + @encodedExample(annotations.url.example) + url: String +) extends PrismEnvelope + +object PrismEnvelopeResponse { + given encoder: JsonEncoder[PrismEnvelopeResponse] = + DeriveJsonEncoder.gen[PrismEnvelopeResponse] + + given decoder: JsonDecoder[PrismEnvelopeResponse] = + DeriveJsonDecoder.gen[PrismEnvelopeResponse] + + given schema: Schema[PrismEnvelopeResponse] = Schema.derived + + object annotations { + object resource + extends Annotation[String]( + description = "JCS normalized and base64url encoded json of the resource", + example = "" // TODO Add example + ) + + object url + extends Annotation[String]( + description = "DID url that can be used to resolve this resource", + example = + "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a?resourceService=agent-base-url&resourcePath=credential-definition-registry/definitions/did-url/ef3e4135-8fcf-3ce7-b5bb-df37defc13f6?resourceHash=4074bb1a8e0ea45437ad86763cd7e12de3fe8349ef19113df773b0d65c8a9c46" + ) + } +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala index 96592dc7d7..664981ba6f 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala @@ -9,11 +9,13 @@ import org.hyperledger.identus.iam.authentication.apikey.ApiKeyEndpointSecurityL import org.hyperledger.identus.iam.authentication.oidc.JwtCredentials import org.hyperledger.identus.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import org.hyperledger.identus.pollux.credentialdefinition.http.{ + CredentialDefinitionDidUrlResponsePage, CredentialDefinitionInput, CredentialDefinitionResponse, CredentialDefinitionResponsePage, FilterInput } +import org.hyperledger.identus.pollux.PrismEnvelopeResponse import sttp.apispec.{ExternalDocumentation, Tag} import sttp.model.StatusCode import sttp.tapir.{ @@ -52,7 +54,7 @@ object CredentialDefinitionRegistryEndpoints { val tag = Tag(name = tagName, description = Option(tagDescription), externalDocs = Option(tagExternalDocumentation)) - val createCredentialDefinitionEndpoint: Endpoint[ + val createCredentialDefinitionHttpUrlEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), (RequestContext, CredentialDefinitionInput), ErrorResponse, @@ -79,15 +81,52 @@ object CredentialDefinitionRegistryEndpoints { .out(jsonBody[http.CredentialDefinitionResponse]) .description("Credential definition record") .errorOut(basicFailureAndNotFoundAndForbidden) - .name("createCredentialDefinition") - .summary("Publish new definition to the definition registry") + .name("createCredentialDefinitionHttpUrl") + .summary("Publish new definition to the definition registry, resolvable by HTTP url") .description( "Create the new credential definition record with metadata and internal JSON Schema on behalf of Cloud Agent. " + "The credential definition will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it." ) .tag(tagName) - val getCredentialDefinitionByIdEndpoint: PublicEndpoint[ + val createCredentialDefinitionDidUrlEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, CredentialDefinitionInput), + ErrorResponse, + CredentialDefinitionResponse, + Any + ] = + endpoint.post + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in("credential-definition-registry" / "definitions" / "did-url") + .in( + jsonBody[CredentialDefinitionInput] + .description( + "JSON object required for the credential definition creation" + ) + ) + .out( + statusCode(StatusCode.Created) + .description( + "The new credential definition record is successfully created" + ) + ) + .out( + jsonBody[http.CredentialDefinitionResponse] + ) // We use same response as for HTTP url on DID url for definitions + .description("Credential definition record") + .errorOut(basicFailureAndNotFoundAndForbidden) + .name("createCredentialDefinitionDidUrl") + .summary("Publish new definition to the definition registry, resolvable by DID url") + .description( + "Create the new credential definition record with metadata and internal JSON Schema on behalf of the Cloud Agent. " + + "The credential definition will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it." + ) + .tag(tagName) + + val getCredentialDefinitionByIdHttpUrlEndpoint: PublicEndpoint[ (RequestContext, UUID), ErrorResponse, CredentialDefinitionResponse, @@ -102,14 +141,40 @@ object CredentialDefinitionRegistryEndpoints { ) .out(jsonBody[CredentialDefinitionResponse].description("CredentialDefinition found by `guid`")) .errorOut(basicFailuresAndNotFound) - .name("getCredentialDefinitionById") + .name("getCredentialDefinitionByIdHttpUrl") .summary("Fetch the credential definition from the registry by `guid`") .description( "Fetch the credential definition by the unique identifier" ) .tag(tagName) - val getCredentialDefinitionInnerDefinitionByIdEndpoint: PublicEndpoint[ + val getCredentialDefinitionByIdDidUrlEndpoint: PublicEndpoint[ + (RequestContext, UUID), + ErrorResponse, + PrismEnvelopeResponse, + Any + ] = + endpoint.get + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + "credential-definition-registry" / "definitions" / "did-url" / path[UUID]("guid").description( + "Globally unique identifier of the credential definition record" + ) + ) + .out( + jsonBody[PrismEnvelopeResponse].description( + "CredentialDefinition found by `guid`, wrapped in an envelope" + ) + ) + .errorOut(basicFailuresAndNotFound) + .name("getCredentialDefinitionByIdDidUrl") + .summary("Fetch the credential definition from the registry by `guid`, wrapped in an envelope") + .description( + "Fetch the credential definition by the unique identifier, it should have been crated via DID url, otherwise not found error is returned." + ) + .tag(tagName) + + val getCredentialDefinitionInnerDefinitionByIdHttpUrlEndpoint: PublicEndpoint[ (RequestContext, UUID), ErrorResponse, zio.json.ast.Json, @@ -124,16 +189,42 @@ object CredentialDefinitionRegistryEndpoints { ) .out(jsonBody[zio.json.ast.Json].description("CredentialDefinition found by `guid`")) .errorOut(basicFailuresAndNotFound) - .name("getCredentialDefinitionInnerDefinitionById") + .name("getCredentialDefinitionInnerDefinitionByIdHttpUrl") .summary("Fetch the inner definition field of the credential definition from the registry by `guid`") .description( "Fetch the inner definition fields of the credential definition by the unique identifier" ) .tag(tagName) + val getCredentialDefinitionInnerDefinitionByIdDidUrlEndpoint: PublicEndpoint[ + (RequestContext, UUID), + ErrorResponse, + PrismEnvelopeResponse, + Any + ] = + endpoint.get + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + "credential-definition-registry" / "definitions" / "did-url" / path[UUID]("guid") / "definition".description( + "Globally unique identifier of the credential definition record" + ) + ) + .out( + jsonBody[PrismEnvelopeResponse].description("CredentialDefinition found by `guid`") + ) + .errorOut(basicFailuresAndNotFound) + .name("getCredentialDefinitionInnerDefinitionByIdDidUrl") + .summary( + "Fetch the inner definition field of the credential definition from the registry by `guid`, wrapped in an envelope" + ) + .description( + "Fetch the inner definition fields of the credential definition by the unique identifier, it should have been crated via DID url, otherwise not found error is returned." + ) + .tag(tagName) + private val credentialDefinitionFilterInput: EndpointInput[http.FilterInput] = EndpointInput.derived[http.FilterInput] private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] - val lookupCredentialDefinitionsByQueryEndpoint: Endpoint[ + val lookupCredentialDefinitionsByQueryHttpUrlEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), ( RequestContext, @@ -159,10 +250,43 @@ object CredentialDefinitionRegistryEndpoints { .in(query[Option[Order]]("order")) .out(jsonBody[CredentialDefinitionResponsePage].description("Collection of CredentialDefinitions records.")) .errorOut(basicFailures) - .name("lookupCredentialDefinitionsByQuery") + .name("lookupCredentialDefinitionsByQueryHttpUrl") .summary("Lookup credential definitions by indexed fields") .description( "Lookup credential definitions by `author`, `name`, `tag` parameters and control the pagination by `offset` and `limit` parameters " ) .tag(tagName) + + val lookupCredentialDefinitionsByQueryDidUrlEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + ( + RequestContext, + FilterInput, + PaginationInput, + Option[Order] + ), + ErrorResponse, + CredentialDefinitionDidUrlResponsePage, + Any + ] = + endpoint.get + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + "credential-definition-registry" / "definitions" / "did-url".description( + "Lookup credential definitions by query" + ) + ) + .in(credentialDefinitionFilterInput) + .in(paginationInput) + .in(query[Option[Order]]("order")) + .out(jsonBody[CredentialDefinitionDidUrlResponsePage].description("Collection of CredentialDefinitions records.")) + .errorOut(basicFailures) + .name("lookupCredentialDefinitionsByQueryDidUrl") + .summary("Lookup credential definitions by indexed fields") + .description( + "Lookup DID url resolvable credential definitions by `author`, `name`, `tag` parameters and control the pagination by `offset` and `limit` parameters " + ) + .tag(tagName) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala index 6d3c053ecb..5d4e60615a 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.credentialdefinition +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext} import org.hyperledger.identus.api.http.model.{Order, PaginationInput} @@ -15,39 +16,57 @@ import zio.* import java.util.UUID class CredentialDefinitionRegistryServerEndpoints( + config: AppConfig, credentialDefinitionController: CredentialDefinitionController, authenticator: Authenticator[BaseEntity], authorizer: Authorizer[BaseEntity] ) { - val createCredentialDefinitionServerEndpoint: ZServerEndpoint[Any, Any] = - createCredentialDefinitionEndpoint + object create { + val http: ZServerEndpoint[Any, Any] = createCredentialDefinitionHttpUrlEndpoint .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) - .serverLogic { - case wac => { case (ctx: RequestContext, credentialDefinitionInput: CredentialDefinitionInput) => + .serverLogic { wac => + { case (ctx: RequestContext, credentialDefinitionInput: CredentialDefinitionInput) => credentialDefinitionController .createCredentialDefinition(credentialDefinitionInput)(ctx) .provideSomeLayer(ZLayer.succeed(wac)) .logTrace(ctx) } } + val did: ZServerEndpoint[Any, Any] = createCredentialDefinitionDidUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, credentialDefinitionInput: CredentialDefinitionInput) => + credentialDefinitionController + .createCredentialDefinitionDidUrl(credentialDefinitionInput)(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } - val getCredentialDefinitionByIdServerEndpoint: ZServerEndpoint[Any, Any] = - getCredentialDefinitionByIdEndpoint.zServerLogic { case (ctx: RequestContext, guid: UUID) => - credentialDefinitionController - .getCredentialDefinitionByGuid(guid)(ctx) - .logTrace(ctx) - } + val all = List(http, did) + } - val getCredentialDefinitionInnerDefinitionByIdServerEndpoint: ZServerEndpoint[Any, Any] = - getCredentialDefinitionInnerDefinitionByIdEndpoint.zServerLogic { case (ctx: RequestContext, guid: UUID) => - credentialDefinitionController - .getCredentialDefinitionInnerDefinitionByGuid(guid)(ctx) - .logTrace(ctx) + object get { + val http: ZServerEndpoint[Any, Any] = getCredentialDefinitionByIdHttpUrlEndpoint.zServerLogic { + case (ctx: RequestContext, guid: UUID) => + credentialDefinitionController + .getCredentialDefinitionByGuid(guid)(ctx) + .logTrace(ctx) } + val did: ZServerEndpoint[Any, Any] = getCredentialDefinitionByIdDidUrlEndpoint.zServerLogic { + case (ctx: RequestContext, guid: UUID) => + credentialDefinitionController + .getCredentialDefinitionByGuidDidUrl(config.agent.httpEndpoint.serviceName, guid)(ctx) + .logTrace(ctx) + } + + val all = List(http, did) - val lookupCredentialDefinitionsByQueryServerEndpoint: ZServerEndpoint[Any, Any] = - lookupCredentialDefinitionsByQueryEndpoint + } + + object getMany { + val http: ZServerEndpoint[Any, Any] = lookupCredentialDefinitionsByQueryHttpUrlEndpoint .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) .serverLogic { case wac => { @@ -62,22 +81,57 @@ class CredentialDefinitionRegistryServerEndpoints( .logTrace(ctx) } } + val did: ZServerEndpoint[Any, Any] = lookupCredentialDefinitionsByQueryDidUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { + case wac => { + case (ctx: RequestContext, filter: FilterInput, paginationInput: PaginationInput, order: Option[Order]) => + credentialDefinitionController + .lookupCredentialDefinitionsDidUrl( + config.agent.httpEndpoint.serviceName, + filter, + paginationInput.toPagination, + order + )(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } + + val all = List(http, did) + + } + + object getRaw { + val http: ZServerEndpoint[Any, Any] = getCredentialDefinitionInnerDefinitionByIdHttpUrlEndpoint.zServerLogic { + case (ctx: RequestContext, guid: UUID) => + credentialDefinitionController + .getCredentialDefinitionInnerDefinitionByGuid(guid)(ctx) + .logTrace(ctx) + } + val did: ZServerEndpoint[Any, Any] = getCredentialDefinitionInnerDefinitionByIdDidUrlEndpoint.zServerLogic { + case (ctx: RequestContext, guid: UUID) => + credentialDefinitionController + .getCredentialDefinitionInnerDefinitionByGuidDidUrl(config.agent.httpEndpoint.serviceName, guid)(ctx) + .logTrace(ctx) + } + + val all = List(http, did) + + } val all: List[ZServerEndpoint[Any, Any]] = - List( - createCredentialDefinitionServerEndpoint, - getCredentialDefinitionByIdServerEndpoint, - getCredentialDefinitionInnerDefinitionByIdServerEndpoint, - lookupCredentialDefinitionsByQueryServerEndpoint - ) + create.all ++ getMany.all ++ getRaw.all ++ get.all } object CredentialDefinitionRegistryServerEndpoints { - def all: URIO[CredentialDefinitionController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[CredentialDefinitionController & DefaultAuthenticator & AppConfig, List[ZServerEndpoint[Any, Any]]] = { for { credentialDefinitionRegistryService <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[DefaultAuthenticator] + config <- ZIO.service[AppConfig] credentialDefinitionRegistryEndpoints = new CredentialDefinitionRegistryServerEndpoints( + config, credentialDefinitionRegistryService, authenticator, authenticator diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionController.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionController.scala index 3f3b8ffe31..cffaa70a0e 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionController.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionController.scala @@ -3,11 +3,13 @@ package org.hyperledger.identus.pollux.credentialdefinition.controller import org.hyperledger.identus.api.http.* import org.hyperledger.identus.api.http.model.{Order, Pagination} import org.hyperledger.identus.pollux.credentialdefinition.http.{ + CredentialDefinitionDidUrlResponsePage, CredentialDefinitionInput, CredentialDefinitionResponse, CredentialDefinitionResponsePage, FilterInput } +import org.hyperledger.identus.pollux.PrismEnvelopeResponse import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* @@ -19,17 +21,25 @@ trait CredentialDefinitionController { rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponse] - def getCredentialDefinitionByGuid(id: UUID)(implicit + def createCredentialDefinitionDidUrl(in: CredentialDefinitionInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponse] + + def getCredentialDefinitionByGuid(guid: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, CredentialDefinitionResponse] + def getCredentialDefinitionByGuidDidUrl(baseUrlServiceName: String, guid: UUID)(implicit + rc: RequestContext + ): IO[ErrorResponse, PrismEnvelopeResponse] + def getCredentialDefinitionInnerDefinitionByGuid(id: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, zio.json.ast.Json] - def delete(guid: UUID)(implicit + def getCredentialDefinitionInnerDefinitionByGuidDidUrl(baseUrlServiceName: String, guid: UUID)(implicit rc: RequestContext - ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponse] + ): IO[ErrorResponse, PrismEnvelopeResponse] def lookupCredentialDefinitions( filter: FilterInput, @@ -39,4 +49,13 @@ trait CredentialDefinitionController { rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponsePage] + def lookupCredentialDefinitionsDidUrl( + baseUrlServiceName: String, + filter: FilterInput, + pagination: Pagination, + order: Option[Order] + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionDidUrlResponsePage] + } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerImpl.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerImpl.scala index c4039f4d0f..3051efe7b9 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerImpl.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerImpl.scala @@ -1,21 +1,25 @@ package org.hyperledger.identus.pollux.credentialdefinition.controller +import cats.implicits.* import org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, PublicationState} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.api.http.* import org.hyperledger.identus.api.http.model.{CollectionStats, Order, Pagination} import org.hyperledger.identus.castor.core.model.did.{LongFormPrismDID, PrismDID} +import org.hyperledger.identus.pollux.{credentialdefinition, PrismEnvelopeResponse} import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition.FilteredEntries +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.service.CredentialDefinitionService -import org.hyperledger.identus.pollux.credentialdefinition import org.hyperledger.identus.pollux.credentialdefinition.http.{ + CredentialDefinitionDidUrlResponse, + CredentialDefinitionDidUrlResponsePage, + CredentialDefinitionInnerDefinitionDidUrlResponse, CredentialDefinitionInput, CredentialDefinitionResponse, CredentialDefinitionResponsePage, FilterInput } import org.hyperledger.identus.pollux.credentialdefinition.http.CredentialDefinitionInput.toDomain -import org.hyperledger.identus.pollux.credentialdefinition.http.CredentialDefinitionResponse.fromDomain import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* import zio.json.ast.Json @@ -34,7 +38,23 @@ class CredentialDefinitionControllerImpl(service: CredentialDefinitionService, m _ <- validatePrismDID(in.author) result <- service .create(toDomain(in)) - .map(cs => fromDomain(cs).withBaseUri(rc.request.uri)) + .map(cs => CredentialDefinitionResponse.fromDomain(cs).withBaseUri(rc.request.uri)) + } yield result + } + + private def couldNotParseCredDefResponse(e: String) = ErrorResponse + .internalServerError(detail = Some(s"Error occurred while parsing the credential definition response: $e")) + + override def createCredentialDefinitionDidUrl( + in: CredentialDefinitionInput + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponse] = { + for { + _ <- validatePrismDID(in.author) + result <- service + .create(toDomain(in), ResourceResolutionMethod.did) + .map(cs => CredentialDefinitionResponse.fromDomain(cs).withBaseUri(rc.request.uri)) } yield result } @@ -44,28 +64,54 @@ class CredentialDefinitionControllerImpl(service: CredentialDefinitionService, m service .getByGUID(guid) .map( - fromDomain(_) + CredentialDefinitionResponse + .fromDomain(_) .withSelf(rc.request.uri.toString) ) } + override def getCredentialDefinitionByGuidDidUrl( + baseUrlServiceName: String, + guid: UUID + )(implicit rc: RequestContext): IO[ErrorResponse, PrismEnvelopeResponse] = { + + val res = for { + cd <- service.getByGUID(guid, ResourceResolutionMethod.did) + response <- ZIO + .fromEither(CredentialDefinitionDidUrlResponse.asPrismEnvelopeResponse(cd, baseUrlServiceName)) + .mapError(couldNotParseCredDefResponse) + + } yield response + + res + } + override def getCredentialDefinitionInnerDefinitionByGuid(id: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, Json] = { service .getByGUID(id) - .map(fromDomain(_).definition) + .map(CredentialDefinitionResponse.fromDomain(_).definition) } - override def delete(guid: UUID)(implicit + override def getCredentialDefinitionInnerDefinitionByGuidDidUrl(baseUrlServiceName: String, guid: UUID)(implicit rc: RequestContext - ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponse] = { - service - .delete(guid) - .map( - fromDomain(_) - .withBaseUri(rc.request.uri) - ) + ): IO[ErrorResponse, PrismEnvelopeResponse] = { + val res = for { + cd <- service.getByGUID(guid, ResourceResolutionMethod.did) + authorDid <- ZIO + .fromEither(PrismDID.fromString(cd.author)) + .mapError(_ => ErrorResponse.internalServerError(detail = Some("Invalid credential definition author DID"))) + response <- ZIO + .fromEither( + CredentialDefinitionInnerDefinitionDidUrlResponse + .asPrismEnvelopeResponse(cd.definition, authorDid, cd.guid, baseUrlServiceName) + ) + .mapError(couldNotParseCredDefResponse) + + } yield response + + res } override def lookupCredentialDefinitions( @@ -77,16 +123,43 @@ class CredentialDefinitionControllerImpl(service: CredentialDefinitionService, m ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionResponsePage] = { for { filteredEntries: FilteredEntries <- service.lookup( - filter.toDomain, + filter.toDomain(), pagination.offset, pagination.limit ) entries = filteredEntries.entries - .map(fromDomain(_).withBaseUri(rc.request.uri)) + .map(CredentialDefinitionResponse.fromDomain(_).withBaseUri(rc.request.uri)) .toList page = CredentialDefinitionResponsePage(entries) stats = CollectionStats(filteredEntries.totalCount, filteredEntries.count) - } yield CredentialDefinitionControllerLogic(rc, pagination, page, stats).result + } yield CredentialDefinitionControllerLogic(rc, pagination, stats).result(page) + } + + override def lookupCredentialDefinitionsDidUrl( + baseUrlServiceName: String, + filter: FilterInput, + pagination: Pagination, + order: Option[Order] + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialDefinitionDidUrlResponsePage] = { + for { + filteredEntries: FilteredEntries <- service.lookup( + filter.toDomain(ResourceResolutionMethod.did), + pagination.offset, + pagination.limit + ) + + entriesZio = filteredEntries.entries + .traverse(cd => CredentialDefinitionDidUrlResponse.asPrismEnvelopeResponse(cd, baseUrlServiceName)) + + entries <- ZIO + .fromEither(entriesZio) + .mapError(couldNotParseCredDefResponse) + + page = CredentialDefinitionDidUrlResponsePage(entries) + stats = CollectionStats(filteredEntries.totalCount, filteredEntries.count) + } yield CredentialDefinitionControllerLogic(rc, pagination, stats).resultDidUrl(page) } private def validatePrismDID(author: String) = diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerLogic.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerLogic.scala index 9d90e3b5bd..ee446116f8 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerLogic.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/controller/CredentialDefinitionControllerLogic.scala @@ -3,27 +3,24 @@ package org.hyperledger.identus.pollux.credentialdefinition.controller import org.hyperledger.identus.api.http.model.{CollectionStats, Pagination} import org.hyperledger.identus.api.http.RequestContext import org.hyperledger.identus.api.util.PaginationUtils -import org.hyperledger.identus.pollux.credentialdefinition.http.CredentialDefinitionResponsePage +import org.hyperledger.identus.pollux.credentialdefinition.http.{ + CredentialDefinitionDidUrlResponsePage, + CredentialDefinitionResponsePage +} import sttp.model.Uri case class CredentialDefinitionControllerLogic( ctx: RequestContext, pagination: Pagination, - page: CredentialDefinitionResponsePage, stats: CollectionStats ) { - private def composeNextUri(uri: Uri): Option[Uri] = - PaginationUtils.composeNextUri(uri, page.contents, pagination, stats) - - private def composePreviousUri(uri: Uri): Option[Uri] = - PaginationUtils.composePreviousUri(uri, page.contents, pagination, stats) + val self = ctx.request.uri.toString + val pageOf = ctx.request.uri.copy(querySegments = Seq.empty).toString - def result: CredentialDefinitionResponsePage = { - val self = ctx.request.uri.toString - val pageOf = ctx.request.uri.copy(querySegments = Seq.empty).toString - val next = composeNextUri(ctx.request.uri).map(_.toString) - val previous = composePreviousUri(ctx.request.uri).map(_.toString) + def result(page: CredentialDefinitionResponsePage): CredentialDefinitionResponsePage = { + val next = PaginationUtils.composeNextUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + val previous = PaginationUtils.composePreviousUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) val pageResult = page.copy( self = self, @@ -39,4 +36,20 @@ case class CredentialDefinitionControllerLogic( pageResult } + + def resultDidUrl(page: CredentialDefinitionDidUrlResponsePage): CredentialDefinitionDidUrlResponsePage = { + val next = PaginationUtils.composeNextUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + val previous = PaginationUtils.composePreviousUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + + val pageResult = page.copy( + self = self, + pageOf = pageOf, + next = next, + previous = previous, + contents = page.contents + ) + + pageResult + } + } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponse.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponse.scala new file mode 100644 index 0000000000..9b57575dd2 --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponse.scala @@ -0,0 +1,69 @@ +package org.hyperledger.identus.pollux.credentialdefinition.http + +import org.hyperledger.identus.castor.core.model.did.{DIDUrl, PrismDID} +import org.hyperledger.identus.pollux.core.model +import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition +import org.hyperledger.identus.pollux.PrismEnvelopeResponse +import org.hyperledger.identus.shared.crypto.Sha256Hash +import org.hyperledger.identus.shared.json.Json as JsonUtils +import org.hyperledger.identus.shared.utils.Base64Utils +import zio.json.* +import zio.json.ast.Json + +import java.util.UUID +import scala.collection.immutable.ListMap + +object CredentialDefinitionDidUrlResponse { + + def asPrismEnvelopeResponse(cd: CredentialDefinition, serviceName: String): Either[String, PrismEnvelopeResponse] = { + for { + authorDid <- PrismDID.fromString(cd.author) + canonicalized <- JsonUtils.canonicalizeToJcs(cd.toJson).left.map(_.toString) + encoded = Base64Utils.encodeURL(canonicalized.getBytes) + hash = Sha256Hash.compute(encoded.getBytes).hexEncoded + didUrl = DIDUrl( + authorDid.did, + Seq(), + ListMap( + "resourceService" -> Seq(serviceName), + "resourcePath" -> Seq(s"credential-definition-registry/definitions/did-url/${cd.guid}?resourceHash=$hash"), + ), + None + ).toString + } yield PrismEnvelopeResponse( + resource = encoded, + url = didUrl + ) + } +} + +object CredentialDefinitionInnerDefinitionDidUrlResponse { + + def asPrismEnvelopeResponse( + innerDefinition: Json, + authorDid: PrismDID, + definitionGuid: UUID, + serviceName: String + ): Either[String, PrismEnvelopeResponse] = { + for { + canonicalized <- JsonUtils.canonicalizeToJcs(innerDefinition.toJson).left.map(_.toString) + encoded = Base64Utils.encodeURL(canonicalized.getBytes) + hash = Sha256Hash.compute(encoded.getBytes).hexEncoded + didUrl = DIDUrl( + authorDid.did, + Seq(), + ListMap( + "resourceService" -> Seq(serviceName), + "resourcePath" -> Seq( + s"credential-definition-registry/definitions/did-url/$definitionGuid/definition?resourceHash=$hash" + ), + ), + None + ).toString + } yield PrismEnvelopeResponse( + resource = encoded, + url = didUrl + ) + } + +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponsePage.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponsePage.scala new file mode 100644 index 0000000000..bfcb77011e --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/CredentialDefinitionDidUrlResponsePage.scala @@ -0,0 +1,93 @@ +package org.hyperledger.identus.pollux.credentialdefinition.http + +import org.hyperledger.identus.api.http.Annotation +import org.hyperledger.identus.pollux.credentialdefinition.http.CredentialDefinitionResponsePage.annotations +import org.hyperledger.identus.pollux.PrismEnvelopeResponse +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{description, encodedExample} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +case class CredentialDefinitionDidUrlResponsePage( + @description(annotations.contents.description) + @encodedExample(annotations.contents.example) + contents: Seq[PrismEnvelopeResponse], + @description(annotations.kind.description) + @encodedExample(annotations.kind.example) + kind: String = "CredentialDefinitionDidUrlPage", + @description(annotations.self.description) + @encodedExample(annotations.self.example) + self: String = "", + @description(annotations.pageOf.description) + @encodedExample(annotations.pageOf.example) + pageOf: String = "", + @description(annotations.next.description) + @encodedExample(annotations.next.example) + next: Option[String] = None, + @description(annotations.previous.description) + @encodedExample(annotations.previous.example) + previous: Option[String] = None +) { + def withSelf(self: String) = copy(self = self) +} + +object CredentialDefinitionDidUrlResponsePage { + given encoder: JsonEncoder[CredentialDefinitionDidUrlResponsePage] = + DeriveJsonEncoder.gen[CredentialDefinitionDidUrlResponsePage] + + given decoder: JsonDecoder[CredentialDefinitionDidUrlResponsePage] = + DeriveJsonDecoder.gen[CredentialDefinitionDidUrlResponsePage] + + given schema: Schema[CredentialDefinitionDidUrlResponsePage] = Schema.derived + + val Example = CredentialDefinitionDidUrlResponsePage( + contents = annotations.contents.example, + kind = annotations.kind.example, + self = annotations.self.example, + pageOf = annotations.pageOf.example, + next = Some(annotations.next.example), + previous = Some(annotations.previous.example) + ) + + object annotations { + + object contents + extends Annotation[Seq[PrismEnvelopeResponse]]( + description = + "A sequence of PrismEnvelopeResponse objects representing the list of credential definitions that the API response contains", + example = Seq.empty + ) + + object kind + extends Annotation[String]( + description = + "A string field indicating the type of the API response. In this case, it will always be set to `CredentialDefinitionDidUrlPage`", + example = "CredentialDefinitionDidUrlPage" + ) // TODO Tech Debt ticket - the kind in a collection should be collection, not the underlying record type + + object self + extends Annotation[String]( + description = "A string field containing the URL of the current API endpoint", + example = "/cloud-agent/credential-definition-registry/definitions/did-url?skip=10&limit=10" + ) + + object pageOf + extends Annotation[String]( + description = "A string field indicating the type of resource that the contents field contains", + example = "/cloud-agent/credential-definition-registry/definitions/did-url" + ) + + object next + extends Annotation[String]( + description = "An optional string field containing the URL of the next page of results. " + + "If the API response does not contain any more pages, this field should be set to None.", + example = "/cloud-agent/credential-definition-registry/definitions/did-url?skip=20&limit=10" + ) + + object previous + extends Annotation[String]( + description = "An optional string field containing the URL of the previous page of results. " + + "If the API response is the first page of results, this field should be set to None.", + example = "/cloud-agent/credential-definition-registry/definitions/did-url?skip=0&limit=10" + ) + } +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/FilterInput.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/FilterInput.scala index cd39afb2b1..fcda095187 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/FilterInput.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialdefinition/http/FilterInput.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.credentialdefinition.http import org.hyperledger.identus.api.http.* import org.hyperledger.identus.pollux.core.model import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.credentialdefinition.http.FilterInput.annotations import sttp.tapir.EndpointIO.annotations.{example, query} import sttp.tapir.Validator.* @@ -21,7 +22,8 @@ case class FilterInput( @example(Option(annotations.tag.example)) tag: Option[String] = Option.empty[String] ) { - def toDomain = CredentialDefinition.Filter(author, name, version, tag) + def toDomain(resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http) = + CredentialDefinition.Filter(author, name, version, tag, resolutionMethod) } object FilterInput { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryEndpoints.scala index 26e21ea5c9..1fae593271 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryEndpoints.scala @@ -9,24 +9,16 @@ import org.hyperledger.identus.iam.authentication.apikey.ApiKeyEndpointSecurityL import org.hyperledger.identus.iam.authentication.oidc.JwtCredentials import org.hyperledger.identus.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import org.hyperledger.identus.pollux.credentialschema.http.{ + CredentialSchemaDidUrlResponsePage, CredentialSchemaInput, CredentialSchemaResponse, CredentialSchemaResponsePage, FilterInput } +import org.hyperledger.identus.pollux.PrismEnvelopeResponse import sttp.apispec.{ExternalDocumentation, Tag} import sttp.model.StatusCode -import sttp.tapir.{ - endpoint, - extractFromRequest, - path, - query, - statusCode, - stringToPath, - Endpoint, - EndpointInput, - PublicEndpoint -} +import sttp.tapir.* import sttp.tapir.json.zio.{jsonBody, schemaForZioJsonValue} import zio.json.ast.Json @@ -65,8 +57,10 @@ object SchemaRegistryEndpoints { ) val tag = Tag(name = tagName, description = Option(tagDescription), externalDocs = Option(tagExternalDocumentation)) + val httpUrlPathPrefix = "schema-registry" / "schemas" + val didUrlPathPrefix = "schema-registry" / "schemas" / "did-url" - val createSchemaEndpoint: Endpoint[ + val createSchemaHttpUrlEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), (RequestContext, CredentialSchemaInput), ErrorResponse, @@ -77,7 +71,7 @@ object SchemaRegistryEndpoints { .securityIn(apiKeyHeader) .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) - .in("schema-registry" / "schemas") + .in(httpUrlPathPrefix) .in( jsonBody[CredentialSchemaInput] .description( @@ -94,16 +88,51 @@ object SchemaRegistryEndpoints { .description("Credential schema record") .errorOut(basicFailureAndNotFoundAndForbidden) .name("createSchema") - .summary("Publish new schema to the schema registry") + .summary("Publish new schema to the schema registry, http url resolvable") + .description( + "Create the new credential schema record with metadata and internal JSON Schema on behalf of Cloud Agent. " + + "The credential schema will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it." + ) + .tag(tagName) + + val createSchemaDidUrlEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, CredentialSchemaInput), + ErrorResponse, + PrismEnvelopeResponse, + Any + ] = + endpoint.post + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in(didUrlPathPrefix) + .in( + jsonBody[CredentialSchemaInput] + .description( + "JSON object required for the credential schema creation" + ) + ) + .out( + statusCode(StatusCode.Created) + .description( + "The new credential schema record is successfully created" + ) + ) + .out(jsonBody[PrismEnvelopeResponse]) + .description("Credential schema record") + .errorOut(basicFailureAndNotFoundAndForbidden) + .name("createSchemaDidUrl") + .summary("Publish new schema to the schema registry, did url resolvable") .description( "Create the new credential schema record with metadata and internal JSON Schema on behalf of Cloud Agent. " + "The credential schema will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it." ) .tag(tagName) - val updateSchemaEndpoint: Endpoint[ + val updateSchemaHttpUrlEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), - (RequestContext, String, UUID, CredentialSchemaInput), + (RequestContext, UUID, CredentialSchemaInput), ErrorResponse, CredentialSchemaResponse, Any @@ -113,9 +142,9 @@ object SchemaRegistryEndpoints { .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( - "schema-registry" / - path[String]("author").description(CredentialSchemaResponse.annotations.author.description) / - path[UUID]("id").description(CredentialSchemaResponse.annotations.id.description) + httpUrlPathPrefix / path[UUID]("id").description( + CredentialSchemaResponse.annotations.id.description + ) ) .in( jsonBody[CredentialSchemaInput] @@ -140,7 +169,46 @@ object SchemaRegistryEndpoints { ) .tag(tagName) - val getSchemaByIdEndpoint: PublicEndpoint[ + val updateSchemaDidUrlEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, UUID, CredentialSchemaInput), + ErrorResponse, + PrismEnvelopeResponse, + Any + ] = + endpoint.put + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + didUrlPathPrefix / path[UUID]("id").description( + CredentialSchemaResponse.annotations.id.description + ) + ) + .in( + jsonBody[CredentialSchemaInput] + .description( + "JSON object required for the credential schema update" + ) + ) + .out( + statusCode(StatusCode.Ok) + .description( + "The credential schema record is successfully updated" + ) + ) + .out(jsonBody[PrismEnvelopeResponse]) + .description("Credential schema record wrapped in an envelope") + .errorOut(basicFailureAndNotFoundAndForbidden) + .name("updateSchemaDidUrl") + .summary("Publish the new version of the credential schema to the schema registry") + .description( + "Publish the new version of the credential schema record with metadata and internal JSON Schema on behalf of Cloud Agent. " + + "The credential schema will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it." + ) + .tag(tagName) + + val getSchemaByIdHttpUrlEndpoint: PublicEndpoint[ (RequestContext, UUID), ErrorResponse, CredentialSchemaResponse, @@ -149,7 +217,7 @@ object SchemaRegistryEndpoints { endpoint.get .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( - "schema-registry" / "schemas" / path[UUID]("guid").description( + httpUrlPathPrefix / path[UUID]("guid").description( "Globally unique identifier of the credential schema record" ) ) @@ -162,16 +230,42 @@ object SchemaRegistryEndpoints { ) .tag(tagName) - val getRawSchemaByIdEndpoint: PublicEndpoint[ + val getSchemaByIdDidUrlEndpoint: PublicEndpoint[ (RequestContext, UUID), ErrorResponse, - Json, // changed to generic Json type + PrismEnvelopeResponse, Any ] = endpoint.get .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( - "schema-registry" / "schemas" / path[UUID]("guid") / "schema".description( + didUrlPathPrefix / path[UUID]("guid").description( + "Globally unique identifier of the credential schema record" + ) + ) + .out( + jsonBody[PrismEnvelopeResponse].description( + "CredentialSchema found by `guid`, wrapped in an envelope" + ) + ) + .errorOut(basicFailuresAndNotFound) + .name("getSchemaByIdDidUrl") + .summary("Fetch the schema from the registry by `guid`") + .description( + "Fetch the credential schema by the unique identifier" + ) + .tag(tagName) + + val getRawSchemaByIdHttpUrlEndpoint: PublicEndpoint[ + (RequestContext, UUID), + ErrorResponse, + Json, // returns json of raw schema + Any + ] = + endpoint.get + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + httpUrlPathPrefix / path[UUID]("guid") / "schema".description( "Globally unique identifier of the credential schema record" ) ) @@ -182,9 +276,32 @@ object SchemaRegistryEndpoints { .description("Fetch the credential schema by the unique identifier") .tag("Schema Registry") + val getRawSchemaByIdDidUrlEndpoint: PublicEndpoint[ + (RequestContext, UUID), + ErrorResponse, + PrismEnvelopeResponse, // returns an envelope, where resource is a json of wrapped schema + Any + ] = + endpoint.get + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + didUrlPathPrefix / path[UUID]("guid") / "schema".description( + "Globally unique identifier of the credential schema record" + ) + ) + .out( + jsonBody[PrismEnvelopeResponse].description("Raw JSON response of the CredentialSchema") + ) + .errorOut(basicFailuresAndNotFound) + .name("getRawSchemaByIdDidUrl") + .summary("Fetch the schema from the registry by `guid`") + .description("Fetch the credential schema by the unique identifier") + .tag("Schema Registry") + private val credentialSchemaFilterInput: EndpointInput[FilterInput] = EndpointInput.derived[FilterInput] private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] - val lookupSchemasByQueryEndpoint: Endpoint[ + + val lookupSchemasByQueryHttpUrlEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), ( RequestContext, @@ -212,4 +329,37 @@ object SchemaRegistryEndpoints { "Lookup schemas by `author`, `name`, `tags` parameters and control the pagination by `offset` and `limit` parameters " ) .tag(tagName) + + val lookupSchemasByQueryDidUrlEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + ( + RequestContext, + FilterInput, + PaginationInput, + Option[Order] + ), + ErrorResponse, + CredentialSchemaDidUrlResponsePage, + Any + ] = + endpoint.get + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in("schema-registry" / "schemas" / "did-url".description("Lookup schemas by query")) + .in(credentialSchemaFilterInput) + .in(paginationInput) + .in(query[Option[Order]]("order")) + .out( + jsonBody[CredentialSchemaDidUrlResponsePage].description( + "Collection of CredentialSchema records each wrapped in an envelope." + ) + ) + .errorOut(basicFailuresAndForbidden) + .name("lookupSchemasByQueryDidUrl") + .summary("Lookup schemas by indexed fields") + .description( + "Lookup schemas by `author`, `name`, `tags` parameters and control the pagination by `offset` and `limit` parameters " + ) + .tag(tagName) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryServerEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryServerEndpoints.scala index 75c8f4b1ec..7d8c57e7ca 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryServerEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/SchemaRegistryServerEndpoints.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.credentialschema +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.api.http.model.{Order, PaginationInput} import org.hyperledger.identus.api.http.RequestContext @@ -15,12 +16,14 @@ import zio.* import java.util.UUID class SchemaRegistryServerEndpoints( + config: AppConfig, credentialSchemaController: CredentialSchemaController, authenticator: Authenticator[BaseEntity], authorizer: Authorizer[BaseEntity] ) { - val createSchemaServerEndpoint: ZServerEndpoint[Any, Any] = - createSchemaEndpoint + + object create { + val http: ZServerEndpoint[Any, Any] = createSchemaHttpUrlEndpoint .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, schemaInput: CredentialSchemaInput) => @@ -30,65 +33,125 @@ class SchemaRegistryServerEndpoints( .logTrace(ctx) } } + val did: ZServerEndpoint[Any, Any] = createSchemaDidUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, schemaInput: CredentialSchemaInput) => + credentialSchemaController + .createSchemaDidUrl(config.agent.httpEndpoint.serviceName, schemaInput)(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } - val updateSchemaServerEndpoint: ZServerEndpoint[Any, Any] = - updateSchemaEndpoint + val all = List(http, did) + + } + + object update { + val http: ZServerEndpoint[Any, Any] = updateSchemaHttpUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, id: UUID, schemaInput: CredentialSchemaInput) => + credentialSchemaController + .updateSchema(id, schemaInput)(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } + val did: ZServerEndpoint[Any, Any] = updateSchemaDidUrlEndpoint .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) .serverLogic { wac => - { case (ctx: RequestContext, author: String, id: UUID, schemaInput: CredentialSchemaInput) => + { case (ctx: RequestContext, id: UUID, schemaInput: CredentialSchemaInput) => credentialSchemaController - .updateSchema(author, id, schemaInput)(ctx) + .updateSchemaDidUrl(config.agent.httpEndpoint.serviceName, id, schemaInput)(ctx) .provideSomeLayer(ZLayer.succeed(wac)) .logTrace(ctx) } } + val all = List(http, did) + + } - val getSchemaByIdServerEndpoint: ZServerEndpoint[Any, Any] = - getSchemaByIdEndpoint + object get { + val http: ZServerEndpoint[Any, Any] = getSchemaByIdHttpUrlEndpoint .zServerLogic { case (ctx: RequestContext, guid: UUID) => credentialSchemaController .getSchemaByGuid(guid)(ctx) .logTrace(ctx) } + val did: ZServerEndpoint[Any, Any] = getSchemaByIdDidUrlEndpoint + .zServerLogic { case (ctx: RequestContext, guid: UUID) => + credentialSchemaController + .getSchemaByGuidDidUrl(config.agent.httpEndpoint.serviceName, guid)(ctx) + .logTrace(ctx) + } + val all = List(http, did) + + } - val getRawSchemaByIdServerEndpoint: ZServerEndpoint[Any, Any] = - getRawSchemaByIdEndpoint + object getRaw { + val http: ZServerEndpoint[Any, Any] = getRawSchemaByIdHttpUrlEndpoint .zServerLogic { case (ctx: RequestContext, guid: UUID) => credentialSchemaController.getSchemaJsonByGuid(guid)(ctx) } + val did: ZServerEndpoint[Any, Any] = getRawSchemaByIdDidUrlEndpoint + .zServerLogic { case (ctx: RequestContext, guid: UUID) => + credentialSchemaController.getSchemaJsonByGuidDidUrl(config.agent.httpEndpoint.serviceName, guid)(ctx) + } + val all = List(http, did) - val lookupSchemasByQueryServerEndpoint: ZServerEndpoint[Any, Any] = - lookupSchemasByQueryEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) - .serverLogic { wac => - { case (ctx: RequestContext, filter: FilterInput, paginationInput: PaginationInput, order: Option[Order]) => - credentialSchemaController - .lookupSchemas( - filter, - paginationInput.toPagination, - order - )(ctx) - .provideSomeLayer(ZLayer.succeed(wac)) - .logTrace(ctx) + } + + object getMany { + val http: ZServerEndpoint[Any, Any] = + lookupSchemasByQueryHttpUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, filter: FilterInput, paginationInput: PaginationInput, order: Option[Order]) => + credentialSchemaController + .lookupSchemas( + filter, + paginationInput.toPagination, + order, + )(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } } - } + + val did: ZServerEndpoint[Any, Any] = + lookupSchemasByQueryDidUrlEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, filter: FilterInput, paginationInput: PaginationInput, order: Option[Order]) => + credentialSchemaController + .lookupSchemasDidUrl( + config.agent.httpEndpoint.serviceName, + filter, + paginationInput.toPagination, + order, + )(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } + + val all = List(http, did) + } val all: List[ZServerEndpoint[Any, Any]] = - List( - createSchemaServerEndpoint, - updateSchemaServerEndpoint, - getSchemaByIdServerEndpoint, - getRawSchemaByIdServerEndpoint, - lookupSchemasByQueryServerEndpoint - ) + create.all ++ update.all ++ getMany.all ++ getRaw.all ++ get.all } object SchemaRegistryServerEndpoints { - def all: URIO[CredentialSchemaController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[CredentialSchemaController & DefaultAuthenticator & AppConfig, List[ZServerEndpoint[Any, Any]]] = { for { authenticator <- ZIO.service[DefaultAuthenticator] + config <- ZIO.service[AppConfig] schemaRegistryService <- ZIO.service[CredentialSchemaController] schemaRegistryEndpoints = new SchemaRegistryServerEndpoints( + config, schemaRegistryService, authenticator, authenticator diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaController.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaController.scala index 938ddfaed1..d5576d5a27 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaController.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaController.scala @@ -3,11 +3,13 @@ package org.hyperledger.identus.pollux.credentialschema.controller import org.hyperledger.identus.api.http.* import org.hyperledger.identus.api.http.model.{Order, Pagination} import org.hyperledger.identus.pollux.credentialschema.http.{ + CredentialSchemaDidUrlResponsePage, CredentialSchemaInput, CredentialSchemaResponse, CredentialSchemaResponsePage, FilterInput } +import org.hyperledger.identus.pollux.PrismEnvelopeResponse import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* import zio.json.ast.Json @@ -20,27 +22,48 @@ trait CredentialSchemaController { rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] - def updateSchema(author: String, id: UUID, in: CredentialSchemaInput)(implicit + def createSchemaDidUrl(baseUrlServiceName: String, in: CredentialSchemaInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PrismEnvelopeResponse] + + def updateSchema(id: UUID, in: CredentialSchemaInput)(implicit rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] + def updateSchemaDidUrl(baseUrlServiceName: String, id: UUID, in: CredentialSchemaInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PrismEnvelopeResponse] + def getSchemaByGuid(id: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, CredentialSchemaResponse] + def getSchemaByGuidDidUrl(baseUrlServiceName: String, id: UUID)(implicit + rc: RequestContext + ): IO[ErrorResponse, PrismEnvelopeResponse] + def getSchemaJsonByGuid(id: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, Json] - def delete(guid: UUID)(implicit + def getSchemaJsonByGuidDidUrl(baseUrlServiceName: String, id: UUID)(implicit rc: RequestContext - ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] + ): IO[ErrorResponse, PrismEnvelopeResponse] def lookupSchemas( filter: FilterInput, pagination: Pagination, - order: Option[Order] + order: Option[Order], )(implicit rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponsePage] + + def lookupSchemasDidUrl( + baseUrlServiceName: String, + filter: FilterInput, + pagination: Pagination, + order: Option[Order], + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaDidUrlResponsePage] } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerImpl.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerImpl.scala index 3804d24a4e..91e66f97d8 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerImpl.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerImpl.scala @@ -1,22 +1,29 @@ package org.hyperledger.identus.pollux.credentialschema.controller +import cats.implicits.* import org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, PublicationState} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.api.http.* import org.hyperledger.identus.api.http.model.{CollectionStats, Order, Pagination} import org.hyperledger.identus.castor.core.model.did.{LongFormPrismDID, PrismDID} +import org.hyperledger.identus.pollux.core.model import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema.FilteredEntries +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.service.CredentialSchemaService import org.hyperledger.identus.pollux.credentialschema.http.{ + CredentialSchemaDidUrlResponse, + CredentialSchemaDidUrlResponsePage, + CredentialSchemaInnerDidUrlResponse, CredentialSchemaInput, CredentialSchemaResponse, CredentialSchemaResponsePage, FilterInput } import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaInput.toDomain -import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaResponse.fromDomain +import org.hyperledger.identus.pollux.PrismEnvelopeResponse import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* +import zio.json.* import zio.json.ast.Json import java.util.UUID @@ -24,41 +31,89 @@ import scala.language.implicitConversions class CredentialSchemaControllerImpl(service: CredentialSchemaService, managedDIDService: ManagedDIDService) extends CredentialSchemaController { + + private def parsingCredentialSchemaError(e: String) = ErrorResponse + .internalServerError(detail = Some(s"Error occurred while parsing the credential schema response: $e")) + override def createSchema( in: CredentialSchemaInput )(implicit rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] = { for { - validated <- validatePrismDID(in.author) + _ <- validatePrismDID(in.author) result <- service .create(toDomain(in)) - .map(cs => fromDomain(cs).withBaseUri(rc.request.uri)) + .map(cs => CredentialSchemaResponse.fromDomain(cs).withBaseUri(rc.request.uri)) } yield result } - override def updateSchema(author: String, id: UUID, in: CredentialSchemaInput)(implicit + def createSchemaDidUrl(baseUrlServiceName: String, in: CredentialSchemaInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PrismEnvelopeResponse] = { + val res = for { + validated <- validatePrismDID(in.author) + result <- service.create(toDomain(in), ResourceResolutionMethod.did) + response <- ZIO + .fromEither(CredentialSchemaDidUrlResponse.asPrismEnvelopeResponse(result, baseUrlServiceName)) + .mapError(parsingCredentialSchemaError) + + } yield response + + res + } + + override def updateSchema(id: UUID, in: CredentialSchemaInput)(implicit rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] = { for { _ <- validatePrismDID(in.author) result <- service - .update(id, toDomain(in).copy(author = author)) - .map(cs => fromDomain(cs).withBaseUri(rc.request.uri)) + .update(id, toDomain(in)) + .map(cs => CredentialSchemaResponse.fromDomain(cs).withBaseUri(rc.request.uri)) } yield result } + override def updateSchemaDidUrl(baseUrlServiceName: String, id: UUID, in: CredentialSchemaInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PrismEnvelopeResponse] = { + val res = for { + _ <- validatePrismDID(in.author) + cs <- service + .update(id, toDomain(in), ResourceResolutionMethod.did) + result <- ZIO + .fromEither(CredentialSchemaDidUrlResponse.asPrismEnvelopeResponse(cs, baseUrlServiceName)) + .mapError(parsingCredentialSchemaError) + } yield result + + res + } + override def getSchemaByGuid(guid: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, CredentialSchemaResponse] = { service .getByGUID(guid) .map( - fromDomain(_) + CredentialSchemaResponse + .fromDomain(_) .withSelf(rc.request.uri.toString) ) } + override def getSchemaByGuidDidUrl(baseUrlServiceName: String, guid: UUID)(implicit + rc: RequestContext + ): IO[ErrorResponse, PrismEnvelopeResponse] = { + val res: IO[ErrorResponse, PrismEnvelopeResponse] = for { + cs <- service.getByGUID(guid, ResourceResolutionMethod.did) + response <- ZIO + .fromEither(CredentialSchemaDidUrlResponse.asPrismEnvelopeResponse(cs, baseUrlServiceName)) + .mapError(parsingCredentialSchemaError) + } yield response + + res + } + override def getSchemaJsonByGuid(guid: UUID)(implicit rc: RequestContext ): IO[ErrorResponse, Json] = { @@ -69,15 +124,22 @@ class CredentialSchemaControllerImpl(service: CredentialSchemaService, managedDI ) } - override def delete(guid: UUID)(implicit + override def getSchemaJsonByGuidDidUrl(baseUrlServiceName: String, id: UUID)(implicit rc: RequestContext - ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponse] = { - service - .delete(guid) - .map( - fromDomain(_) - .withBaseUri(rc.request.uri) - ) + ): IO[ErrorResponse, PrismEnvelopeResponse] = { + val res = for { + cs <- service.getByGUID(id, ResourceResolutionMethod.did) + authorDid <- ZIO + .fromEither(PrismDID.fromString(cs.author)) + .mapError(_ => ErrorResponse.internalServerError(detail = Some("Invalid schema author DID"))) + response <- ZIO + .fromEither( + CredentialSchemaInnerDidUrlResponse.asPrismEnvelopeResponse(cs.schema, authorDid, cs.id, baseUrlServiceName) + ) + .mapError(parsingCredentialSchemaError) + } yield response + + res } override def lookupSchemas( @@ -89,16 +151,44 @@ class CredentialSchemaControllerImpl(service: CredentialSchemaService, managedDI ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaResponsePage] = { for { filteredEntries: FilteredEntries <- service.lookup( - filter.toDomain, + filter.toDomain(), pagination.offset, pagination.limit ) entries = filteredEntries.entries - .map(fromDomain(_).withBaseUri(rc.request.uri)) + .map(CredentialSchemaResponse.fromDomain(_).withBaseUri(rc.request.uri)) .toList page = CredentialSchemaResponsePage(entries) stats = CollectionStats(filteredEntries.totalCount, filteredEntries.count) - } yield CredentialSchemaControllerLogic(rc, pagination, page, stats).result + } yield CredentialSchemaControllerLogic(rc, pagination, stats).result(page) + } + + override def lookupSchemasDidUrl( + baseUrlServiceName: String, + filter: FilterInput, + pagination: Pagination, + order: Option[Order], + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialSchemaDidUrlResponsePage] = { + for { + filteredEntries: FilteredEntries <- service.lookup( + filter.toDomain(ResourceResolutionMethod.did), + pagination.offset, + pagination.limit + ) + entriesZio = filteredEntries.entries + .traverse(cs => CredentialSchemaDidUrlResponse.asPrismEnvelopeResponse(cs, baseUrlServiceName)) + + entries <- ZIO + .fromEither(entriesZio) + .mapError(e => + ErrorResponse.internalServerError(detail = Some(s"Error occurred while parsing a schema response: $e")) + ) + + page = CredentialSchemaDidUrlResponsePage(entries) + stats = CollectionStats(filteredEntries.totalCount, filteredEntries.count) + } yield CredentialSchemaControllerLogic(rc, pagination, stats).resultDidUrl(page) } private def validatePrismDID(author: String) = diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerLogic.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerLogic.scala index b4afaf255e..f80e256ff9 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerLogic.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/controller/CredentialSchemaControllerLogic.scala @@ -3,27 +3,24 @@ package org.hyperledger.identus.pollux.credentialschema.controller import org.hyperledger.identus.api.http.model.{CollectionStats, Pagination} import org.hyperledger.identus.api.http.RequestContext import org.hyperledger.identus.api.util.PaginationUtils -import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaResponsePage +import org.hyperledger.identus.pollux.credentialschema.http.{ + CredentialSchemaDidUrlResponsePage, + CredentialSchemaResponsePage +} import sttp.model.Uri case class CredentialSchemaControllerLogic( ctx: RequestContext, pagination: Pagination, - page: CredentialSchemaResponsePage, stats: CollectionStats ) { - private def composeNextUri(uri: Uri): Option[Uri] = - PaginationUtils.composeNextUri(uri, page.contents, pagination, stats) - - private def composePreviousUri(uri: Uri): Option[Uri] = - PaginationUtils.composePreviousUri(uri, page.contents, pagination, stats) + val self = ctx.request.uri.toString + val pageOf = ctx.request.uri.copy(querySegments = Seq.empty).toString - def result: CredentialSchemaResponsePage = { - val self = ctx.request.uri.toString - val pageOf = ctx.request.uri.copy(querySegments = Seq.empty).toString - val next = composeNextUri(ctx.request.uri).map(_.toString) - val previous = composePreviousUri(ctx.request.uri).map(_.toString) + def result(page: CredentialSchemaResponsePage): CredentialSchemaResponsePage = { + val next = PaginationUtils.composeNextUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + val previous = PaginationUtils.composePreviousUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) val pageResult = page.copy( self = self, @@ -39,4 +36,19 @@ case class CredentialSchemaControllerLogic( pageResult } + + def resultDidUrl(page: CredentialSchemaDidUrlResponsePage): CredentialSchemaDidUrlResponsePage = { + val next = PaginationUtils.composeNextUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + val previous = PaginationUtils.composePreviousUri(ctx.request.uri, page.contents, pagination, stats).map(_.toString) + + val pageResult = page.copy( + self = self, + pageOf = pageOf, + next = next, + previous = previous, + contents = page.contents + ) + + pageResult + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponse.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponse.scala new file mode 100644 index 0000000000..a269e85872 --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponse.scala @@ -0,0 +1,68 @@ +package org.hyperledger.identus.pollux.credentialschema.http + +import org.hyperledger.identus.castor.core.model.did.{DIDUrl, PrismDID} +import org.hyperledger.identus.pollux.core.model +import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema +import org.hyperledger.identus.pollux.PrismEnvelopeResponse +import org.hyperledger.identus.shared.crypto.Sha256Hash +import org.hyperledger.identus.shared.json.Json as JsonUtils +import org.hyperledger.identus.shared.utils.Base64Utils +import zio.json.* +import zio.json.ast.Json + +import java.util.UUID +import scala.collection.immutable.ListMap + +object CredentialSchemaDidUrlResponse { + + def asPrismEnvelopeResponse(cs: CredentialSchema, serviceName: String): Either[String, PrismEnvelopeResponse] = { + + for { + authorDid <- PrismDID.fromString(cs.author) + canonicalized <- JsonUtils.canonicalizeToJcs(cs.toJson).left.map(_.toString) + encoded = Base64Utils.encodeURL(canonicalized.getBytes) + hash = Sha256Hash.compute(encoded.getBytes).hexEncoded + didUrl = DIDUrl( + authorDid.did, + Seq(), + ListMap( + "resourceService" -> Seq(serviceName), + "resourcePath" -> Seq(s"schema-registry/schemas/did-url/${cs.guid}?resourceHash=$hash"), + ), + None + ).toString + } yield PrismEnvelopeResponse( + resource = encoded, + url = didUrl + ) + } + +} + +object CredentialSchemaInnerDidUrlResponse { + + def asPrismEnvelopeResponse( + innerSchema: Json, + authorDid: PrismDID, + schemaGuid: UUID, + serviceName: String + ): Either[String, PrismEnvelopeResponse] = { + for { + canonicalized <- JsonUtils.canonicalizeToJcs(innerSchema.toJson).left.map(_.toString) + encoded = Base64Utils.encodeURL(canonicalized.getBytes) + hash = Sha256Hash.compute(encoded.getBytes).hexEncoded + didUrl = DIDUrl( + authorDid.did, + Seq(), + ListMap( + "resourceService" -> Seq(serviceName), + "resourcePath" -> Seq(s"schema-registry/schemas/did-url/$schemaGuid/schema?resourceHash=$hash"), + ), + None + ).toString + } yield PrismEnvelopeResponse( + resource = encoded, + url = didUrl + ) + } +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponsePage.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponsePage.scala new file mode 100644 index 0000000000..e18de3c60c --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaDidUrlResponsePage.scala @@ -0,0 +1,91 @@ +package org.hyperledger.identus.pollux.credentialschema.http + +import org.hyperledger.identus.api.http.Annotation +import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaDidUrlResponsePage.annotations +import org.hyperledger.identus.pollux.PrismEnvelopeResponse +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{description, encodedExample} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +case class CredentialSchemaDidUrlResponsePage( + @description(annotations.contents.description) + @encodedExample(annotations.contents.example) + contents: Seq[PrismEnvelopeResponse], + @description(annotations.kind.description) + @encodedExample(annotations.kind.example) + kind: String = "CredentialSchemaDidUrlPage", + @description(annotations.self.description) + @encodedExample(annotations.self.example) + self: String = "", + @description(annotations.pageOf.description) + @encodedExample(annotations.pageOf.example) + pageOf: String = "", + @description(annotations.next.description) + @encodedExample(annotations.next.example) + next: Option[String] = None, + @description(annotations.previous.description) + @encodedExample(annotations.previous.example) + previous: Option[String] = None +) { + def withSelf(self: String) = copy(self = self) +} + +object CredentialSchemaDidUrlResponsePage { + given encoder: JsonEncoder[CredentialSchemaDidUrlResponsePage] = + DeriveJsonEncoder.gen[CredentialSchemaDidUrlResponsePage] + given decoder: JsonDecoder[CredentialSchemaDidUrlResponsePage] = + DeriveJsonDecoder.gen[CredentialSchemaDidUrlResponsePage] + given schema: Schema[CredentialSchemaDidUrlResponsePage] = Schema.derived + + val Example = CredentialSchemaDidUrlResponsePage( + contents = annotations.contents.example, + kind = annotations.kind.example, + self = annotations.self.example, + pageOf = annotations.pageOf.example, + next = Some(annotations.next.example), + previous = Some(annotations.previous.example) + ) + + object annotations { + + object contents + extends Annotation[Seq[PrismEnvelopeResponse]]( + description = + "A sequence of PrismEnvelopeResponse objects representing the list of credential schemas wrapped in an envelope", + example = Seq.empty + ) + + object kind + extends Annotation[String]( + description = + "A string field indicating the type of the API response. In this case, it will always be set to `CredentialSchemaPage`", + example = "CredentialSchemaPage" + ) // TODO Tech Debt ticket - the kind in a collection should be collection, not the underlying record type + + object self + extends Annotation[String]( + description = "A string field containing the URL of the current API endpoint", + example = "/cloud-agent/schema-registry/schemas/did-url?skip=10&limit=10" + ) + + object pageOf + extends Annotation[String]( + description = "A string field indicating the type of resource that the contents field contains", + example = "/cloud-agent/schema-registry/schemas/did-url" + ) + + object next + extends Annotation[String]( + description = "An optional string field containing the URL of the next page of results. " + + "If the API response does not contain any more pages, this field should be set to None.", + example = "/cloud-agent/schema-registry/schemas/did-url?skip=20&limit=10" + ) + + object previous + extends Annotation[String]( + description = "An optional string field containing the URL of the previous page of results. " + + "If the API response is the first page of results, this field should be set to None.", + example = "/cloud-agent/schema-registry/schemas/did-url?skip=0&limit=10" + ) + } +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponse.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponse.scala index ccb7e5f3cd..0d21684ddd 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponse.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponse.scala @@ -3,9 +3,10 @@ package org.hyperledger.identus.pollux.credentialschema.http import org.hyperledger.identus.api.http.* import org.hyperledger.identus.pollux.core.model import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod.* import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaResponse.annotations import sttp.model.Uri -import sttp.model.Uri.* import sttp.tapir.json.zio.schemaForZioJsonValue import sttp.tapir.Schema import sttp.tapir.Schema.annotations.{default, description, encodedExample, encodedName} @@ -52,6 +53,9 @@ case class CredentialSchemaResponse( @description(annotations.proof.description) @encodedExample(annotations.proof.example.toJson) proof: Option[Proof], + @description(annotations.resolutionMethod.description) + @encodedExample(annotations.resolutionMethod.example) + resolutionMethod: ResourceResolutionMethod, @description(annotations.kind.description) @encodedExample(annotations.kind.example) kind: String = "CredentialSchema", @@ -78,15 +82,15 @@ object CredentialSchemaResponse { schema = cs.schema, author = cs.author, authored = cs.authored, + resolutionMethod = cs.resolutionMethod, proof = None ) - given scala.Conversion[CredentialSchema, CredentialSchemaResponse] = fromDomain - given encoder: zio.json.JsonEncoder[CredentialSchemaResponse] = DeriveJsonEncoder.gen[CredentialSchemaResponse] given decoder: zio.json.JsonDecoder[CredentialSchemaResponse] = DeriveJsonDecoder.gen[CredentialSchemaResponse] + given schema: Schema[CredentialSchemaResponse] = Schema.derived object annotations { @@ -134,6 +138,13 @@ object CredentialSchemaResponse { description = "A string that identifies the type of resource being returned in the response.", example = "CredentialSchema" ) + + object resolutionMethod + extends Annotation[String]( + description = s"The method used to resolve the schema. It can be either HTTP or DID.", + example = ResourceResolutionMethod.http.toString + ) + object proof extends Annotation[Proof]( description = "A digital signature over the Credential Schema for the sake of asserting authorship. " + diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponsePage.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponsePage.scala index ac2f0850ea..b3f8174c69 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponsePage.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/CredentialSchemaResponsePage.scala @@ -2,9 +2,10 @@ package org.hyperledger.identus.pollux.credentialschema.http import org.hyperledger.identus.api.http.Annotation import org.hyperledger.identus.pollux.credentialschema.http.CredentialSchemaResponsePage.annotations +import sttp.tapir.generic.auto.* import sttp.tapir.Schema import sttp.tapir.Schema.annotations.{description, encodedExample} -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} +import zio.json.* case class CredentialSchemaResponsePage( @description(annotations.contents.description) diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/FilterInput.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/FilterInput.scala index 48ad4f542b..5f7500ec20 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/FilterInput.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/pollux/credentialschema/http/FilterInput.scala @@ -3,9 +3,10 @@ package org.hyperledger.identus.pollux.credentialschema.http import org.hyperledger.identus.api.http.* import org.hyperledger.identus.pollux.core.model import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod.* import org.hyperledger.identus.pollux.credentialschema.http.FilterInput.annotations import sttp.tapir.EndpointIO.annotations.{example, query} -import sttp.tapir.Validator.* case class FilterInput( @query @@ -21,7 +22,8 @@ case class FilterInput( @example(annotations.tags.example.headOption) tags: Option[String] = Option.empty[String] ) { - def toDomain = CredentialSchema.Filter(author, name, version, tags) + def toDomain(resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http) = + CredentialSchema.Filter(author, name, version, tags, resolutionMethod) } object FilterInput { diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala index 05b217c71e..2ef8229f7b 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala @@ -1,5 +1,7 @@ package org.hyperledger.identus.api.util +import com.typesafe.config.ConfigFactory +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.http.DocModels import org.hyperledger.identus.agent.server.AgentHttpServer import org.hyperledger.identus.castor.controller.{DIDController, DIDRegistrarController} @@ -23,6 +25,7 @@ import org.hyperledger.identus.verification.controller.VcVerificationController import org.scalatestplus.mockito.MockitoSugar.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter import zio.{Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer} +import zio.config.typesafe.TypesafeConfigProvider import java.nio.charset.StandardCharsets import java.nio.file.{Files, Path} @@ -42,6 +45,11 @@ object Tapir2StaticOAS extends ZIOAppDefault { val path = Path.of(args.head) Using(Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { writer => writer.write(yaml) } } + val configLayer = ZLayer.fromZIO( + TypesafeConfigProvider + .fromTypesafeConfig(ConfigFactory.load()) + .load(AppConfig.config) + ) effect.provideSomeLayer( ZLayer.succeed(mock[ConnectionController]) ++ ZLayer.succeed(mock[CredentialDefinitionController]) ++ @@ -60,7 +68,8 @@ object Tapir2StaticOAS extends ZIOAppDefault { ZLayer.succeed(mock[EventController]) ++ ZLayer.succeed(mock[CredentialIssuerController]) ++ ZLayer.succeed(mock[PresentationExchangeController]) ++ - ZLayer.succeed(mock[Oid4vciAuthenticatorFactory]) + ZLayer.succeed(mock[Oid4vciAuthenticatorFactory]) ++ + configLayer ) } diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerImplSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerImplSpec.scala index 422928756c..7d37bbb9c3 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerImplSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerImplSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.issue.controller +import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, ManagedDIDState, PublicationState} import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} import org.hyperledger.identus.api.http.ErrorResponse @@ -21,7 +22,9 @@ import org.hyperledger.identus.mercury.protocol.connection.ConnectionResponse import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation import org.hyperledger.identus.pollux.core.model.{CredentialFormat, DidCommID, IssueCredentialRecord} import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.{ProtocolState, Role} -import org.hyperledger.identus.pollux.core.service.MockCredentialService +import org.hyperledger.identus.pollux.core.repository.CredentialDefinitionRepositoryInMemory +import org.hyperledger.identus.pollux.core.service.{CredentialDefinitionServiceImpl, MockCredentialService} +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.shared.models.{KeyId, WalletId} import sttp.client3.{basicRequest, DeserializationException, UriContext} import sttp.client3.ziojson.* @@ -53,9 +56,8 @@ object IssueControllerImplSpec extends ZIOSpecDefault with IssueControllerTestTo credentialFormat = Some("JWT"), claims = json.toJsonAST.toOption.get, automaticIssuance = Some(true), - issuingDID = Some( - "did:prism:332518729a7b7805f73a788e0944802527911901d9b7c16152281be9bc62d944:CosBCogBEkkKFW15LWtleS1hdXRoZW50aWNhdGlvbhAESi4KCXNlY3AyNTZrMRIhAuYoRIefsLhkvYwHz8gDtkG2b0kaZTDOLj_SExWX1fOXEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQLOzab8f0ibt1P0zdMfoWDQTSlPc8_tkV9Jk5BBsXB8fA" - ), + issuingDID = + "did:prism:332518729a7b7805f73a788e0944802527911901d9b7c16152281be9bc62d944:CosBCogBEkkKFW15LWtleS1hdXRoZW50aWNhdGlvbhAESi4KCXNlY3AyNTZrMRIhAuYoRIefsLhkvYwHz8gDtkG2b0kaZTDOLj_SExWX1fOXEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQLOzab8f0ibt1P0zdMfoWDQTSlPc8_tkV9Jk5BBsXB8fA", issuingKid = Some(KeyId("some_kid_id")), connectionId = Some(UUID.fromString("123e4567-e89b-12d3-a456-426614174000")) ) @@ -177,11 +179,18 @@ object IssueControllerImplSpec extends ZIOSpecDefault with IssueControllerTestTo ) ) ) + + private val credentialDefinitionServiceLayer = + CredentialDefinitionRepositoryInMemory.layer + >+> GenericSecretStorageInMemory.layer >+> + ResourceUrlResolver.layer >>> CredentialDefinitionServiceImpl.layer + val baseLayer = MockManagedDIDService.empty >+> MockDIDService.empty >+> MockCredentialService.empty >+> MockConnectionService.empty + >+> credentialDefinitionServiceLayer def spec = (httpErrorResponses @@ migrate( schema = "public", diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala index bcc8e989ab..c7d1bd7a8f 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala @@ -61,7 +61,7 @@ trait IssueControllerTestTools extends PostgresTestContainerSupport { lazy val testEnvironmentLayer = ZLayer.makeSome[ - ManagedDIDService & DIDService & CredentialService & ConnectionService, + ManagedDIDService & DIDService & CredentialService & CredentialDefinitionService & ConnectionService, IssueController & AppConfig & PostgreSQLContainer & AuthenticatorWithAuthZ[BaseEntity] ](IssueControllerImpl.layer, configLayer, pgContainerLayer, DefaultEntityAuthenticator.layer) diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala index ea80db437c..6facffe5ed 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala @@ -17,6 +17,7 @@ import org.hyperledger.identus.pollux.core.repository.{ CredentialStatusListRepositoryInMemory } import org.hyperledger.identus.pollux.core.service.* +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.PrismDidResolver import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.{Clock, Random, URLayer, ZIO, ZLayer} @@ -48,7 +49,7 @@ object OIDCCredentialIssuerServiceSpec CredentialRepositoryInMemory.layer, CredentialStatusListRepositoryInMemory.layer, PrismDidResolver.layer, - ResourceURIDereferencerImpl.layer, + ResourceUrlResolver.layer, credentialDefinitionServiceLayer, GenericSecretStorageInMemory.layer, LinkSecretServiceImpl.layer, diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala index 3ffdc9d43a..ae15a7b190 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.credentialdefinition +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, Entity} import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage import org.hyperledger.identus.api.http.ErrorResponse @@ -69,7 +70,8 @@ object CredentialDefinitionBasicSpec extends ZIOSpecDefault with CredentialDefin for { controller <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - } yield httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + } yield httpBackend(config, controller, authenticator) def createCredentialDefinitionResponseZIO = for { backend <- backendZIO diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala index 7eac221f70..735d49012d 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.credentialdefinition +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.agent.walletapi.service.MockManagedDIDService import org.hyperledger.identus.api.http.ErrorResponse @@ -28,7 +29,8 @@ object CredentialDefinitionFailureSpec extends ZIOSpecDefault with CredentialDef for { credentialDefinitionRegistryService <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(credentialDefinitionRegistryService, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, credentialDefinitionRegistryService, authenticator) response: CredentialDefinitionBadRequestResponse <- basicRequest .post(credentialDefinitionUriBase) .body("""{"foo":"bar"}""") diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala index 6da8f500fb..169df11436 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.credentialdefinition +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.container.util.MigrationAspects.migrate import org.hyperledger.identus.iam.authentication.AuthenticatorWithAuthZ @@ -25,13 +26,14 @@ object CredentialDefinitionLookupAndPaginationSpec def fetchAllPages( uri: Uri - ): ZIO[CredentialDefinitionController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + ): ZIO[CredentialDefinitionController & AuthenticatorWithAuthZ[BaseEntity] & AppConfig, Throwable, List[ CredentialDefinitionResponsePage ]] = { for { controller <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) response: CredentialDefinitionResponsePageType <- for { response <- basicRequest @@ -81,8 +83,8 @@ object CredentialDefinitionLookupAndPaginationSpec _ <- deleteAllCredentialDefinitions controller <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) - + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) inputs <- Generator.credentialDefinitionInput.runCollectN(10) _ <- inputs .map(in => diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala index 541a95a3af..19636104a5 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala @@ -1,6 +1,8 @@ package org.hyperledger.identus.pollux.credentialdefinition import com.dimafeng.testcontainers.PostgreSQLContainer +import com.typesafe.config.ConfigFactory +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.http.CustomServerInterceptors import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, ManagedDIDState, PublicationState} @@ -10,11 +12,8 @@ import org.hyperledger.identus.api.http.ErrorResponse import org.hyperledger.identus.castor.core.model.did.PrismDIDOperation import org.hyperledger.identus.iam.authentication.{AuthenticatorWithAuthZ, DefaultEntityAuthenticator} import org.hyperledger.identus.pollux.core.repository.CredentialDefinitionRepository -import org.hyperledger.identus.pollux.core.service.{ - CredentialDefinitionService, - CredentialDefinitionServiceImpl, - ResourceURIDereferencerImpl -} +import org.hyperledger.identus.pollux.core.service.{CredentialDefinitionService, CredentialDefinitionServiceImpl} +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.credentialdefinition.controller.{ CredentialDefinitionController, CredentialDefinitionControllerImpl @@ -35,6 +34,7 @@ import sttp.tapir.server.interceptor.CustomiseInterceptors import sttp.tapir.server.stub.TapirStubInterpreter import sttp.tapir.ztapir.RIOMonadError import zio.* +import zio.config.typesafe.TypesafeConfigProvider import zio.json.EncoderOps import zio.mock.Expectation import zio.test.{Assertion, Gen, ZIOSpecDefault} @@ -56,7 +56,7 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { private val controllerLayer = GenericSecretStorageInMemory.layer >+> systemTransactorLayer >+> contextAwareTransactorLayer >+> JdbcCredentialDefinitionRepository.layer >+> - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> CredentialDefinitionServiceImpl.layer >+> CredentialDefinitionControllerImpl.layer @@ -76,14 +76,21 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { val authenticatorLayer: TaskLayer[AuthenticatorWithAuthZ[BaseEntity]] = DefaultEntityAuthenticator.layer + val configLayer = ZLayer.fromZIO( + TypesafeConfigProvider + .fromTypesafeConfig(ConfigFactory.load()) + .load(AppConfig.config) + ) + lazy val testEnvironmentLayer = ZLayer.makeSome[ ManagedDIDService, CredentialDefinitionController & CredentialDefinitionRepository & CredentialDefinitionService & - PostgreSQLContainer & AuthenticatorWithAuthZ[BaseEntity] & GenericSecretStorage + PostgreSQLContainer & AuthenticatorWithAuthZ[BaseEntity] & GenericSecretStorage & AppConfig ]( controllerLayer, pgContainerLayer, - authenticatorLayer + authenticatorLayer, + configLayer ) val credentialDefinitionUriBase = uri"http://test.com/credential-definition-registry/definitions" @@ -96,23 +103,25 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { } def httpBackend( + config: AppConfig, controller: CredentialDefinitionController, authenticator: AuthenticatorWithAuthZ[BaseEntity] ) = { + val credentialDefinitionRegistryEndpoints = - CredentialDefinitionRegistryServerEndpoints(controller, authenticator, authenticator) + CredentialDefinitionRegistryServerEndpoints(config, controller, authenticator, authenticator) val backend = TapirStubInterpreter( bootstrapOptions(new RIOMonadError[Any]), SttpBackendStub(new RIOMonadError[Any]) ) - .whenServerEndpoint(credentialDefinitionRegistryEndpoints.createCredentialDefinitionServerEndpoint) + .whenServerEndpoint(credentialDefinitionRegistryEndpoints.create.http) .thenRunLogic() - .whenServerEndpoint(credentialDefinitionRegistryEndpoints.getCredentialDefinitionByIdServerEndpoint) + .whenServerEndpoint(credentialDefinitionRegistryEndpoints.get.http) .thenRunLogic() .whenServerEndpoint( - credentialDefinitionRegistryEndpoints.lookupCredentialDefinitionsByQueryServerEndpoint + credentialDefinitionRegistryEndpoints.getMany.http ) .thenRunLogic() .backend() @@ -188,13 +197,14 @@ trait CredentialDefinitionGen { def generateCredentialDefinitionsN( count: Int - ): ZIO[CredentialDefinitionController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + ): ZIO[CredentialDefinitionController & AppConfig & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ CredentialDefinitionInput ]] = for { controller <- ZIO.service[CredentialDefinitionController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) inputs <- Generator.credentialDefinitionInput.runCollectN(count) _ <- inputs .map(in => diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaAnoncredSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaAnoncredSpec.scala index 29064e587f..a9c5fff060 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaAnoncredSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaAnoncredSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.schema +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.api.http.ErrorResponse import org.hyperledger.identus.container.util.MigrationAspects.* @@ -51,7 +52,9 @@ object CredentialSchemaAnoncredSpec extends ZIOSpecDefault with CredentialSchema + wrapSpec(unsupportedSchemaSpec) + wrapSpec(wrongSchemaSpec) - private def wrapSpec(spec: Spec[CredentialSchemaController & AuthenticatorWithAuthZ[BaseEntity], Throwable]) = { + private def wrapSpec( + spec: Spec[CredentialSchemaController & AppConfig & AuthenticatorWithAuthZ[BaseEntity], Throwable] + ) = { (spec @@ nondeterministic @@ sequential @@ timed @@ migrateEach( schema = "public", @@ -65,7 +68,8 @@ object CredentialSchemaAnoncredSpec extends ZIOSpecDefault with CredentialSchema def getSchemaZIO(uuid: UUID) = for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) response <- basicRequest .get(credentialSchemaUriBase.addPath(uuid.toString)) .response(asJsonAlways[CredentialSchemaResponse]) @@ -142,7 +146,8 @@ object CredentialSchemaAnoncredSpec extends ZIOSpecDefault with CredentialSchema for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) response <- basicRequest .post(credentialSchemaUriBase) diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaBasicSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaBasicSpec.scala index ea7cda5e27..ce72b3c1f2 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaBasicSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaBasicSpec.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.api.http.ErrorResponse @@ -69,7 +70,8 @@ object CredentialSchemaBasicSpec extends ZIOSpecDefault with CredentialSchemaTes for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - } yield httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + } yield httpBackend(config, controller, authenticator) def createSchemaResponseZIO = for { backend <- backendZIO diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaFailureSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaFailureSpec.scala index 4069ee5715..73882dc2d5 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaFailureSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaFailureSpec.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.agent.walletapi.service.MockManagedDIDService import org.hyperledger.identus.api.http.ErrorResponse @@ -27,7 +28,8 @@ object CredentialSchemaFailureSpec extends ZIOSpecDefault with CredentialSchemaT for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) response: SchemaBadRequestResponse <- basicRequest .post(credentialSchemaUriBase) .body("""{"foo":"bar"}""") diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala index a348ae5254..6806cc918b 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.container.util.MigrationAspects.migrate import org.hyperledger.identus.iam.authentication.AuthenticatorWithAuthZ @@ -28,13 +29,14 @@ object CredentialSchemaLookupAndPaginationSpec def fetchAllPages( uri: Uri - ): ZIO[CredentialSchemaController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + ): ZIO[CredentialSchemaController & AppConfig & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ CredentialSchemaResponsePage ]] = { for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) response: SchemaPageResponse <- basicRequest .get(uri) .response(asJsonAlways[CredentialSchemaResponsePage]) @@ -77,7 +79,8 @@ object CredentialSchemaLookupAndPaginationSpec _ <- deleteAllCredentialSchemas controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) inputs <- Generator.schemaInput.runCollectN(101) _ <- inputs diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaMultiTenancySpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaMultiTenancySpec.scala index 8737c4a1a5..c797e8a74a 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaMultiTenancySpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaMultiTenancySpec.scala @@ -3,7 +3,7 @@ package org.hyperledger.identus.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer import org.hyperledger.identus.agent.walletapi.model.Entity import org.hyperledger.identus.container.util.MigrationAspects.* -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaGuidNotFoundError +import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaUpdateError import org.hyperledger.identus.pollux.core.model.schema.`type`.CredentialJsonSchemaType import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.service.{CredentialSchemaService, CredentialSchemaServiceImpl} @@ -106,10 +106,10 @@ object CredentialSchemaMultiTenancySpec extends ZIOSpecDefault with CredentialSc .exit aliceCannotUpdateBobsVCSchema = assert(notFoundSchemaAError)( - fails(isSubtype[CredentialSchemaGuidNotFoundError](anything)) + fails(isSubtype[CredentialSchemaUpdateError](anything)) ) bobCannotUpdateAlicesVCSchema = assert(notFoundSchemaBError)( - fails(isSubtype[CredentialSchemaGuidNotFoundError](anything)) + fails(isSubtype[CredentialSchemaUpdateError](anything)) ) fetchedSchemaAbyB <- service.getByGUID(updatedSchemaA.guid).provideLayer(Bob.wacLayer) diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala index 2c3f148e54..5917f13f1c 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala @@ -1,6 +1,8 @@ package org.hyperledger.identus.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import com.typesafe.config.ConfigFactory +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.http.CustomServerInterceptors import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, ManagedDIDState, PublicationState} import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} @@ -31,6 +33,7 @@ import sttp.tapir.server.interceptor.CustomiseInterceptors import sttp.tapir.server.stub.TapirStubInterpreter import sttp.tapir.ztapir.RIOMonadError import zio.* +import zio.config.typesafe.TypesafeConfigProvider import zio.json.{DecoderOps, EncoderOps} import zio.json.ast.Json import zio.json.ast.Json.* @@ -65,13 +68,19 @@ trait CredentialSchemaTestTools extends PostgresTestContainerSupport { ) ) + val configLayer = ZLayer.fromZIO( + TypesafeConfigProvider + .fromTypesafeConfig(ConfigFactory.load()) + .load(AppConfig.config) + ) + val authenticatorLayer: TaskLayer[AuthenticatorWithAuthZ[BaseEntity]] = DefaultEntityAuthenticator.layer lazy val testEnvironmentLayer = ZLayer.makeSome[ ManagedDIDService, CredentialSchemaController & CredentialSchemaRepository & CredentialSchemaService & PostgreSQLContainer & - AuthenticatorWithAuthZ[BaseEntity] + AuthenticatorWithAuthZ[BaseEntity] & AppConfig ]( CredentialSchemaControllerImpl.layer, CredentialSchemaServiceImpl.layer, @@ -79,7 +88,8 @@ trait CredentialSchemaTestTools extends PostgresTestContainerSupport { contextAwareTransactorLayer, systemTransactorLayer, pgContainerLayer, - authenticatorLayer + authenticatorLayer, + configLayer ) val credentialSchemaUriBase = uri"http://test.com/schema-registry/schemas" @@ -91,22 +101,26 @@ trait CredentialSchemaTestTools extends PostgresTestContainerSupport { .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) } - def httpBackend(controller: CredentialSchemaController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { - val schemaRegistryEndpoints = SchemaRegistryServerEndpoints(controller, authenticator, authenticator) + def httpBackend( + config: AppConfig, + controller: CredentialSchemaController, + authenticator: AuthenticatorWithAuthZ[BaseEntity] + ) = { + val schemaRegistryEndpoints = SchemaRegistryServerEndpoints(config, controller, authenticator, authenticator) val backend = TapirStubInterpreter( bootstrapOptions(new RIOMonadError[Any]), SttpBackendStub(new RIOMonadError[Any]) ) - .whenServerEndpoint(schemaRegistryEndpoints.createSchemaServerEndpoint) + .whenServerEndpoint(schemaRegistryEndpoints.create.http) .thenRunLogic() - .whenServerEndpoint(schemaRegistryEndpoints.getSchemaByIdServerEndpoint) + .whenServerEndpoint(schemaRegistryEndpoints.get.http) .thenRunLogic() - .whenServerEndpoint(schemaRegistryEndpoints.getRawSchemaByIdServerEndpoint) + .whenServerEndpoint(schemaRegistryEndpoints.getRaw.http) .thenRunLogic() .whenServerEndpoint( - schemaRegistryEndpoints.lookupSchemasByQueryServerEndpoint + schemaRegistryEndpoints.getMany.http ) .thenRunLogic() .backend() @@ -179,11 +193,14 @@ trait CredentialSchemaGen { def generateSchemasN( count: Int - ): ZIO[CredentialSchemaController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[CredentialSchemaInput]] = + ): ZIO[CredentialSchemaController & AppConfig & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + CredentialSchemaInput + ]] = for { controller <- ZIO.service[CredentialSchemaController] authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] - backend = httpBackend(controller, authenticator) + config <- ZIO.service[AppConfig] + backend = httpBackend(config, controller, authenticator) inputs <- Generator.schemaInput.runCollectN(count) _ <- inputs .map(in => diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala index f9a5b68968..d3ca097e3d 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala @@ -7,6 +7,7 @@ import org.hyperledger.identus.castor.core.model.did.VerificationRelationship import org.hyperledger.identus.castor.core.service.MockDIDService import org.hyperledger.identus.iam.authentication.{AuthenticatorWithAuthZ, DefaultEntityAuthenticator} import org.hyperledger.identus.pollux.core.service.* +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.core.service.verification.{VcVerificationService, VcVerificationServiceImpl} import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} @@ -59,7 +60,7 @@ trait VcVerificationControllerTestTools extends PostgresTestContainerSupport { VcVerificationController & VcVerificationService & AuthenticatorWithAuthZ[BaseEntity] ]( didResolverLayer, - ResourceURIDereferencerImpl.layer, + ResourceUrlResolver.layer, VcVerificationControllerImpl.layer, VcVerificationServiceImpl.layer, DefaultEntityAuthenticator.layer diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala index 935a38244f..3964378fec 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala @@ -17,6 +17,8 @@ trait ManagedDIDService { private[walletapi] def nonSecretStorage: DIDNonSecretStorage + protected def getDefaultDidDocumentServices: Set[Service] = Set.empty + def syncManagedDIDState: ZIO[WalletAccessContext, GetManagedDIDError, Unit] def syncUnconfirmedUpdateOperations: ZIO[WalletAccessContext, GetManagedDIDError, Unit] diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala index 4e8763bdad..861f3a8b10 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala @@ -8,6 +8,7 @@ import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService.DEFAULT import org.hyperledger.identus.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage, WalletSecretStorage} import org.hyperledger.identus.agent.walletapi.util.* import org.hyperledger.identus.castor.core.model.did.* +import org.hyperledger.identus.castor.core.model.did.Service as DidDocumentService import org.hyperledger.identus.castor.core.model.error.DIDOperationError import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.castor.core.util.DIDOperationValidator @@ -24,6 +25,7 @@ import scala.language.implicitConversions * indy-wallet-sdk. */ class ManagedDIDServiceImpl private[walletapi] ( + defaultDidDocumentServices: Set[DidDocumentService], didService: DIDService, didOpValidator: DIDOperationValidator, private[walletapi] val secretStorage: DIDSecretStorage, @@ -54,6 +56,8 @@ class ManagedDIDServiceImpl private[walletapi] ( def syncUnconfirmedUpdateOperations: ZIO[WalletAccessContext, GetManagedDIDError, Unit] = syncUnconfirmedUpdateOperationsByDID(did = None) + override protected def getDefaultDidDocumentServices: Set[DidDocumentService] = defaultDidDocumentServices + override def findDIDKeyPair( did: CanonicalPrismDID, keyId: KeyId @@ -125,9 +129,16 @@ class ManagedDIDServiceImpl private[walletapi] ( ): ZIO[WalletAccessContext, CreateManagedDIDError, LongFormPrismDID] = { val effect = for { _ <- ZIO - .fromEither(ManagedDIDTemplateValidator.validate(didTemplate)) - .mapError(CreateManagedDIDError.InvalidArgument.apply) - material <- didCreateHandler.materialize(didTemplate) + .fromEither(ManagedDIDTemplateValidator.validate(didTemplate, defaultDidDocumentServices)) + .mapError { x => + println("x: " + x) + + CreateManagedDIDError.InvalidArgument(x) + } + _ <- ZIO.logInfo(s"Old did template after validation: $didTemplate") + newDidTemplate = didTemplate.copy(services = didTemplate.services ++ defaultDidDocumentServices) + _ <- ZIO.logInfo(s"Creating managed DID with template2: $newDidTemplate") + material <- didCreateHandler.materialize(newDidTemplate) _ <- ZIO .fromEither(didOpValidator.validate(material.operation)) .mapError(CreateManagedDIDError.InvalidOperation.apply) @@ -361,11 +372,13 @@ class ManagedDIDServiceImpl private[walletapi] ( object ManagedDIDServiceImpl { val layer: RLayer[ - DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & WalletSecretStorage & Apollo, + Set[DidDocumentService] & DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & + WalletSecretStorage & Apollo, ManagedDIDService ] = { ZLayer.fromZIO { for { + defaultDidDocumentServices <- ZIO.service[Set[DidDocumentService]] didService <- ZIO.service[DIDService] didOpValidator <- ZIO.service[DIDOperationValidator] secretStorage <- ZIO.service[DIDSecretStorage] @@ -374,6 +387,7 @@ object ManagedDIDServiceImpl { apollo <- ZIO.service[Apollo] createDIDSem <- Semaphore.make(1) } yield ManagedDIDServiceImpl( + defaultDidDocumentServices, didService, didOpValidator, secretStorage, diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala index 782ee6d9ec..6938ef3547 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala @@ -4,6 +4,7 @@ import org.hyperledger.identus.agent.walletapi.model.error.CommonWalletStorageEr import org.hyperledger.identus.agent.walletapi.model.ManagedDIDDetail import org.hyperledger.identus.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage, WalletSecretStorage} import org.hyperledger.identus.castor.core.model.did.CanonicalPrismDID +import org.hyperledger.identus.castor.core.model.did.Service as DidDocumentService import org.hyperledger.identus.castor.core.model.error.DIDOperationError import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.castor.core.util.DIDOperationValidator @@ -13,6 +14,7 @@ import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* class ManagedDIDServiceWithEventNotificationImpl( + defaultDidDocumentServices: Set[DidDocumentService], didService: DIDService, didOpValidator: DIDOperationValidator, override private[walletapi] val secretStorage: DIDSecretStorage, @@ -22,6 +24,7 @@ class ManagedDIDServiceWithEventNotificationImpl( createDIDSem: Semaphore, eventNotificationService: EventNotificationService ) extends ManagedDIDServiceImpl( + defaultDidDocumentServices, didService, didOpValidator, secretStorage, @@ -57,11 +60,12 @@ class ManagedDIDServiceWithEventNotificationImpl( object ManagedDIDServiceWithEventNotificationImpl { val layer: RLayer[ - DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & WalletSecretStorage & Apollo & - EventNotificationService, + Set[DidDocumentService] & DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & + WalletSecretStorage & Apollo & EventNotificationService, ManagedDIDService ] = ZLayer.fromZIO { for { + defaultDidDocumentServices <- ZIO.service[Set[DidDocumentService]] didService <- ZIO.service[DIDService] didOpValidator <- ZIO.service[DIDOperationValidator] secretStorage <- ZIO.service[DIDSecretStorage] @@ -71,6 +75,7 @@ object ManagedDIDServiceWithEventNotificationImpl { createDIDSem <- Semaphore.make(1) eventNotificationService <- ZIO.service[EventNotificationService] } yield ManagedDIDServiceWithEventNotificationImpl( + defaultDidDocumentServices, didService, didOpValidator, secretStorage, diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala index 82abee2b7f..3e9385ae86 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala @@ -3,15 +3,31 @@ package org.hyperledger.identus.agent.walletapi.util import org.hyperledger.identus.agent.walletapi.model.ManagedDIDTemplate import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.castor.core.model.did.{EllipticCurve, VerificationRelationship} +import org.hyperledger.identus.castor.core.model.did.Service as DidDocumentService object ManagedDIDTemplateValidator { - def validate(template: ManagedDIDTemplate): Either[String, Unit] = + def validate( + template: ManagedDIDTemplate, + defaultDidDocumentServices: Set[DidDocumentService] = Set.empty + ): Either[String, Unit] = for { _ <- validateReservedKeyId(template) _ <- validateCurveUsage(template) + _ <- validatePresenceOfDefaultDidServices(template.services, defaultDidDocumentServices) } yield () + private def validatePresenceOfDefaultDidServices( + services: Seq[DidDocumentService], + defaultDidDocumentServices: Set[DidDocumentService] + ): Either[String, Unit] = { + + services.map(_.id).intersect(defaultDidDocumentServices.toSeq.map(_.id)) match { + case Nil => Right(()) + case x => Left(s"Default DID services cannot be overridden: ${x.mkString("[", ", ", "]")}") + } + } + private def validateReservedKeyId(template: ManagedDIDTemplate): Either[String, Unit] = { val keyIds = template.publicKeys.map(_.id) val reservedKeyIds = keyIds.filter(id => ManagedDIDService.reservedKeyIds.contains(id)) diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala index 7950a38265..c32600a08a 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala @@ -11,6 +11,11 @@ import org.hyperledger.identus.agent.walletapi.sql.* import org.hyperledger.identus.agent.walletapi.storage.* import org.hyperledger.identus.agent.walletapi.vault.{VaultDIDSecretStorage, VaultWalletSecretStorage} import org.hyperledger.identus.castor.core.model.did.* +import org.hyperledger.identus.castor.core.model.did.{ + Service as DidDocumentService, + ServiceEndpoint as DidDocumentServiceEndpoint, + ServiceType as DidDocumentServiceType +} import org.hyperledger.identus.castor.core.model.error import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.castor.core.util.DIDOperationValidator @@ -82,21 +87,38 @@ object ManagedDIDServiceSpec ) private def serviceLayer = - ZLayer - .makeSome[ - DIDSecretStorage & WalletSecretStorage, - WalletManagementService & ManagedDIDService & TestDIDService - ]( - ManagedDIDServiceImpl.layer, - WalletManagementServiceImpl.layer, - DIDOperationValidator.layer(), - JdbcDIDNonSecretStorage.layer, - JdbcWalletNonSecretStorage.layer, - systemTransactorLayer, - contextAwareTransactorLayer, - testDIDServiceLayer, - apolloLayer - ) + ZLayer.succeed(Set(defaultDidDocumentServiceFixture)) >>> serviceLayerWithoutDidDocumentServices + + private def serviceLayerWithoutDidDocumentServices = ZLayer + .makeSome[ + Set[DidDocumentService] & DIDSecretStorage & WalletSecretStorage, + WalletManagementService & ManagedDIDService & TestDIDService + ]( + ManagedDIDServiceImpl.layer, + WalletManagementServiceImpl.layer, + DIDOperationValidator.layer(), + JdbcDIDNonSecretStorage.layer, + JdbcWalletNonSecretStorage.layer, + systemTransactorLayer, + contextAwareTransactorLayer, + testDIDServiceLayer, + apolloLayer + ) + + private val defaultDidDocumentServiceFixture = DidDocumentService( + id = "agent-base-url", + serviceEndpoint = DidDocumentServiceEndpoint + .Single( + DidDocumentServiceEndpoint.UriOrJsonEndpoint + .Uri( + DidDocumentServiceEndpoint.UriValue + .fromString("http://localhost:8085/") + .toOption + .get // This will fail if URL is invalid, which will prevent app from starting since public endpoint in config is invalid + ) + ), + `type` = DidDocumentServiceType.Single(DidDocumentServiceType.Name.fromStringUnsafe("LinkedResourceV1")) + ) private def generateDIDTemplate( publicKeys: Seq[DIDPublicKeyTemplate] = Nil, @@ -234,6 +256,15 @@ object ManagedDIDServiceSpec } yield assert(didsBefore)(isEmpty) && assert(didsAfter.map(_._1))(hasSameElements(Seq(did))) }, + test("will not create a DID if one of the provided servies includes default service") { + val template = generateDIDTemplate( + services = Seq( + defaultDidDocumentServiceFixture + ) + ) + val result = ZIO.serviceWithZIO[ManagedDIDService](_.createAndStoreDID(template)) + assertZIO(result.exit)(fails(isSubtype[CreateManagedDIDError.InvalidArgument](anything))) + }, test("create and store DID secret in DIDSecretStorage") { val template = generateDIDTemplate( publicKeys = Seq( diff --git a/docs/docusaurus/schemas/credential-schema.md b/docs/docusaurus/schemas/credential-schema.md index 0630211be9..56aff05830 100644 --- a/docs/docusaurus/schemas/credential-schema.md +++ b/docs/docusaurus/schemas/credential-schema.md @@ -62,11 +62,13 @@ The locally unique identifier of the schema. ### longId (String) -Resource identifier of the given credential schema composed from the author's [DID]((/docs/concepts/glossary#decentralized-identifier) reference, id, and version fields. +Resource identifier of the given credential schema composed from the author's DID reference, id, and version fields. **Example:** `{author}/{id}?version={version}` > **Note:** According to the [W3C specification](https://w3c-ccg.github.io/vc-json-schemas/#id), this field is locally unique and combines the Issuer `DID`, `uuid`, and `version`. -**For **example:** `did:example:MDP8AsFhHzhwUvGNuYkX7T/06e126d1-fa44-4882-a243-1e326fbe21db?version=1.0` + +**For example:** `did:example:MDP8AsFhHzhwUvGNuYkX7T/06e126d1-fa44-4882-a243-1e326fbe21db?version=1.0` + --- diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/ResourceResolutionMethod.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/ResourceResolutionMethod.scala new file mode 100644 index 0000000000..3562e47ab5 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/ResourceResolutionMethod.scala @@ -0,0 +1,17 @@ +package org.hyperledger.identus.pollux.core.model + +import sttp.tapir.Schema +import zio.json.* + +enum ResourceResolutionMethod { + case did + case http +} + +object ResourceResolutionMethod { + given schema: Schema[ResourceResolutionMethod] = Schema.derivedEnumeration.defaultStringBased + given encoder: JsonEncoder[ResourceResolutionMethod] = JsonEncoder[String].contramap(_.toString) + given decoder: JsonDecoder[ResourceResolutionMethod] = JsonDecoder[String].mapOrFail { s => + ResourceResolutionMethod.values.find(_.toString == s).toRight(s"Unknown ResourceResolutionMethod: $s") + } +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaServiceError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaServiceError.scala index 362e2aa59d..f2d19a3d17 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaServiceError.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaServiceError.scala @@ -19,6 +19,12 @@ final case class CredentialSchemaGuidNotFoundError(guid: UUID) s"Credential Schema record cannot be found by `guid`=$guid" ) +final case class CredentialSchemaIdNotFoundError(id: UUID) + extends CredentialSchemaServiceError( + StatusCode.NotFound, + s"Credential Schema record cannot be found by `id`=$id" + ) + final case class CredentialSchemaUpdateError(id: UUID, version: String, author: String, message: String) extends CredentialSchemaServiceError( StatusCode.BadRequest, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala index c71b88ede1..4f4a1d7eef 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala @@ -1,7 +1,7 @@ package org.hyperledger.identus.pollux.core.model.error import org.hyperledger.identus.pollux.core.model.DidCommID -import org.hyperledger.identus.pollux.core.service.URIDereferencerError +import org.hyperledger.identus.shared.http.GenericUriResolverError import org.hyperledger.identus.shared.json.JsonSchemaError import org.hyperledger.identus.shared.models.{Failure, StatusCode} @@ -56,25 +56,13 @@ object PresentationError { "Issued credential not found" ) - final case class InvalidSchemaURIError(schemaUri: String, error: Throwable) - extends PresentationError( - StatusCode.BadRequest, - s"Invalid Schema Uri: $schemaUri, Error: ${error.getMessage}" - ) - - final case class InvalidCredentialDefinitionURIError(credentialDefinitionUri: String, error: Throwable) - extends PresentationError( - StatusCode.BadRequest, - s"Invalid Credential Definition Uri: $credentialDefinitionUri, Error: ${error.getMessage}" - ) - - final case class SchemaURIDereferencingError(error: URIDereferencerError) + final case class SchemaURIDereferencingError(error: GenericUriResolverError) extends PresentationError( error.statusCode, error.userFacingMessage ) - final case class CredentialDefinitionURIDereferencingError(error: URIDereferencerError) + final case class CredentialDefinitionURIDereferencingError(error: GenericUriResolverError) extends PresentationError( error.statusCode, error.userFacingMessage diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialDefinition.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialDefinition.scala index d6d97a45d6..64e715a722 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialDefinition.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialDefinition.scala @@ -1,7 +1,7 @@ package org.hyperledger.identus.pollux.core.model.schema -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod.* import zio.* import zio.json.* @@ -56,7 +56,8 @@ case class CredentialDefinition( keyCorrectnessProofJsonSchemaId: String, keyCorrectnessProof: CorrectnessProof, signatureType: String, - supportRevocation: Boolean + supportRevocation: Boolean, + resolutionMethod: ResourceResolutionMethod ) { def longId = CredentialDefinition.makeLongId(author, guid, version) } @@ -85,11 +86,12 @@ object CredentialDefinition { definitionSchemaId: String, definition: Definition, proofSchemaId: String, - proof: CorrectnessProof + proof: CorrectnessProof, + resolutionMethod: ResourceResolutionMethod ): UIO[CredentialDefinition] = { for { id <- zio.Random.nextUUID - cs <- make(id, in, definitionSchemaId, definition, proofSchemaId, proof) + cs <- make(id, in, definitionSchemaId, definition, proofSchemaId, proof, resolutionMethod) } yield cs } @@ -99,7 +101,8 @@ object CredentialDefinition { definitionSchemaId: String, definition: Definition, keyCorrectnessProofSchemaId: String, - keyCorrectnessProof: CorrectnessProof + keyCorrectnessProof: CorrectnessProof, + resolutionMethod: ResourceResolutionMethod ): UIO[CredentialDefinition] = { for { ts <- zio.Clock.currentDateTime.map( @@ -121,7 +124,8 @@ object CredentialDefinition { keyCorrectnessProofJsonSchemaId = keyCorrectnessProofSchemaId, keyCorrectnessProof = keyCorrectnessProof, signatureType = in.signatureType, - supportRevocation = in.supportRevocation + supportRevocation = in.supportRevocation, + resolutionMethod = resolutionMethod ) } @@ -143,7 +147,8 @@ object CredentialDefinition { author: Option[String] = None, name: Option[String] = None, version: Option[String] = None, - tag: Option[String] = None + tag: Option[String] = None, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) case class FilteredEntries(entries: Seq[CredentialDefinition], count: Long, totalCount: Long) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala index 809f0b1c88..d81c821369 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala @@ -8,7 +8,8 @@ import org.hyperledger.identus.pollux.core.model.schema.`type`.{ CredentialSchemaType } import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 -import org.hyperledger.identus.pollux.core.service.URIDereferencer +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.json.{JsonSchemaValidator, JsonSchemaValidatorImpl} import zio.* import zio.json.* @@ -52,6 +53,7 @@ case class CredentialSchema( tags: Seq[String], description: String, `type`: String, + resolutionMethod: ResourceResolutionMethod, schema: Schema ) { def longId = CredentialSchema.makeLongId(author, id, version) @@ -65,13 +67,13 @@ object CredentialSchema { def makeGUID(author: String, id: UUID, version: String) = UUID.nameUUIDFromBytes(makeLongId(author, id, version).getBytes) - def make(in: Input): UIO[CredentialSchema] = { + def make(in: Input, resolutionMethod: ResourceResolutionMethod): UIO[CredentialSchema] = { for { id <- zio.Random.nextUUID - cs <- make(id, in) + cs <- make(id, in, resolutionMethod) } yield cs } - def make(id: UUID, in: Input): UIO[CredentialSchema] = { + def make(id: UUID, in: Input, resolutionMethod: ResourceResolutionMethod): UIO[CredentialSchema] = { for { ts <- zio.Clock.currentDateTime.map( _.atZoneSameInstant(ZoneOffset.UTC).toOffsetDateTime @@ -87,6 +89,7 @@ object CredentialSchema { tags = in.tags, description = in.description, `type` = in.`type`, + resolutionMethod = resolutionMethod, schema = in.schema ) } @@ -108,7 +111,8 @@ object CredentialSchema { author: Option[String] = None, name: Option[String] = None, version: Option[String] = None, - tags: Option[String] = None + tags: Option[String] = None, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) case class FilteredEntries(entries: Seq[CredentialSchema], count: Long, totalCount: Long) @@ -116,9 +120,9 @@ object CredentialSchema { given JsonEncoder[CredentialSchema] = DeriveJsonEncoder.gen[CredentialSchema] given JsonDecoder[CredentialSchema] = DeriveJsonDecoder.gen[CredentialSchema] - def resolveJWTSchema(uri: URI, uriDereferencer: URIDereferencer): IO[CredentialSchemaParsingError, Json] = { + def resolveJWTSchema(uri: URI, uriResolver: UriResolver): IO[CredentialSchemaParsingError, Json] = { for { - content <- uriDereferencer.dereference(uri).orDieAsUnmanagedFailure + content <- uriResolver.resolve(uri.toString).orDieAsUnmanagedFailure json <- ZIO .fromEither(content.fromJson[Json]) .mapError(error => CredentialSchemaParsingError(error)) @@ -127,11 +131,11 @@ object CredentialSchema { def validSchemaValidator( schemaId: String, - uriDereferencer: URIDereferencer + uriResolver: UriResolver ): IO[InvalidURI | CredentialSchemaParsingError, JsonSchemaValidator] = { for { uri <- ZIO.attempt(new URI(schemaId)).mapError(_ => InvalidURI(schemaId)) - json <- resolveJWTSchema(uri, uriDereferencer) + json <- resolveJWTSchema(uri, uriResolver) schemaValidator <- JsonSchemaValidatorImpl .from(json) .orElse( @@ -148,10 +152,10 @@ object CredentialSchema { def validateJWTCredentialSubject( schemaId: String, credentialSubject: String, - uriDereferencer: URIDereferencer + uriResolver: UriResolver ): IO[InvalidURI | CredentialSchemaParsingError | CredentialSchemaValidationError, Unit] = { for { - schemaValidator <- validSchemaValidator(schemaId, uriDereferencer) + schemaValidator <- validSchemaValidator(schemaId, uriResolver) _ <- schemaValidator.validate(credentialSubject).mapError(CredentialSchemaValidationError.apply) } yield () } @@ -159,11 +163,10 @@ object CredentialSchema { def validateAnonCredsClaims( schemaId: String, claims: String, - uriDereferencer: URIDereferencer + uriResolver: UriResolver ): IO[InvalidURI | CredentialSchemaParsingError | VCClaimsParsingError | VCClaimValidationError, Unit] = { for { - uri <- ZIO.attempt(new URI(schemaId)).mapError(_ => InvalidURI(schemaId)) - content <- uriDereferencer.dereference(uri).orDieAsUnmanagedFailure + content <- uriResolver.resolve(schemaId).orDieAsUnmanagedFailure validAttrNames <- AnoncredSchemaSerDesV1.schemaSerDes .deserialize(content) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepository.scala index bcf889d2fd..49c08b7d1f 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepository.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.pollux.core.repository import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.Repository.SearchCapability import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{UIO, URIO} @@ -12,7 +13,7 @@ trait CredentialDefinitionRepository with SearchCapability[WalletTask, CredentialDefinition.Filter, CredentialDefinition] { def create(cs: CredentialDefinition): URIO[WalletAccessContext, CredentialDefinition] - def findByGuid(guid: UUID): UIO[Option[CredentialDefinition]] + def findByGuid(guid: UUID, resolutionMethod: ResourceResolutionMethod): UIO[Option[CredentialDefinition]] def update(cs: CredentialDefinition): URIO[WalletAccessContext, CredentialDefinition] diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialSchemaRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialSchemaRepository.scala index d3fa782ba8..dfd2938e01 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialSchemaRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialSchemaRepository.scala @@ -2,6 +2,7 @@ package org.hyperledger.identus.pollux.core.repository import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.Repository.SearchCapability import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{UIO, URIO} @@ -13,11 +14,15 @@ trait CredentialSchemaRepository with SearchCapability[WalletTask, CredentialSchema.Filter, CredentialSchema] { def create(cs: CredentialSchema): URIO[WalletAccessContext, CredentialSchema] - def findByGuid(guid: UUID): UIO[Option[CredentialSchema]] + def findByGuid(guid: UUID, resolutionMethod: ResourceResolutionMethod): UIO[Option[CredentialSchema]] def update(cs: CredentialSchema): URIO[WalletAccessContext, CredentialSchema] - def getAllVersions(id: UUID, author: String): URIO[WalletAccessContext, List[String]] + def getAllVersions( + id: UUID, + author: String, + resolutionMethod: ResourceResolutionMethod + ): URIO[WalletAccessContext, List[CredentialSchema]] def delete(guid: UUID): URIO[WalletAccessContext, CredentialSchema] diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala index 4732fc7ddd..6a86509592 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala @@ -22,7 +22,7 @@ trait CredentialStatusListRepository { def createNewForTheWallet( jwtIssuer: Issuer, - statusListRegistryUrl: String + statusListRegistryServiceName: String ): URIO[WalletAccessContext, CredentialStatusList] def allocateSpaceForCredential( diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionService.scala index 1aecbbe7a4..d73752a670 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionService.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.core.service import org.hyperledger.identus.pollux.core.model.error.CredentialDefinitionServiceError import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{IO, ZIO} @@ -16,16 +17,20 @@ trait CredentialDefinitionService { * @return * Created instance of the Credential Definition */ - def create(in: Input): Result[CredentialDefinition] + def create( + in: Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): Result[CredentialDefinition] /** @param guid * Globally unique UUID of the credential definition * @return * The instance of the credential definition or credential service error */ - def getByGUID(guid: UUID): IO[CredentialDefinitionServiceError, CredentialDefinition] - - def delete(guid: UUID): Result[CredentialDefinition] + def getByGUID( + guid: UUID, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): IO[CredentialDefinitionServiceError, CredentialDefinition] def lookup(filter: Filter, skip: Int, limit: Int): Result[FilteredEntries] } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala index cf6a468b21..17c521bd43 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala @@ -12,13 +12,13 @@ import org.hyperledger.identus.pollux.core.model.error.{ } import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.{ CredentialSchemaParsingError, - CredentialSchemaValidationError, - InvalidURI + CredentialSchemaValidationError } import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition.{Filter, FilteredEntries} import org.hyperledger.identus.pollux.core.model.secret.CredentialDefinitionSecret +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.CredentialDefinitionRepository import org.hyperledger.identus.pollux.core.repository.Repository.SearchQuery import org.hyperledger.identus.pollux.core.service.serdes.{ @@ -26,23 +26,25 @@ import org.hyperledger.identus.pollux.core.service.serdes.{ ProofKeyCredentialDefinitionSchemaSerDesV1, PublicCredentialDefinitionSerDesV1 } +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.json.JsonSchemaError import zio.* -import java.net.URI import java.util.UUID import scala.util.Try class CredentialDefinitionServiceImpl( genericSecretStorage: GenericSecretStorage, credentialDefinitionRepository: CredentialDefinitionRepository, - uriDereferencer: URIDereferencer + uriResolver: UriResolver ) extends CredentialDefinitionService { - override def create(in: CredentialDefinition.Input): Result[CredentialDefinition] = { + override def create( + in: CredentialDefinition.Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): Result[CredentialDefinition] = { for { - uri <- ZIO.attempt(new URI(in.schemaId)).mapError(error => InvalidURI(in.schemaId)).orDieAsUnmanagedFailure - content <- uriDereferencer.dereference(uri).orDieAsUnmanagedFailure + content <- uriResolver.resolve(in.schemaId).orDieAsUnmanagedFailure anoncredSchema <- AnoncredSchemaSerDesV1.schemaSerDes .deserialize(content) .mapError(error => CredentialSchemaParsingError(error.error)) @@ -85,7 +87,8 @@ class CredentialDefinitionServiceImpl( PublicCredentialDefinitionSerDesV1.version, publicCredentialDefinitionJson, ProofKeyCredentialDefinitionSchemaSerDesV1.version, - proofKeyCredentialDefinitionJson + proofKeyCredentialDefinitionJson, + resolutionMethod ) createdCredentialDefinition <- credentialDefinitionRepository.create(cd) _ <- genericSecretStorage @@ -103,30 +106,26 @@ class CredentialDefinitionServiceImpl( case error: CredentialDefinitionCreationError => error } - override def delete(guid: UUID): Result[CredentialDefinition] = + override def getByGUID( + guid: UUID, + resolutionMethod: ResourceResolutionMethod + ): IO[CredentialDefinitionServiceError, CredentialDefinition] = { for { - existingOpt <- credentialDefinitionRepository.findByGuid(guid) - _ <- ZIO.fromOption(existingOpt).mapError(_ => CredentialDefinitionGuidNotFoundError(guid)) - result <- credentialDefinitionRepository.delete(guid) + resultOpt <- credentialDefinitionRepository.findByGuid(guid, resolutionMethod) + result <- ZIO.fromOption(resultOpt).mapError(_ => CredentialDefinitionGuidNotFoundError(guid)) } yield result + } override def lookup(filter: CredentialDefinition.Filter, skip: Int, limit: Int): Result[FilteredEntries] = { credentialDefinitionRepository .search(SearchQuery(filter, skip, limit)) .map(sr => FilteredEntries(sr.entries, sr.count.toInt, sr.totalCount.toInt)) } - - override def getByGUID(guid: UUID): IO[CredentialDefinitionServiceError, CredentialDefinition] = { - for { - resultOpt <- credentialDefinitionRepository.findByGuid(guid) - result <- ZIO.fromOption(resultOpt).mapError(_ => CredentialDefinitionGuidNotFoundError(guid)) - } yield result - } } object CredentialDefinitionServiceImpl { val layer: URLayer[ - GenericSecretStorage & CredentialDefinitionRepository & URIDereferencer, + GenericSecretStorage & CredentialDefinitionRepository & UriResolver, CredentialDefinitionService ] = ZLayer.fromFunction(CredentialDefinitionServiceImpl(_, _, _)) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaService.scala index 1fdb69d04e..922d87300f 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaService.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.core.service import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaServiceError import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{IO, ZIO} @@ -15,18 +16,26 @@ trait CredentialSchemaService { * @return * Created instance of the Credential Schema */ - def create(in: Input): Result[CredentialSchema] + def create( + in: Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): Result[CredentialSchema] /** @param guid * Globally unique UUID of the credential schema * @return * The instance of the credential schema or credential service error */ - def getByGUID(guid: UUID): IO[CredentialSchemaServiceError, CredentialSchema] + def getByGUID( + guid: UUID, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): IO[CredentialSchemaServiceError, CredentialSchema] - def update(id: UUID, in: Input): Result[CredentialSchema] - - def delete(id: UUID): Result[CredentialSchema] + def update( + id: UUID, + in: Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): Result[CredentialSchema] def lookup(filter: Filter, skip: Int, limit: Int): Result[FilteredEntries] } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaServiceImpl.scala index ec96d7d678..4854371764 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialSchemaServiceImpl.scala @@ -3,12 +3,14 @@ package org.hyperledger.identus.pollux.core.service import org.hyperledger.identus.pollux.core.model.error.{ CredentialSchemaError, CredentialSchemaGuidNotFoundError, + CredentialSchemaIdNotFoundError, CredentialSchemaServiceError, CredentialSchemaUpdateError, CredentialSchemaValidationError } import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema.FilteredEntries +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.CredentialSchemaRepository import org.hyperledger.identus.pollux.core.repository.Repository.SearchQuery import zio.* @@ -18,9 +20,12 @@ import java.util.UUID class CredentialSchemaServiceImpl( credentialSchemaRepository: CredentialSchemaRepository ) extends CredentialSchemaService { - override def create(in: CredentialSchema.Input): Result[CredentialSchema] = { + override def create( + in: CredentialSchema.Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): Result[CredentialSchema] = { for { - credentialSchema <- CredentialSchema.make(in) + credentialSchema <- CredentialSchema.make(in, resolutionMethod) _ <- CredentialSchema.validateCredentialSchema(credentialSchema) createdCredentialSchema <- credentialSchemaRepository.create(credentialSchema) } yield createdCredentialSchema @@ -28,9 +33,12 @@ class CredentialSchemaServiceImpl( CredentialSchemaValidationError(e) } - override def getByGUID(guid: UUID): IO[CredentialSchemaServiceError, CredentialSchema] = { + override def getByGUID( + guid: UUID, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): IO[CredentialSchemaServiceError, CredentialSchema] = { for { - resultOpt <- credentialSchemaRepository.findByGuid(guid) + resultOpt <- credentialSchemaRepository.findByGuid(guid, resolutionMethod) result <- ZIO.fromOption(resultOpt).mapError(_ => CredentialSchemaGuidNotFoundError(guid)) } yield result } @@ -38,25 +46,40 @@ class CredentialSchemaServiceImpl( def getBy( author: String, id: UUID, - version: String + version: String, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ): Result[CredentialSchema] = { - getByGUID(CredentialSchema.makeGUID(author, id, version)) + getByGUID(CredentialSchema.makeGUID(author, id, version), resolutionMethod) } override def update( - guid: UUID, - in: CredentialSchema.Input + id: UUID, + in: CredentialSchema.Input, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ): Result[CredentialSchema] = { + for { - cs <- CredentialSchema.make(guid, in) + existingVersions <- credentialSchemaRepository.getAllVersions(id, in.author, resolutionMethod) + _ <- + if existingVersions.isEmpty then + ZIO.fail( + CredentialSchemaUpdateError( + id, + in.version, + in.author, + s"No Schema exists of author: ${in.author}, with provided id: $id" + ) + ) + else ZIO.unit + resolutionMethod = existingVersions.head.resolutionMethod + cs <- CredentialSchema.make(id, in, resolutionMethod) _ <- CredentialSchema.validateCredentialSchema(cs).mapError(CredentialSchemaValidationError.apply) - existingVersions <- credentialSchemaRepository.getAllVersions(guid, in.author) - _ <- ZIO.fromOption(existingVersions.headOption).mapError(_ => CredentialSchemaGuidNotFoundError(guid)) - _ <- existingVersions.find(_ > in.version) match { + _ <- ZIO.fromOption(existingVersions.headOption).mapError(_ => CredentialSchemaIdNotFoundError(id)) + _ <- existingVersions.find(_.version > in.version) match { case Some(higherVersion) => ZIO.fail( CredentialSchemaUpdateError( - guid, + id, in.version, in.author, s"Higher version is found: $higherVersion" @@ -65,11 +88,11 @@ class CredentialSchemaServiceImpl( case None => ZIO.succeed(cs) } - _ <- existingVersions.find(_ == in.version) match { + _ <- existingVersions.find(_.version == in.version) match { case Some(existingVersion) => ZIO.fail( CredentialSchemaUpdateError( - guid, + id, in.version, in.author, s"The version already exists: $existingVersion" @@ -81,17 +104,10 @@ class CredentialSchemaServiceImpl( } yield updated } - override def delete(guid: UUID): Result[CredentialSchema] = - for { - existingOpt <- credentialSchemaRepository.findByGuid(guid) - _ <- ZIO.fromOption(existingOpt).mapError(_ => CredentialSchemaGuidNotFoundError(guid)) - result <- credentialSchemaRepository.delete(guid) - } yield result - override def lookup( filter: CredentialSchema.Filter, skip: Int, - limit: Int + limit: Int, ): Result[CredentialSchema.FilteredEntries] = { credentialSchemaRepository .search(SearchQuery(filter, skip, limit)) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala index 1e7fb43a4b..3414f8a351 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala @@ -133,7 +133,7 @@ trait CredentialService { def generateJWTCredential( recordId: DidCommID, - statusListRegistryUrl: String, + statusListRegistryServiceName: String, ): ZIO[WalletAccessContext, RecordNotFound | CredentialRequestValidationFailed, IssueCredentialRecord] def generateSDJWTCredential( diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala index 0063b8715a..a95ead74b0 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala @@ -4,6 +4,7 @@ import cats.implicits.* import io.circe.* import io.circe.parser.* import io.circe.syntax.* +import io.circe.Json import org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, PublicationState} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage @@ -27,7 +28,7 @@ import org.hyperledger.identus.pollux.prex.{ClaimFormat, Jwt, PresentationDefini import org.hyperledger.identus.pollux.sdjwt.* import org.hyperledger.identus.pollux.vc.jwt.{Issuer as JwtIssuer, *} import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Secp256k1KeyPair} -import org.hyperledger.identus.shared.http.{DataUrlResolver, GenericUriResolver} +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils @@ -35,14 +36,13 @@ import zio.* import zio.json.* import zio.prelude.ZValidation -import java.net.URI import java.time.{Instant, ZoneId} import java.util.UUID import scala.language.implicitConversions object CredentialServiceImpl { val layer: URLayer[ - CredentialRepository & CredentialStatusListRepository & DidResolver & URIDereferencer & GenericSecretStorage & + CredentialRepository & CredentialStatusListRepository & DidResolver & UriResolver & GenericSecretStorage & CredentialDefinitionService & LinkSecretService & DIDService & ManagedDIDService, CredentialService ] = { @@ -51,7 +51,7 @@ object CredentialServiceImpl { credentialRepo <- ZIO.service[CredentialRepository] credentialStatusListRepo <- ZIO.service[CredentialStatusListRepository] didResolver <- ZIO.service[DidResolver] - uriDereferencer <- ZIO.service[URIDereferencer] + uriResolver <- ZIO.service[UriResolver] genericSecretStorage <- ZIO.service[GenericSecretStorage] credDefenitionService <- ZIO.service[CredentialDefinitionService] linkSecretService <- ZIO.service[LinkSecretService] @@ -62,7 +62,7 @@ object CredentialServiceImpl { credentialRepo, credentialStatusListRepo, didResolver, - uriDereferencer, + uriResolver, genericSecretStorage, credDefenitionService, linkSecretService, @@ -82,7 +82,7 @@ class CredentialServiceImpl( credentialRepository: CredentialRepository, credentialStatusListRepository: CredentialStatusListRepository, didResolver: DidResolver, - uriDereferencer: URIDereferencer, + uriResolver: UriResolver, genericSecretStorage: GenericSecretStorage, credentialDefinitionService: CredentialDefinitionService, linkSecretService: LinkSecretService, @@ -304,7 +304,11 @@ class CredentialServiceImpl( for { credentialDefinition <- getCredentialDefinition(credentialDefinitionGUID) _ <- CredentialSchema - .validateAnonCredsClaims(credentialDefinition.schemaId, claims.noSpaces, uriDereferencer) + .validateAnonCredsClaims( + credentialDefinition.schemaId, + claims.noSpaces, + uriResolver, + ) .orDieAsUnmanagedFailure attributes <- CredentialService.convertJsonClaimsToAttributes(claims) offer <- createAnonCredsDidCommOfferCredential( @@ -446,7 +450,7 @@ class CredentialServiceImpl( .collectAll( schemaIds.map(schemaId => CredentialSchema - .validateJWTCredentialSubject(schemaId, claims.noSpaces, uriDereferencer) + .validateJWTCredentialSubject(schemaId, claims.noSpaces, uriResolver) ) ) .orDieAsUnmanagedFailure @@ -724,8 +728,8 @@ class CredentialServiceImpl( ) .orDieWith(_ => RuntimeException(s"No AnonCreds attachment found in the offer")) credentialOffer = anoncreds.AnoncredCredentialOffer(attachmentData) - credDefContent <- uriDereferencer - .dereference(new URI(credentialOffer.getCredDefId)) + credDefContent <- uriResolver + .resolve(credentialOffer.getCredDefId) .orDieAsUnmanagedFailure credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent) linkSecret <- linkSecretService.fetchOrCreate() @@ -849,8 +853,8 @@ class CredentialServiceImpl( ): URIO[WalletAccessContext, anoncreds.AnoncredCredential] = { for { credential <- ZIO.succeed(anoncreds.AnoncredCredential(new String(credentialBytes))) - credDefContent <- uriDereferencer - .dereference(new URI(credential.getCredDefId)) + credDefContent <- uriResolver + .resolve(credential.getCredDefId) .orDieAsUnmanagedFailure credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent) metadata <- ZIO @@ -1432,12 +1436,6 @@ class CredentialServiceImpl( ZIO.fail(CredentialRequestValidationFailed("domain/challenge proof validation failed")) clock = java.time.Clock.system(ZoneId.systemDefault) - - genericUriResolver = GenericUriResolver( - Map( - "data" -> DataUrlResolver(), - ) - ) verificationResult <- JwtPresentation .verify( jwt, @@ -1447,7 +1445,7 @@ class CredentialServiceImpl( verifyDates = false, leeway = Duration.Zero ) - )(didResolver, genericUriResolver)(clock) + )(didResolver, uriResolver)(clock) .mapError(errors => CredentialRequestValidationFailed(errors*)) result <- verificationResult match diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala new file mode 100644 index 0000000000..74d6fa0131 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala @@ -0,0 +1,30 @@ +package org.hyperledger.identus.pollux.core.service + +import org.hyperledger.identus.pollux.core.service.uriResolvers.* +import org.hyperledger.identus.pollux.vc.jwt.DidResolver +import org.hyperledger.identus.shared.http.{GenericUriResolver, GenericUriResolverError, UriResolver} +import org.hyperledger.identus.shared.http.DataUrlResolver +import zio.* +import zio.http.* + +class GenericUriResolverImpl(client: Client, didResolver: DidResolver) extends UriResolver { + private val httpUrlResolver = HttpUrlResolver(client) + private val genericUriResolver = new GenericUriResolver( + Map( + "http" -> httpUrlResolver, + "https" -> httpUrlResolver, + "data" -> DataUrlResolver(), + "resource" -> ResourceUrlResolver(Map.empty), + "did" -> DidUrlResolver(httpUrlResolver, didResolver) + ) + ) + override def resolve(uri: String): IO[GenericUriResolverError, String] = { + + genericUriResolver.resolve(uri) + } + +} + +object GenericUriResolverImpl { + val layer: URLayer[Client & DidResolver, UriResolver] = ZLayer.fromFunction(GenericUriResolverImpl(_, _)) +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/HttpURIDereferencerImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/HttpURIDereferencerImpl.scala deleted file mode 100644 index b38aaaf8ba..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/HttpURIDereferencerImpl.scala +++ /dev/null @@ -1,41 +0,0 @@ -package org.hyperledger.identus.pollux.core.service - -import org.hyperledger.identus.pollux.core.service.URIDereferencerError.* -import zio.* -import zio.http.* - -import java.net.URI -import java.nio.charset.StandardCharsets - -class HttpURIDereferencerImpl(client: Client) extends URIDereferencer { - - override def dereference(uri: URI): IO[URIDereferencerError, String] = { - val program = for { - url <- ZIO.fromOption(URL.fromURI(uri)).mapError(_ => InvalidURI(uri)) - response <- client - .request(Request(url = url)) - .mapError(t => ConnectionError(t.getMessage)) - body <- response.status match { - case Status.Ok => - response.body.asString.mapError(t => ResponseProcessingError(t.getMessage)) - case Status.NotFound => - ZIO.fail(ResourceNotFound(uri)) - case status if status.isError => - response.body.asStream - .take(1024) // Only take the first 1024 bytes from the response body (if any). - .runCollect - .map(c => new String(c.toArray, StandardCharsets.UTF_8)) - .orDie - .flatMap(errorMessage => ZIO.fail(UnexpectedUpstreamResponseReceived(status.code, Some(errorMessage)))) - case status => - ZIO.fail(UnexpectedUpstreamResponseReceived(status.code)) - } - } yield body - program.provideSomeLayer(zio.Scope.default) - } - -} - -object HttpURIDereferencerImpl { - val layer: URLayer[Client, URIDereferencer] = ZLayer.fromFunction(HttpURIDereferencerImpl(_)) -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala index c0db44819d..532000c850 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala @@ -12,6 +12,7 @@ import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataServiceE UnsupportedCredentialFormat } import org.hyperledger.identus.shared.db.Errors.UnexpectedAffectedRow +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.{Failure, StatusCode, WalletAccessContext} import zio.* @@ -81,7 +82,7 @@ trait OID4VCIIssuerMetadataService { ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, Unit] } -class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataRepository, uriDereferencer: URIDereferencer) +class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataRepository, uriResolver: UriResolver) extends OID4VCIIssuerMetadataService { override def createCredentialIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, CredentialIssuer] = @@ -140,7 +141,7 @@ class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataReposito } schemaUri <- ZIO.attempt(new URI(schemaId)).mapError(t => InvalidSchemaId(schemaId, t.getMessage)) _ <- CredentialSchema - .validSchemaValidator(schemaUri.toString(), uriDereferencer) + .validSchemaValidator(schemaUri.toString(), uriResolver) .catchAll { case e: InvalidURI => ZIO.fail(InvalidSchemaId(schemaId, e.userFacingMessage)) case e: CredentialSchemaParsingError => ZIO.fail(InvalidSchemaId(schemaId, e.cause)) @@ -184,7 +185,7 @@ class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataReposito } object OID4VCIIssuerMetadataServiceImpl { - def layer: URLayer[OID4VCIIssuerMetadataRepository & URIDereferencer, OID4VCIIssuerMetadataService] = { + def layer: URLayer[OID4VCIIssuerMetadataRepository & UriResolver, OID4VCIIssuerMetadataService] = { ZLayer.fromFunction(OID4VCIIssuerMetadataServiceImpl(_, _)) } } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala index 55c41fe35a..8200595ebf 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala @@ -19,13 +19,13 @@ import org.hyperledger.identus.pollux.core.repository.{CredentialRepository, Pre import org.hyperledger.identus.pollux.core.service.serdes.* import org.hyperledger.identus.pollux.sdjwt.{CredentialCompact, HolderPrivateKey, PresentationCompact, SDJWT} import org.hyperledger.identus.pollux.vc.jwt.* +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils import zio.* import zio.json.* -import java.net.URI import java.time.Instant import java.util.{Base64 as JBase64, UUID} import java.util as ju @@ -33,7 +33,7 @@ import scala.util.chaining.* import scala.util.Try private class PresentationServiceImpl( - uriDereferencer: URIDereferencer, + uriResolver: UriResolver, linkSecretService: LinkSecretService, presentationRepository: PresentationRepository, credentialRepository: CredentialRepository, @@ -765,8 +765,7 @@ private class PresentationServiceImpl( private def resolveSchema(schemaUri: String): IO[PresentationError, (String, AnoncredSchemaDef)] = { for { - uri <- ZIO.attempt(new URI(schemaUri)).mapError(e => InvalidSchemaURIError(schemaUri, e)) - content <- uriDereferencer.dereference(uri).mapError(e => SchemaURIDereferencingError(e)) + content <- uriResolver.resolve(schemaUri).mapError(e => SchemaURIDereferencingError(e)) anoncredSchema <- AnoncredSchemaSerDesV1.schemaSerDes .deserialize(content) @@ -785,10 +784,9 @@ private class PresentationServiceImpl( credentialDefinitionUri: String ): IO[PresentationError, (String, AnoncredCredentialDefinition)] = { for { - uri <- ZIO - .attempt(new URI(credentialDefinitionUri)) - .mapError(e => InvalidCredentialDefinitionURIError(credentialDefinitionUri, e)) - content <- uriDereferencer.dereference(uri).mapError(e => CredentialDefinitionURIDereferencingError(e)) + content <- uriResolver + .resolve(credentialDefinitionUri) + .mapError(e => CredentialDefinitionURIDereferencingError(e)) _ <- PublicCredentialDefinitionSerDesV1.schemaSerDes .validate(content) @@ -1307,7 +1305,7 @@ private class PresentationServiceImpl( object PresentationServiceImpl { val layer: URLayer[ - URIDereferencer & LinkSecretService & PresentationRepository & CredentialRepository, + UriResolver & LinkSecretService & PresentationRepository & CredentialRepository, PresentationService ] = ZLayer.fromFunction(PresentationServiceImpl(_, _, _, _)) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/ResourceURIDereferencerImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/ResourceURIDereferencerImpl.scala deleted file mode 100644 index 7b79ed8f2c..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/ResourceURIDereferencerImpl.scala +++ /dev/null @@ -1,37 +0,0 @@ -package org.hyperledger.identus.pollux.core.service - -import org.hyperledger.identus.pollux.core.service.URIDereferencerError.ResourceNotFound -import zio.* - -import java.net.URI - -class ResourceURIDereferencerImpl(extraResources: Map[String, String]) extends URIDereferencer { - - override def dereference(uri: URI): IO[URIDereferencerError, String] = { - for { - scheme <- ZIO.succeed(uri.getScheme) - body <- scheme match - case "resource" => - val inputStream = this.getClass.getResourceAsStream(uri.getPath) - if (inputStream != null) - val content = scala.io.Source.fromInputStream(inputStream).mkString - inputStream.close() - ZIO.succeed(content) - else ZIO.fail(ResourceNotFound(uri)) - case _ => - extraResources - .get(uri.toString) - .map(ZIO.succeed(_)) - .getOrElse(ZIO.fail(ResourceNotFound(uri))) - } yield body - } - -} - -object ResourceURIDereferencerImpl { - def layer: ULayer[ResourceURIDereferencerImpl] = - ZLayer.succeed(new ResourceURIDereferencerImpl(Map.empty)) - - def layerWithExtraResources: URLayer[Map[String, String], ResourceURIDereferencerImpl] = - ZLayer.fromFunction(ResourceURIDereferencerImpl(_)) -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/URIDereferencer.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/URIDereferencer.scala deleted file mode 100644 index fa80836744..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/URIDereferencer.scala +++ /dev/null @@ -1,49 +0,0 @@ -package org.hyperledger.identus.pollux.core.service - -import org.hyperledger.identus.shared.models.{Failure, StatusCode} -import zio.IO - -import java.net.URI - -trait URIDereferencer { - def dereference(uri: URI): IO[URIDereferencerError, String] -} - -sealed trait URIDereferencerError( - val statusCode: StatusCode, - val userFacingMessage: String -) extends Failure { - override val namespace: String = "URIDereferencer" -} - -object URIDereferencerError { - final case class InvalidURI(uri: URI) - extends URIDereferencerError( - StatusCode.UnprocessableContent, - s"The URI to dereference is invalid: uri=[$uri]" - ) - - final case class ConnectionError(cause: String) - extends URIDereferencerError( - StatusCode.BadGateway, - s"An error occurred while connecting to the URI's underlying server: cause=[$cause]" - ) - - final case class ResourceNotFound(uri: URI) - extends URIDereferencerError( - StatusCode.NotFound, - s"The resource was not found on the URI's underlying server: uri=[$uri]" - ) - - final case class ResponseProcessingError(cause: String) - extends URIDereferencerError( - StatusCode.BadGateway, - s"An error occurred while processing the URI's underlying server response: cause=[$cause]" - ) - - final case class UnexpectedUpstreamResponseReceived(status: Int, content: Option[String] = None) - extends URIDereferencerError( - StatusCode.BadGateway, - s"An unexpected response was received from the URI's underlying server: status=[$status], content=[${content.getOrElse("n/a")}]" - ) -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala new file mode 100644 index 0000000000..b252d35b1a --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala @@ -0,0 +1,132 @@ +package org.hyperledger.identus.pollux.core.service.uriResolvers + +import io.lemonlabs.uri.{Url, UrlPath} +import org.hyperledger.identus.pollux.vc.jwt +import org.hyperledger.identus.pollux.vc.jwt.* +import org.hyperledger.identus.shared.crypto.Sha256Hash +import org.hyperledger.identus.shared.http.{GenericUriResolverError, UriResolver} +import org.hyperledger.identus.shared.models.PrismEnvelopeData +import org.hyperledger.identus.shared.models.StatusCode +import zio.* +import zio.json.* + +class DidUrlResolver(httpUrlResolver: HttpUrlResolver, didResolver: DidResolver) extends UriResolver { + import DidUrlResolver.* + + def resolve(uri: String): IO[GenericUriResolverError, String] = { + + for { + parsed <- ZIO.fromTry(Url.parseTry(uri)).mapError(_ => InvalidURI(uri)) + maybeResourceService = parsed.query.param("resourceService") + maybeResourcePath = parsed.query.param("resourcePath") + maybeResourceHash = parsed.query.param("resourceHash") + serviceAndPath <- ZIO + .fromOption(maybeResourceService zip maybeResourcePath) + .mapError(_ => MissingRequiredParams(uri)) + (resourceService, resourcePath) = serviceAndPath + didStr = parsed.removeQueryString().toString + didDocument <- didResolver.resolve(didStr).flatMap { + case DIDResolutionFailed(err) => + err match + case InvalidDid(message) => ZIO.fail(DidResolutionError(didStr, message)) + case NotFound(message) => ZIO.fail(DidResolutionError(didStr, message)) + case RepresentationNotSupported(message) => ZIO.fail(DidResolutionError(didStr, message)) + case InvalidPublicKeyLength(message) => ZIO.fail(DidResolutionError(didStr, message)) + case InvalidPublicKeyType(message) => ZIO.fail(DidResolutionError(didStr, message)) + case UnsupportedPublicKeyType(message) => ZIO.fail(DidResolutionError(didStr, message)) + case jwt.Error(error, message) => ZIO.fail(DidResolutionError(didStr, message)) + case DIDResolutionSucceeded(didDocument, didDocumentMetadata) => ZIO.succeed(didDocument) + } + service <- ZIO + .fromOption( + didDocument.service.find(x => x.id == s"$didStr#$resourceService" && x.`type` == "LinkedResourceV1") + ) + .mapError(_ => + DidDocumentParsingError( + s"""Service with id: "$resourceService" and type: "LinkedResourceV1" not found inside DID document""" + ) + ) + baseUrl <- ZIO + .fromOption(service.serviceEndpoint.asString) + .mapError(_ => DidDocumentParsingError("serviceEndpoint is not a string")) + + path <- ZIO.fromOption(UrlPath.parseOption(resourcePath)).mapError(_ => InvalidUrlPath(resourcePath)) + finalUrl <- ZIO + .fromTry(Url.parseTry(baseUrl).map(x => x.withPath(path)).map(_.toString)) + .mapError(_ => InvalidURI(baseUrl)) + result <- httpUrlResolver.resolve(finalUrl) + + validatedResult <- result.fromJson[PrismEnvelopeData] match { + case Right(env) => validateResourceIntegrity(env, maybeResourceHash) + case Left(err) => + ZIO.debug("Error parsing response as PrismEnvelope. Falling back to plain json") *> ZIO.succeed(result) + } + + } yield validatedResult + + } + + private def validateResourceIntegrity( + envelope: PrismEnvelopeData, + maybeResourceHash: Option[String] + ): IO[DidUrlResolverError, String] = { + val envelopeAsStr = envelope.toJson + maybeResourceHash.fold(ZIO.succeed(envelopeAsStr)) { hash => + val computedHash = Sha256Hash.compute(envelope.resource.getBytes()).hexEncoded + if (computedHash == hash) ZIO.succeed(envelopeAsStr) + else ZIO.fail(InvalidHash(hash, computedHash)) + } + } + +} + +object DidUrlResolver { + + class DidUrlResolverError(statusCode: StatusCode, userFacingMessage: String) + extends GenericUriResolverError(statusCode, userFacingMessage) + + final case class InvalidURI(uri: String) + extends DidUrlResolverError( + StatusCode.UnprocessableContent, + s"The URI to resolve is invalid: uri=[$uri]" + ) + + final case class InvalidUrlPath(path: String) + extends DidUrlResolverError( + StatusCode.UnprocessableContent, + s"Invalid URL path: $path" + ) + + final case class MissingRequiredParams(url: String) + extends DidUrlResolverError( + StatusCode.UnprocessableContent, + s"DID URL must have resourcePath and resourceService query parameters, got invalid URL: $url" + ) + + final case class DidResolutionError(didStr: String, reason: String) + extends DidUrlResolverError( + StatusCode.InternalServerError, + s"Error resolving DID: $didStr, error: $reason" + ) + + final case class DidDocumentParsingError(customMessage: String) + extends DidUrlResolverError( + StatusCode.InternalServerError, + s"Error parsing DID document: $customMessage" + ) + + final case class InvalidHash(expectedHash: String, computedHash: String) + extends DidUrlResolverError( + StatusCode.UnprocessableContent, + s"Invalid hash, expected: $expectedHash, computed: $computedHash" + ) + + final case class InvalidResponseFromResourceServer(resourceUrl: String) + extends DidUrlResolverError( + StatusCode.UnprocessableContent, + s"Invalid Json response, expected an envelope, resourceUrl: $resourceUrl" + ) + + val layer: URLayer[HttpUrlResolver & DidResolver, DidUrlResolver] = + ZLayer.fromFunction(DidUrlResolver(_, _)) +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/HttpUrlResolver.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/HttpUrlResolver.scala new file mode 100644 index 0000000000..535311b648 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/HttpUrlResolver.scala @@ -0,0 +1,75 @@ +package org.hyperledger.identus.pollux.core.service.uriResolvers + +import org.hyperledger.identus.shared.http.{GenericUriResolverError, UriResolver} +import org.hyperledger.identus.shared.models.StatusCode +import zio.* +import zio.http.* + +import java.nio.charset.StandardCharsets + +class HttpUrlResolver(client: Client) extends UriResolver { + import HttpUriResolver.* + override def resolve(uri: String): IO[GenericUriResolverError, String] = { + val program = for { + url <- ZIO.fromEither(URL.decode(uri)).mapError(_ => InvalidURI(uri)) + response <- client + .request(Request(url = url)) + .mapError(t => ConnectionError(t.getMessage)) + body <- response.status match { + case Status.Ok => + response.body.asString.mapError(t => ResponseProcessingError(t.getMessage)) + case Status.NotFound => + ZIO.fail(ResourceNotFound(uri)) + case status if status.isError => + response.body.asStream + .take(1024) // Only take the first 1024 bytes from the response body (if any). + .runCollect + .map(c => new String(c.toArray, StandardCharsets.UTF_8)) + .orDie + .flatMap(errorMessage => ZIO.fail(UnexpectedUpstreamResponseReceived(status.code, Some(errorMessage)))) + case status => + ZIO.fail(UnexpectedUpstreamResponseReceived(status.code)) + } + } yield body + program.provideSomeLayer(zio.Scope.default) + } + +} + +object HttpUriResolver { + val layer: URLayer[Client, HttpUrlResolver] = ZLayer.fromFunction(HttpUrlResolver(_)) + + class HttpUriResolverError(statusCode: StatusCode, userFacingMessage: String) + extends GenericUriResolverError(statusCode, userFacingMessage) + + final case class InvalidURI(uri: String) + extends HttpUriResolverError( + StatusCode.UnprocessableContent, + s"The URI to resolve is invalid: uri=[$uri]" + ) + + final case class ConnectionError(cause: String) + extends HttpUriResolverError( + StatusCode.BadGateway, + s"An error occurred while connecting to the URI's underlying server: cause=[$cause]" + ) + + final case class ResourceNotFound(uri: String) + extends HttpUriResolverError( + StatusCode.NotFound, + s"The resource was not found on the URI's underlying server: uri=[$uri]" + ) + + final case class ResponseProcessingError(cause: String) + extends HttpUriResolverError( + StatusCode.BadGateway, + s"An error occurred while processing the URI's underlying server response: cause=[$cause]" + ) + + final case class UnexpectedUpstreamResponseReceived(status: Int, content: Option[String] = None) + extends HttpUriResolverError( + StatusCode.BadGateway, + s"An unexpected response was received from the URI's underlying server: status=[$status], content=[${content.getOrElse("n/a")}]" + ) + +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/ResourceUrlResolver.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/ResourceUrlResolver.scala new file mode 100644 index 0000000000..0e95320ed8 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/ResourceUrlResolver.scala @@ -0,0 +1,57 @@ +package org.hyperledger.identus.pollux.core.service.uriResolvers + +import org.hyperledger.identus.shared.http.{GenericUriResolverError, InvalidUri, UriResolver} +import org.hyperledger.identus.shared.models.StatusCode +import zio.* + +import java.net.URI +import scala.util.Try + +class ResourceUrlResolver(extraResources: Map[String, String]) extends UriResolver { + import ResourceUrlResolver.* + + def resolve(uri: String): IO[GenericUriResolverError, String] = { + for { + javaUri <- ZIO.fromTry(Try(URI(uri))).mapError(_ => InvalidUri(uri)) + scheme <- ZIO.succeed(javaUri.getScheme) + body <- scheme match + case "resource" => + val inputStream = this.getClass.getResourceAsStream(javaUri.getPath) + if (inputStream != null) + val content = scala.io.Source.fromInputStream(inputStream).mkString + inputStream.close() + ZIO.succeed(content) + else ZIO.fail(ResourceNotFound(uri)) + case _ => + extraResources + .get(uri) + .map(ZIO.succeed(_)) + .getOrElse(ZIO.fail(ResourceNotFound(uri))) + } yield body + + } + +} + +class ResourceUrlResolverError(statusCode: StatusCode, userFacingMessage: String) + extends GenericUriResolverError(statusCode, userFacingMessage) + +object ResourceUrlResolver { + def layer: ULayer[ResourceUrlResolver] = + ZLayer.succeed(new ResourceUrlResolver(Map.empty)) + + def layerWithExtraResources: URLayer[Map[String, String], ResourceUrlResolver] = + ZLayer.fromFunction(ResourceUrlResolver(_)) + + final case class InvalidURI(uri: String) + extends ResourceUrlResolverError( + StatusCode.UnprocessableContent, + s"The URI to resolve is invalid: uri=[$uri]" + ) + + final case class ResourceNotFound(uri: String) + extends ResourceUrlResolverError( + StatusCode.NotFound, + s"The resource was not found on the URI's underlying server: uri=[$uri]" + ) +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala index 5cb0fbc27a..2d43eb120f 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala @@ -1,7 +1,6 @@ package org.hyperledger.identus.pollux.core.service.verification import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema -import org.hyperledger.identus.pollux.core.service.URIDereferencer import org.hyperledger.identus.pollux.vc.jwt.{ CredentialPayload, CredentialSchema as JwtCredentialSchema, @@ -10,13 +9,12 @@ import org.hyperledger.identus.pollux.vc.jwt.{ JWTVerification, JwtCredential } -import org.hyperledger.identus.pollux.vc.jwt.CredentialPayload.Implicits +import org.hyperledger.identus.shared.http.UriResolver import zio.* import java.time.OffsetDateTime -class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDereferencer) - extends VcVerificationService { +class VcVerificationServiceImpl(didResolver: DidResolver, uriResolver: UriResolver) extends VcVerificationService { override def verify( vcVerificationRequests: List[VcVerificationRequest] ): IO[VcVerificationServiceError, List[VcVerificationResult]] = { @@ -56,7 +54,7 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe decodedJwt <- JwtCredential .decodeJwt(JWT(credential)) - .mapError(error => VcVerificationServiceError.UnexpectedError(s"Unable decode JWT: $error")) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Unable to decode JWT: $error")) credentialSchema <- ZIO .fromOption(decodedJwt.maybeCredentialSchema) @@ -71,7 +69,7 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe CredentialSchema .validSchemaValidator( credentialSchema.id, - uriDereferencer + uriResolver ) .mapError(error => VcVerificationServiceError.UnexpectedError(s"Schema Validator Failed: $error")) ) @@ -119,7 +117,7 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe .validateJWTCredentialSubject( credentialSchema.id, CredentialPayload.Implicits.jwtVcEncoder(decodedJwt.vc).noSpaces, - uriDereferencer + uriResolver ) .mapError(error => VcVerificationServiceError.UnexpectedError(s"JWT Credential Subject Validation Failed: $error") @@ -281,6 +279,6 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe } object VcVerificationServiceImpl { - val layer: URLayer[DidResolver & URIDereferencer, VcVerificationService] = + val layer: URLayer[DidResolver & UriResolver, VcVerificationService] = ZLayer.fromFunction(VcVerificationServiceImpl(_, _)) } diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala index 341565de22..940db21325 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala @@ -5,6 +5,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.Cre import org.hyperledger.identus.pollux.core.model.schema.`type`.{AnoncredSchemaType, CredentialJsonSchemaType} import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 import org.hyperledger.identus.pollux.core.model.schema.AnoncredSchemaTypeSpec.test +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.json.JsonSchemaError.JsonValidationErrors import zio.json.* import zio.json.ast.Json @@ -29,6 +30,7 @@ object CredentialSchemaSpec extends ZIOSpecDefault { tags = Seq("tag1", "tag2"), description = "Json Schema", `type` = CredentialJsonSchemaType.VC_JSON_SCHEMA_URI, + resolutionMethod = ResourceResolutionMethod.http, schema = innerJsonSchema.fromJson[Json].getOrElse(Json.Null) ) } @@ -44,6 +46,7 @@ object CredentialSchemaSpec extends ZIOSpecDefault { tags = Seq("tag1", "tag2"), description = "Anoncred Schema", `type` = AnoncredSchemaSerDesV1.version, + resolutionMethod = ResourceResolutionMethod.http, schema = innerJsonSchema.fromJson[Json].getOrElse(Json.Null) ) } diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala index 1b444499c4..439a3de513 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialDefinitionRepositoryInMemory.scala @@ -38,15 +38,20 @@ class CredentialDefinitionRepositoryInMemory( } yield record } - override def findByGuid(guid: UUID): UIO[Option[CredentialDefinition]] = { + override def findByGuid( + guid: UUID, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http + ): UIO[Option[CredentialDefinition]] = { for { storeRefs <- walletRefs.get - storeRefOption <- ZIO.filter(storeRefs.values)(storeRef => storeRef.get.map(_.contains(guid))).map(_.headOption) + storeRefOption <- ZIO + .filter(storeRefs.values)(storeRef => storeRef.get.map(x => x.contains(guid))) + .map(_.headOption) record <- storeRefOption match { case Some(storeRef) => storeRef.get.map(_.get(guid)) case None => ZIO.none } - } yield record + } yield record.fold(None)(x => if x.resolutionMethod == resolutionMethod then Some(x) else None) } override def update(cs: CredentialDefinition): URIO[WalletAccessContext, CredentialDefinition] = { diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala index d86e8a5d95..8ea5ca21d1 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala @@ -99,6 +99,8 @@ class CredentialStatusListRepositoryInMemory( case DecodingError(message) => new Throwable(message) case IndexOutOfBounds(message) => new Throwable(message) } + resourcePath = + s"credential-status/$id" emptyJwtCredential <- VCStatusList2021 .build( vcId = s"$statusListRegistryUrl/credential-status/$id", diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala index 35353652ab..88ada5aa74 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceSpecHelper.scala @@ -4,6 +4,7 @@ import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemo import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition import org.hyperledger.identus.pollux.core.repository.CredentialDefinitionRepositoryInMemory +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.models.WalletId.* import zio.* @@ -15,7 +16,7 @@ trait CredentialDefinitionServiceSpecHelper { protected val defaultWalletLayer = ZLayer.succeed(WalletAccessContext(WalletId.default)) protected val credentialDefinitionServiceLayer = - GenericSecretStorageInMemory.layer ++ CredentialDefinitionRepositoryInMemory.layer ++ ResourceURIDereferencerImpl.layer >>> + GenericSecretStorageInMemory.layer ++ CredentialDefinitionRepositoryInMemory.layer ++ ResourceUrlResolver.layer >>> CredentialDefinitionServiceImpl.layer ++ defaultWalletLayer val defaultDefinition = diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala index 5e35878bb4..0e15ad2d55 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala @@ -14,6 +14,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError.* import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.{ProtocolState, Role} +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.{CredentialIssuer, JWT, JwtCredential, JwtCredentialPayload} import org.hyperledger.identus.shared.models.{KeyId, UnmanagedFailureException, WalletAccessContext, WalletId} import zio.* @@ -33,7 +34,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS ).provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> credentialServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ) @@ -522,7 +523,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS // Issuer generates credential credentialGenerateRecord <- issuerSvc.generateJWTCredential( issuerRecordId, - "https://test-status-list.registry" + "status-list-registry" ) decodedJWT <- credentialGenerateRecord.issueCredentialData.get.attachments.head.data match { case MyBase64(value) => @@ -590,7 +591,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS .service[CredentialService] .provideSomeLayer( holderCredDefResolverLayer >>> - ResourceURIDereferencerImpl.layerWithExtraResources >>> + ResourceUrlResolver.layerWithExtraResources >>> credentialServiceLayer ) offerCredential <- ZIO.fromEither(OfferCredential.readFromMessage(msg)) diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifierSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifierSpec.scala index 3e3c5bc6b3..fd71889562 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifierSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifierSpec.scala @@ -101,7 +101,7 @@ object CredentialServiceNotifierSpec extends MockSpecDefault with CredentialServ _ <- svc.markOfferSent(issuerRecordId) _ <- svc.receiveCredentialRequest(requestCredential()) _ <- svc.acceptCredentialRequest(issuerRecordId) - _ <- svc.generateJWTCredential(issuerRecordId, "https://test-status-list.registry") + _ <- svc.generateJWTCredential(issuerRecordId, "status-list-registry") _ <- svc.markCredentialSent(issuerRecordId) consumer <- ens.consumer[IssueCredentialRecord]("Issue") events <- consumer.poll(50) diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala index ed0641edb4..a03eb88803 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala @@ -17,6 +17,7 @@ import org.hyperledger.identus.pollux.core.repository.{ } import org.hyperledger.identus.pollux.prex.{ClaimFormat, Ldp, PresentationDefinition} import org.hyperledger.identus.pollux.vc.jwt.* +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -32,8 +33,8 @@ trait CredentialServiceSpecHelper { CredentialDefinitionRepositoryInMemory.layer >>> CredentialDefinitionServiceImpl.layer protected val credentialServiceLayer - : URLayer[DIDService & ManagedDIDService & URIDereferencer, CredentialService & CredentialDefinitionService] = - ZLayer.makeSome[DIDService & ManagedDIDService & URIDereferencer, CredentialService & CredentialDefinitionService]( + : URLayer[DIDService & ManagedDIDService & UriResolver, CredentialService & CredentialDefinitionService] = + ZLayer.makeSome[DIDService & ManagedDIDService & UriResolver, CredentialService & CredentialDefinitionService]( CredentialRepositoryInMemory.layer, CredentialStatusListRepositoryInMemory.layer, ZLayer.fromFunction(PrismDidResolver(_)), diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala index 55a4e82d55..eff67638bc 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala @@ -252,9 +252,9 @@ object MockCredentialService extends Mock[CredentialService] { override def generateJWTCredential( recordId: DidCommID, - statusListRegistryUrl: String, + statusListRegistryServiceName: String, ): ZIO[WalletAccessContext, RecordNotFound | CredentialRequestValidationFailed, IssueCredentialRecord] = - proxy(GenerateJWTCredential, recordId, statusListRegistryUrl) + proxy(GenerateJWTCredential, recordId, statusListRegistryServiceName) override def generateSDJWTCredential( recordId: DidCommID, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala index b92b00d6d4..e438d4687b 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala @@ -10,8 +10,10 @@ import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.model.error.PresentationError import org.hyperledger.identus.pollux.core.repository.* import org.hyperledger.identus.pollux.core.service.serdes.* +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.crypto.KmpSecp256k1KeyOps +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -26,18 +28,18 @@ trait PresentationServiceSpecHelper { AgentPeerService.makeLayer(PeerDID.makePeerDid(serviceEndpoint = Some("http://localhost:9099"))) val genericSecretStorageLayer = GenericSecretStorageInMemory.layer - val uriDereferencerLayer = ResourceURIDereferencerImpl.layer + val uriResolverLayer = ResourceUrlResolver.layer val credentialDefLayer = - CredentialDefinitionRepositoryInMemory.layer ++ uriDereferencerLayer >>> CredentialDefinitionServiceImpl.layer + CredentialDefinitionRepositoryInMemory.layer ++ uriResolverLayer >>> CredentialDefinitionServiceImpl.layer val linkSecretLayer = genericSecretStorageLayer >+> LinkSecretServiceImpl.layer val presentationServiceLayer = ZLayer.make[ - PresentationService & CredentialDefinitionService & URIDereferencer & LinkSecretService & PresentationRepository & + PresentationService & CredentialDefinitionService & UriResolver & LinkSecretService & PresentationRepository & CredentialRepository ]( PresentationServiceImpl.layer, credentialDefLayer, - uriDereferencerLayer, + uriResolverLayer, linkSecretLayer, PresentationRepositoryInMemory.layer, CredentialRepositoryInMemory.layer diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolverSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolverSpec.scala new file mode 100644 index 0000000000..8d24db9ec9 --- /dev/null +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolverSpec.scala @@ -0,0 +1,192 @@ +package org.hyperledger.identus.pollux.core.service.uriResolvers + +import io.circe.* +import io.lemonlabs.uri.Url +import org.hyperledger.identus.pollux.vc.jwt.* +import org.hyperledger.identus.shared.crypto.Sha256Hash +import org.hyperledger.identus.shared.json.Json as JsonUtils +import org.hyperledger.identus.shared.models.PrismEnvelopeData +import org.hyperledger.identus.shared.utils.Base64Utils +import zio.* +import zio.json.* +import zio.test.* +import zio.test.Assertion.* + +import java.time.Instant + +object DidUrlResolverSpec extends ZIOSpecDefault { + + private val schema = """ + |{ + | "guid":"ef3e4135-8fcf-3ce7-b5bb-df37defc13f6", + | "id":"e33a6de7-1f93-404f-9f12-9bd7b397fd2c", + | "longId":"did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a/e33a6de7-1f93-404f-9f12-9bd7b397fd2c?version=1.0.0", + | "name":"driving-license", + | "version":"1.0.0", + | "tags":[ + | "driving", + | "license" + | ], + | "description":"Driving License Schema", + | "type":"https://w3c-ccg.github.io/vc-json-schemas/schema/2.0/schema.json", + | "schema":{ + | "$id":"https://example.com/driving-license-1.0.0", + | "$schema":"https://json-schema.org/draft/2020-12/schema", + | "description":"Driving License", + | "type":"object", + | "properties":{ + | "emailAddress":{ + | "type":"string", + | "format":"email" + | }, + | "givenName":{ + | "type":"string" + | }, + | "familyName":{ + | "type":"string" + | }, + | "dateOfIssuance":{ + | "type":"string", + | "format":"date-time" + | }, + | "drivingLicenseID":{ + | "type":"string" + | }, + | "drivingClass":{ + | "type":"integer" + | } + | }, + | "required":[ + | "emailAddress", + | "familyName", + | "dateOfIssuance", + | "drivingLicenseID", + | "drivingClass" + | ], + | "additionalProperties":true + | }, + | "author":"did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a", + | "authored":"2024-06-20T15:17:41.049526Z", + | "kind":"CredentialSchema", + | "self":"/schema-registry/schemas/ef3e4135-8fcf-3ce7-b5bb-df37defc13f6" + |} + |""".stripMargin + + private val normalizedSchema = JsonUtils.canonicalizeToJcs(schema).toOption.get + private val encodedSchema = Base64Utils.encodeURL(normalizedSchema.getBytes) + + private val schemaHash = Sha256Hash.compute(encodedSchema.getBytes()).hexEncoded + + private val testDidUrl = Url + .parse( + s"did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a?resourceService=agent-base-url&resourcePath=schema-registry/schemas/did-url/ef3e4135-8fcf-3ce7-b5bb-df37defc13f6&resourceHash=$schemaHash" + ) + .toString + + class MockHttpUrlResolver extends HttpUrlResolver(null) { + // Mock implementation, always resolves some schema + override def resolve(uri: String) = { + + val responseEnvelope = PrismEnvelopeData( + resource = encodedSchema, + url = uri + ) + + ZIO.succeed(responseEnvelope.toJson) + + } + } + + private val didResolverLayer = ZLayer.succeed(new DidResolver { + // mock implementation, always resolves the same DID + override def resolve(didUrl: String) = ZIO.succeed( + DIDResolutionSucceeded( + DIDDocument( + id = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a", + alsoKnowAs = Vector.empty[String], + controller = Vector("did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a"), + verificationMethod = Vector( + VerificationMethod( + id = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a#auth-1", + `type` = "JsonWebKey2020", + controller = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a", + publicKeyBase58 = Option.empty, + publicKeyBase64 = Option.empty, + publicKeyJwk = Some( + JsonWebKey( + crv = Some("secp256k1"), + x = Some("HFmBco2W7GT7n-JTx6R0Cd3fV0GpOxuWWC0Uu-B4vik"), + y = Some("1wwJuzZ4e898lWyLjwHi3H83602JI-8ErcWt08czqfI"), + kty = "EC" + ) + ), + publicKeyHex = Option.empty, + publicKeyMultibase = Option.empty, + blockchainAccountId = Option.empty, + ethereumAddress = Option.empty + ), + VerificationMethod( + id = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a#issue-1", + `type` = "JsonWebKey2020", + controller = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a", + publicKeyBase58 = Option.empty, + publicKeyBase64 = Option.empty, + publicKeyJwk = Some( + JsonWebKey( + crv = Some("secp256k1"), + x = Some("CXIFl2R18ameLD-ykSOGKQoCBVbFM5oulkc2vIrJtS4"), + y = Some("D2QYNi6-A9z1lxpRjKbocKSTvNAIsNVslBjlzegYyUA"), + kty = "EC" + ) + ), + publicKeyHex = Option.empty, + publicKeyMultibase = Option.empty, + blockchainAccountId = Option.empty, + ethereumAddress = Option.empty + ) + ), + authentication = Vector( + "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a#auth-1" + ), + assertionMethod = Vector( + "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a#issue-1" + ), + service = Vector( + Service( + id = "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a#agent-base-url", + `type` = "LinkedResourceV1", + serviceEndpoint = Json.fromString("https://agent-url.com") + ) + ) + ), + DIDDocumentMetadata( + created = Some(Instant.parse("2024-06-20T15:16:39Z")), + updated = Some(Instant.parse("2024-06-20T15:16:39Z")), + deactivated = Some(false) + ) + ) + ) + }) + private val httpUrlResolver = ZLayer.succeed(new MockHttpUrlResolver) + + override def spec = { + suite("DidUrlResolverSpec")( + test("Should resolve a DID url correctly") { + for { + didResolver <- ZIO.service[DidResolver] + httpUrlResolver <- ZIO.service[HttpUrlResolver] + didUrlResolver = new DidUrlResolver(httpUrlResolver, didResolver) + response <- didUrlResolver.resolve(testDidUrl) + responseEnvelope <- ZIO.fromEither(response.fromJson[PrismEnvelopeData]) + } yield { + assert(responseEnvelope.url)( + equalTo(testDidUrl) + ) + assert(responseEnvelope.resource)( + equalTo(encodedSchema) + ) + } + } + ).provide(didResolverLayer, httpUrlResolver) + } +} diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala index 3b458cc6df..25daf11202 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala @@ -4,7 +4,7 @@ import io.circe.* import io.circe.syntax.* import org.hyperledger.identus.agent.walletapi.service.MockManagedDIDService import org.hyperledger.identus.castor.core.service.MockDIDService -import org.hyperledger.identus.pollux.core.service.ResourceURIDereferencerImpl +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.pollux.vc.jwt.CredentialPayload.Implicits.* import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} @@ -79,7 +79,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -144,7 +144,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -209,7 +209,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( issuerDidServiceExpectations.toLayer ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -281,7 +281,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -350,7 +350,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -419,7 +419,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -497,7 +497,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -575,7 +575,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -641,7 +641,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -707,7 +707,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -773,7 +773,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), @@ -839,7 +839,7 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS }.provideSomeLayer( MockDIDService.empty ++ MockManagedDIDService.empty ++ - ResourceURIDereferencerImpl.layer >+> + ResourceUrlResolver.layer >+> someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ) diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala index c2f52148ea..73e9c21021 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala @@ -3,8 +3,9 @@ package org.hyperledger.identus.pollux.core.service.verification import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} import org.hyperledger.identus.castor.core.model.did.VerificationRelationship import org.hyperledger.identus.castor.core.service.{DIDService, MockDIDService} -import org.hyperledger.identus.pollux.core.service.{ResourceURIDereferencerImpl, URIDereferencer} +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.* +import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.models.WalletId.* import zio.* @@ -37,12 +38,12 @@ trait VcVerificationServiceSpecHelper { MockManagedDIDService.empty >>> ZLayer.fromFunction(PrismDidResolver(_)) protected val vcVerificationServiceLayer: ZLayer[Any, Nothing, VcVerificationService & WalletAccessContext] = - emptyDidResolverLayer ++ ResourceURIDereferencerImpl.layer >>> + emptyDidResolverLayer ++ ResourceUrlResolver.layer >>> VcVerificationServiceImpl.layer ++ defaultWalletLayer protected val someVcVerificationServiceLayer - : URLayer[DIDService & ManagedDIDService & URIDereferencer, VcVerificationService] = - ZLayer.makeSome[DIDService & ManagedDIDService & URIDereferencer, VcVerificationService]( + : URLayer[DIDService & ManagedDIDService & UriResolver, VcVerificationService] = + ZLayer.makeSome[DIDService & ManagedDIDService & UriResolver, VcVerificationService]( ZLayer.fromFunction(PrismDidResolver(_)), VcVerificationServiceImpl.layer ) diff --git a/pollux/sql-doobie/src/main/resources/sql/pollux/V29__add_resolution_method_to_schema_and_cred_definition.sql b/pollux/sql-doobie/src/main/resources/sql/pollux/V29__add_resolution_method_to_schema_and_cred_definition.sql new file mode 100644 index 0000000000..a85df4838b --- /dev/null +++ b/pollux/sql-doobie/src/main/resources/sql/pollux/V29__add_resolution_method_to_schema_and_cred_definition.sql @@ -0,0 +1,10 @@ +-- Create the enum type +CREATE TYPE resolution_method_enum AS ENUM ('http', 'did'); + +-- Add the column to credential_definition table +ALTER TABLE public.credential_definition + ADD COLUMN resolution_method resolution_method_enum NOT NULL DEFAULT 'http'; + +-- Add the column to credential_schema table +ALTER TABLE public.credential_schema + ADD COLUMN resolution_method resolution_method_enum NOT NULL DEFAULT 'http'; diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialDefinition.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialDefinition.scala index 8f82f8e1e8..75c54e4eac 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialDefinition.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialDefinition.scala @@ -5,6 +5,7 @@ import io.getquill.context.json.PostgresJsonExtensions import io.getquill.doobie.DoobieContext import io.getquill.idiom.* import org.hyperledger.identus.pollux.core.model.schema.{CorrectnessProof, Definition} +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.models.WalletId import java.time.temporal.ChronoUnit @@ -27,6 +28,7 @@ case class CredentialDefinition( keyCorrectnessProof: JsonValue[CorrectnessProof], signatureType: String, supportRevocation: Boolean, + resolutionMethod: ResourceResolutionMethod, walletId: WalletId ) { lazy val uniqueConstraintKey = author + name + version @@ -56,6 +58,7 @@ object CredentialDefinition { schemaId = m.schemaId, signatureType = m.signatureType, supportRevocation = m.supportRevocation, + resolutionMethod = m.resolutionMethod, walletId = walletId ) @@ -77,12 +80,17 @@ object CredentialDefinition { keyCorrectnessProof = db.keyCorrectnessProof.value, schemaId = db.schemaId, signatureType = db.signatureType, - supportRevocation = db.supportRevocation + supportRevocation = db.supportRevocation, + resolutionMethod = db.resolutionMethod ) } } -object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with PostgresJsonExtensions { +object CredentialDefinitionSql + extends DoobieContext.Postgres(SnakeCase) + with PostgresJsonExtensions + with PostgresEnumEncoders { + def insert(credentialDefinition: CredentialDefinition) = run { quote( query[CredentialDefinition] @@ -90,8 +98,13 @@ object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with Po ).returning(cs => cs) } - def findByGUID(guid: UUID) = run { - quote(query[CredentialDefinition].filter(_.guid == lift(guid)).take(1)) + def findByGUID(guid: UUID, resolutionMethod: ResourceResolutionMethod) = run { + quote( + query[CredentialDefinition] + .filter(_.guid == lift(guid)) + .filter(_.resolutionMethod == lift(resolutionMethod)) + .take(1) + ) } def findByID(id: UUID) = run { @@ -143,7 +156,8 @@ object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with Po authorOpt: Option[String] = None, nameOpt: Option[String] = None, versionOpt: Option[String] = None, - tagOpt: Option[String] = None + tagOpt: Option[String] = None, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) = run { val q = idOpt.fold(quote(query[CredentialDefinition]))(id => @@ -158,6 +172,7 @@ object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with Po tagOpt .fold(quote(true))(tag => quote(cs.tags.contains(lift(tag)))) ) + .filter(_.resolutionMethod == lift(resolutionMethod)) .size } @@ -168,7 +183,8 @@ object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with Po versionOpt: Option[String] = None, tagOpt: Option[String] = None, offset: Int = 0, - limit: Int = 1000 + limit: Int = 1000, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) = run { val q = idOpt.fold(quote(query[CredentialDefinition]))(id => @@ -183,6 +199,7 @@ object CredentialDefinitionSql extends DoobieContext.Postgres(SnakeCase) with Po tagOpt .fold(quote(true))(tag => quote(cs.tags.contains(lift(tag)))) ) + .filter(_.resolutionMethod == lift(resolutionMethod)) .sortBy(cs => cs.id) .drop(offset) .take(limit) diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialSchema.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialSchema.scala index 358a175d70..7d1346f3bb 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialSchema.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/CredentialSchema.scala @@ -5,6 +5,7 @@ import io.getquill.context.json.PostgresJsonExtensions import io.getquill.doobie.DoobieContext import io.getquill.idiom.* import org.hyperledger.identus.pollux.core.model.schema.Schema +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.models.WalletId import java.time.temporal.ChronoUnit @@ -22,6 +23,7 @@ case class CredentialSchema( description: String, `type`: String, schema: JsonValue[Schema], + resolutionMethod: ResourceResolutionMethod, walletId: WalletId ) { lazy val uniqueConstraintKey = author + name + version @@ -47,6 +49,7 @@ object CredentialSchema { description = m.description, `type` = m.`type`, schema = JsonValue(m.schema), + resolutionMethod = m.resolutionMethod, walletId = walletId ) @@ -63,12 +66,17 @@ object CredentialSchema { tags = db.tags, description = db.description, `type` = db.`type`, + resolutionMethod = db.resolutionMethod, schema = db.schema.value ) } } -object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with PostgresJsonExtensions { +object CredentialSchemaSql + extends DoobieContext.Postgres(SnakeCase) + with PostgresJsonExtensions + with PostgresEnumEncoders { + def insert(schema: CredentialSchema) = run { quote( query[CredentialSchema] @@ -76,21 +84,27 @@ object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with Postgr ).returning(cs => cs) } - def findByGUID(guid: UUID) = run { - quote(query[CredentialSchema].filter(_.guid == lift(guid)).take(1)) + def findByGUID(guid: UUID, resolutionMethod: ResourceResolutionMethod) = run { + quote( + query[CredentialSchema] + .filter(_.guid == lift(guid)) + .filter(_.resolutionMethod == lift(resolutionMethod)) + .take(1) + ) } + // NOTE: this function is not used def findByID(id: UUID) = run { quote(query[CredentialSchema].filter(_.id == lift(id))) } - def getAllVersions(id: UUID, author: String) = run { + def getAllVersions(id: UUID, author: String, resolutionMethod: ResourceResolutionMethod) = run { quote( query[CredentialSchema] .filter(_.id == lift(id)) .filter(_.author == lift(author)) + .filter(_.resolutionMethod == lift(resolutionMethod)) .sortBy(_.version)(ord = Ord.asc) - .map(_.version) ) } @@ -129,10 +143,16 @@ object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with Postgr authorOpt: Option[String] = None, nameOpt: Option[String] = None, versionOpt: Option[String] = None, - tagOpt: Option[String] = None + tagOpt: Option[String] = None, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) = run { val q = - idOpt.fold(quote(query[CredentialSchema]))(id => quote(query[CredentialSchema].filter(cs => cs.id == lift(id)))) + idOpt.fold(quote(query[CredentialSchema]))(id => + quote( + query[CredentialSchema] + .filter(cs => cs.id == lift(id)) + ) + ) q.dynamic .filterOpt(authorOpt)((cs, author) => quote(cs.author.like(author))) @@ -142,6 +162,7 @@ object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with Postgr tagOpt .fold(quote(true))(tag => quote(cs.tags.contains(lift(tag)))) ) + .filter(_.resolutionMethod == lift(resolutionMethod)) .size } @@ -152,10 +173,16 @@ object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with Postgr versionOpt: Option[String] = None, tagOpt: Option[String] = None, offset: Int = 0, - limit: Int = 1000 + limit: Int = 1000, + resolutionMethod: ResourceResolutionMethod = ResourceResolutionMethod.http ) = run { val q = - idOpt.fold(quote(query[CredentialSchema]))(id => quote(query[CredentialSchema].filter(cs => cs.id == lift(id)))) + idOpt.fold(quote(query[CredentialSchema]))(id => + quote( + query[CredentialSchema] + .filter(cs => cs.id == lift(id)) + ) + ) q.dynamic .filterOpt(authorOpt)((cs, author) => quote(cs.author.like(author))) @@ -165,6 +192,7 @@ object CredentialSchemaSql extends DoobieContext.Postgres(SnakeCase) with Postgr tagOpt .fold(quote(true))(tag => quote(cs.tags.contains(lift(tag)))) ) + .filter(_.resolutionMethod == lift(resolutionMethod)) .sortBy(cs => cs.id) .drop(offset) .take(limit) diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala index 652b137fc4..fd9392204d 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala @@ -1,7 +1,13 @@ package org.hyperledger.identus.pollux.sql.model +import doobie._ +import doobie.postgres._ +import doobie.postgres.implicits._ +import io.getquill.doobie.DoobieContext import io.getquill.MappedEncoding +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.shared.models.WalletId +import org.postgresql.util.PGobject import java.util.UUID @@ -10,4 +16,36 @@ package object db { given MappedEncoding[WalletId, UUID] = MappedEncoding(_.toUUID) given MappedEncoding[UUID, WalletId] = MappedEncoding(WalletId.fromUUID) + given mappedDecoderResourceResolutionMethod: MappedEncoding[ResourceResolutionMethod, String] = + MappedEncoding[ResourceResolutionMethod, String](_.toString) + + given mappedEncoderResourceResolutionMethod: MappedEncoding[String, ResourceResolutionMethod] = + MappedEncoding[String, ResourceResolutionMethod] { + case "did" => ResourceResolutionMethod.did + case "http" => ResourceResolutionMethod.http + case other => throw new IllegalArgumentException(s"Unknown ResourceResolutionMethod: $other") + } + + trait PostgresEnumEncoders { + this: DoobieContext.Postgres[_] => + + given encoderResourceResolutionMethod: Encoder[ResourceResolutionMethod] = encoder[ResourceResolutionMethod]( + java.sql.Types.OTHER, + (index: Index, value: ResourceResolutionMethod, row: PrepareRow) => { + val pgObj = new PGobject() + pgObj.setType("resolution_method_enum") + pgObj.setValue(value.toString) + row.setObject(index, pgObj, java.sql.Types.OTHER) + } + ) + + given decoderResourceResolutionMethod: Decoder[ResourceResolutionMethod] = decoder(row => + index => + row.getObject(index).toString match { + case "did" => ResourceResolutionMethod.did + case "http" => ResourceResolutionMethod.http + } + ) + } + } diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala index db884407a3..b61a711bf2 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala @@ -20,10 +20,7 @@ given statusPurposeGet: Get[StatusPurpose] = Get[String].map { case purpose => throw RuntimeException(s"Invalid status purpose - $purpose") } -given statusPurposePut: Put[StatusPurpose] = Put[String].contramap { - case StatusPurpose.Revocation => StatusPurpose.Revocation.str - case StatusPurpose.Suspension => StatusPurpose.Suspension.str -} +given statusPurposePut: Put[StatusPurpose] = Put[String].contramap(_.toString) given urlGet: Get[URL] = Get[String].map(s => URI.create(s).toURL()) given urlPut: Put[URL] = Put[String].contramap(_.toString()) diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialDefinitionRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialDefinitionRepository.scala index 5dda0f6298..bd093d8de0 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialDefinitionRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialDefinitionRepository.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.sql.repository import doobie.* import doobie.implicits.* import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.{CredentialDefinitionRepository, Repository} import org.hyperledger.identus.pollux.core.repository.Repository.* import org.hyperledger.identus.pollux.sql.model.db.{ @@ -31,9 +32,9 @@ case class JdbcCredentialDefinitionRepository(xa: Transactor[ContextAwareTask], ) } - override def findByGuid(guid: UUID): UIO[Option[CredentialDefinition]] = { + override def findByGuid(guid: UUID, resolutionMethod: ResourceResolutionMethod): UIO[Option[CredentialDefinition]] = { CredentialDefinitionSql - .findByGUID(guid) + .findByGUID(guid, resolutionMethod) .transact(xb) .orDie .map( @@ -85,7 +86,8 @@ case class JdbcCredentialDefinitionRepository(xa: Transactor[ContextAwareTask], versionOpt = query.filter.version, tagOpt = query.filter.tag, offset = query.skip, - limit = query.limit + limit = query.limit, + resolutionMethod = query.filter.resolutionMethod ) .transactWallet(xa) .orDie @@ -96,7 +98,8 @@ case class JdbcCredentialDefinitionRepository(xa: Transactor[ContextAwareTask], authorOpt = query.filter.author, nameOpt = query.filter.name, versionOpt = query.filter.version, - tagOpt = query.filter.tag + tagOpt = query.filter.tag, + resolutionMethod = query.filter.resolutionMethod ) .transactWallet(xa) .orDie diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialSchemaRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialSchemaRepository.scala index 606a1e577d..2c51771a68 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialSchemaRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialSchemaRepository.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.sql.repository import doobie.* import doobie.implicits.* import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.core.repository.{CredentialSchemaRepository, Repository} import org.hyperledger.identus.pollux.core.repository.Repository.* import org.hyperledger.identus.pollux.sql.model.db.{CredentialSchema as CredentialSchemaRow, CredentialSchemaSql} @@ -27,9 +28,9 @@ case class JdbcCredentialSchemaRepository(xa: Transactor[ContextAwareTask], xb: ) } - override def findByGuid(guid: UUID): UIO[Option[CredentialSchema]] = { + override def findByGuid(guid: UUID, resolutionMethod: ResourceResolutionMethod): UIO[Option[CredentialSchema]] = { CredentialSchemaSql - .findByGUID(guid) + .findByGUID(guid, resolutionMethod) .transact(xb) .orDie .map( @@ -38,6 +39,7 @@ case class JdbcCredentialSchemaRepository(xa: Transactor[ContextAwareTask], xb: ) } + // NOTE: this function is not used anywhere override def update(cs: CredentialSchema): URIO[WalletAccessContext, CredentialSchema] = { ZIO.serviceWithZIO[WalletAccessContext](ctx => CredentialSchemaSql @@ -48,13 +50,19 @@ case class JdbcCredentialSchemaRepository(xa: Transactor[ContextAwareTask], xb: ) } - def getAllVersions(id: UUID, author: String): URIO[WalletAccessContext, List[String]] = { + def getAllVersions( + id: UUID, + author: String, + resolutionMethod: ResourceResolutionMethod + ): URIO[WalletAccessContext, List[CredentialSchema]] = { CredentialSchemaSql - .getAllVersions(id, author) + .getAllVersions(id, author, resolutionMethod) .transactWallet(xa) .orDie + .map(_.map(CredentialSchemaRow.toModel)) } + // NOTE: this function is not used anywhere override def delete(guid: UUID): URIO[WalletAccessContext, CredentialSchema] = { CredentialSchemaSql .delete(guid) @@ -81,7 +89,8 @@ case class JdbcCredentialSchemaRepository(xa: Transactor[ContextAwareTask], xb: versionOpt = query.filter.version, tagOpt = query.filter.tags, offset = query.skip, - limit = query.limit + limit = query.limit, + resolutionMethod = query.filter.resolutionMethod ) .transactWallet(xa) .orDie @@ -92,7 +101,8 @@ case class JdbcCredentialSchemaRepository(xa: Transactor[ContextAwareTask], xb: authorOpt = query.filter.author, nameOpt = query.filter.name, versionOpt = query.filter.version, - tagOpt = query.filter.tags + tagOpt = query.filter.tags, + resolutionMethod = query.filter.resolutionMethod ) .transactWallet(xa) .orDie diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala index e6508496f6..f0a9dc2dbe 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala @@ -89,6 +89,8 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T case DecodingError(message) => new Throwable(message) case IndexOutOfBounds(message) => new Throwable(message) } + resourcePath = + s"credential-status/$id" emptyStatusListCredential <- VCStatusList2021 .build( vcId = s"$statusListRegistryUrl/credential-status/$id", diff --git a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala index bdae719bd9..346ef2b580 100644 --- a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala +++ b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.core.service +import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.sql.repository.JdbcOID4VCIIssuerMetadataRepository import org.hyperledger.identus.sharedtest.containers.PostgresTestContainerSupport import org.hyperledger.identus.test.container.MigrationAspects @@ -16,7 +17,7 @@ object OID4VCIIssuerMetadataServiceSpec extends ZIOSpecDefault, PostgresTestCont private val testEnvironmentLayer = ZLayer.make[OID4VCIIssuerMetadataService]( OID4VCIIssuerMetadataServiceImpl.layer, JdbcOID4VCIIssuerMetadataRepository.layer, - ResourceURIDereferencerImpl.layer, + ResourceUrlResolver.layer, contextAwareTransactorLayer, systemTransactorLayer ) diff --git a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala index 87f290aee7..9ace492546 100644 --- a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala +++ b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala @@ -4,6 +4,7 @@ import com.dimafeng.testcontainers.PostgreSQLContainer import doobie.* import doobie.util.transactor.Transactor import io.getquill.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.sql.model.db.{CredentialDefinition, CredentialDefinitionSql} import org.hyperledger.identus.shared.db.ContextAwareTask import org.hyperledger.identus.shared.db.Implicits.* @@ -93,6 +94,7 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr signatureType <- credentialDefinitionSignatureType supportRevocation <- credentialDefinitionSupportRevocation walletId <- Gen.fromZIO(ZIO.serviceWith[WalletAccessContext](_.walletId)) + resolutionMethod <- Gen.fromIterable(ResourceResolutionMethod.values) } yield CredentialDefinition( guid = id, id = id, @@ -109,6 +111,7 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr schemaId = schemaId, signatureType = signatureType, supportRevocation = supportRevocation, + resolutionMethod = resolutionMethod, walletId = walletId ).withTruncatedTimestamp() @@ -136,7 +139,7 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr expected <- Generators.credentialDefinition.runCollectN(1).map(_.head) _ <- CredentialDefinitionSql.insert(expected).transactWallet(tx) actual <- CredentialDefinitionSql - .findByGUID(expected.guid) + .findByGUID(expected.guid, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -147,7 +150,7 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr .update(updatedExpected) .transactWallet(tx) updatedActual2 <- CredentialDefinitionSql - .findByGUID(expected.id) + .findByGUID(expected.id, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -157,7 +160,7 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr deleted <- CredentialDefinitionSql.delete(expected.guid).transactWallet(tx) notFound <- CredentialDefinitionSql - .findByGUID(expected.guid) + .findByGUID(expected.guid, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -196,17 +199,24 @@ object CredentialDefinitionSqlIntegrationSpec extends ZIOSpecDefault with Postgr firstActual = generatedCredentialDefinitions.head firstExpected <- CredentialDefinitionSql - .findByGUID(firstActual.guid) + .findByGUID(firstActual.guid, firstActual.resolutionMethod) .transactWallet(tx) .map(_.headOption) credentialDefinitionCreated = assert(firstActual)(equalTo(firstExpected.get)) totalCount <- CredentialDefinitionSql.totalCount.transactWallet(tx) - lookupCount <- CredentialDefinitionSql.lookupCount().transactWallet(tx) + lookupCountHttpCredDef <- CredentialDefinitionSql + .lookupCount(resolutionMethod = ResourceResolutionMethod.http) + .transactWallet(tx) + lookupCountDidCredDef <- CredentialDefinitionSql + .lookupCount(resolutionMethod = ResourceResolutionMethod.did) + .transactWallet(tx) totalCountIsN = assert(totalCount)(equalTo(generatedCredentialDefinitions.length)) - lookupCountIsN = assert(lookupCount)(equalTo(generatedCredentialDefinitions.length)) + lookupCountIsN = assert(lookupCountHttpCredDef + lookupCountDidCredDef)( + equalTo(generatedCredentialDefinitions.length) + ) } yield allCredentialDefinitionsHaveUniqueId && allCredentialDefinitionsHaveUniqueConstraint && diff --git a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala index e26b6d5193..91aa5d6388 100644 --- a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala +++ b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala @@ -4,6 +4,7 @@ import com.dimafeng.testcontainers.PostgreSQLContainer import doobie.* import doobie.util.transactor.Transactor import io.getquill.* +import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod import org.hyperledger.identus.pollux.sql.model.db.{CredentialSchema, CredentialSchemaSql} import org.hyperledger.identus.shared.db.ContextAwareTask import org.hyperledger.identus.shared.db.Implicits.* @@ -75,6 +76,7 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo authored = OffsetDateTime.now(ZoneOffset.UTC) id = UUID.randomUUID() walletId <- Gen.fromZIO(ZIO.serviceWith[WalletAccessContext](_.walletId)) + resolutionMethod <- Gen.fromIterable(ResourceResolutionMethod.values) } yield CredentialSchema( guid = id, id = id, @@ -86,7 +88,8 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo author = author, authored = authored, tags = tags, - walletId = walletId + walletId = walletId, + resolutionMethod = resolutionMethod ).withTruncatedTimestamp() private val unique = mutable.Set.empty[String] @@ -123,12 +126,12 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo record <- Generators.schema.runCollectN(1).map(_.head).provide(wallet1) _ <- CredentialSchemaSql.insert(record).transactWallet(tx).provide(wallet1) ownRecord <- CredentialSchemaSql - .findByGUID(record.guid) + .findByGUID(record.guid, record.resolutionMethod) .transactWallet(tx) .map(_.headOption) .provide(wallet1) crossRecord <- CredentialSchemaSql - .findByGUID(record.guid) + .findByGUID(record.guid, record.resolutionMethod) .transactWallet(tx) .map(_.headOption) .provide(wallet2) @@ -173,7 +176,7 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo expected <- Generators.schema.runCollectN(1).map(_.head) _ <- CredentialSchemaSql.insert(expected).transactWallet(tx) actual <- CredentialSchemaSql - .findByGUID(expected.guid) + .findByGUID(expected.guid, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -184,7 +187,7 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo .update(updatedExpected) .transactWallet(tx) updatedActual2 <- CredentialSchemaSql - .findByGUID(expected.id) + .findByGUID(expected.id, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -194,7 +197,7 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo deleted <- CredentialSchemaSql.delete(expected.guid).transactWallet(tx) notFound <- CredentialSchemaSql - .findByGUID(expected.guid) + .findByGUID(expected.guid, expected.resolutionMethod) .transactWallet(tx) .map(_.headOption) @@ -231,17 +234,22 @@ object CredentialSchemaSqlIntegrationSpec extends ZIOSpecDefault, PostgresTestCo firstActual = generatedSchemas.head firstExpected <- CredentialSchemaSql - .findByGUID(firstActual.guid) + .findByGUID(firstActual.guid, firstActual.resolutionMethod) .transactWallet(tx) .map(_.headOption) schemaCreated = assert(firstActual)(equalTo(firstExpected.get)) totalCount <- CredentialSchemaSql.totalCount.transactWallet(tx) - lookupCount <- CredentialSchemaSql.lookupCount().transactWallet(tx) + lookupCountHttpSchemas <- CredentialSchemaSql + .lookupCount(resolutionMethod = ResourceResolutionMethod.http) + .transactWallet(tx) + lookupCountDidSchemas <- CredentialSchemaSql + .lookupCount(resolutionMethod = ResourceResolutionMethod.did) + .transactWallet(tx) totalCountIsN = assert(totalCount)(equalTo(generatedSchemas.length)) - lookupCountIsN = assert(lookupCount)(equalTo(generatedSchemas.length)) + lookupCountIsN = assert(lookupCountHttpSchemas + lookupCountDidSchemas)(equalTo(generatedSchemas.length)) } yield allSchemasHaveUniqueId && allSchemasHaveUniqueConstraint && diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala index fc5a4d8c28..33ebac2154 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala @@ -44,7 +44,7 @@ case class DIDDocumentMetadata( created: Option[Instant] = Option.empty, updated: Option[Instant] = Option.empty, deactivated: Option[Boolean] = Option.empty, - versionId: Option[Instant] = Option.empty, + versionId: Option[Instant] = Option.empty, // TODO: this probably should not be an instant, it should be a string nextUpdate: Option[Instant] = Option.empty, nextVersionId: Option[Instant] = Option.empty, equivalentId: Option[Instant] = Option.empty, diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala index 6fb3c7387a..3d24747bd1 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala @@ -30,9 +30,9 @@ case class W3cVerifiableCredentialPayload(payload: W3cCredentialPayload, proof: case class JwtVerifiableCredentialPayload(jwt: JWT) extends VerifiableCredentialPayload -enum StatusPurpose(val str: String) { - case Revocation extends StatusPurpose("Revocation") - case Suspension extends StatusPurpose("Suspension") +enum StatusPurpose { + case Revocation + case Suspension } case class CredentialStatus( diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala index dbaf540498..e4ea76cb6b 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala @@ -52,7 +52,7 @@ object VCStatusList2021 { } yield { val claims = JsonObject() .add("type", "StatusList2021".asJson) - .add("statusPurpose", purpose.str.asJson) + .add("statusPurpose", purpose.toString.asJson) .add("encodedList", encodedBitString.asJson) val w3Credential = W3cCredentialPayload( `@context` = Set( diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/http/DataUrlResolver.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/http/DataUrlResolver.scala new file mode 100644 index 0000000000..e5719d0b7b --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/http/DataUrlResolver.scala @@ -0,0 +1,17 @@ +package org.hyperledger.identus.shared.http + +import io.lemonlabs.uri.DataUrl +import zio.* + +class DataUrlResolver extends UriResolver { + override def resolve(dataUrl: String): IO[GenericUriResolverError, String] = { + + DataUrl.parseOption(dataUrl).fold(ZIO.fail(InvalidUri(dataUrl))) { url => + ZIO.succeed(String(url.data, url.mediaType.charset)) + } + } +} + +object DataUrlResolver { + val layer: ULayer[DataUrlResolver] = ZLayer.succeed(new DataUrlResolver) +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala index f8b5741f3a..25be250c52 100644 --- a/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala @@ -1,7 +1,14 @@ package org.hyperledger.identus.shared.http -import io.lemonlabs.uri.{DataUrl, Uri, Url, Urn} +import io.lemonlabs.uri.{Uri, Url, Urn} +import org.hyperledger.identus.shared.models.{Failure, StatusCode} +import org.hyperledger.identus.shared.models.PrismEnvelopeData +import org.hyperledger.identus.shared.utils.Base64Utils import zio.* +import zio.json.* + +import scala.util +import scala.util.Try trait UriResolver { @@ -12,41 +19,54 @@ trait UriResolver { class GenericUriResolver(resolvers: Map[String, UriResolver]) extends UriResolver { override def resolve(uri: String): IO[GenericUriResolverError, String] = { - val parsedUri = Uri.parse(uri) - parsedUri match - case url: Url => - url.schemeOption.fold(ZIO.fail(InvalidUri(uri)))(schema => - resolvers.get(schema).fold(ZIO.fail(UnsupportedUriSchema(schema)))(resolver => resolver.resolve(uri)) - ) - - case Urn(path) => ZIO.fail(InvalidUri(uri)) // Must be a URL - } - -} - -class DataUrlResolver extends UriResolver { - override def resolve(dataUrl: String): IO[GenericUriResolverError, String] = { - - DataUrl.parseOption(dataUrl).fold(ZIO.fail(InvalidUri(dataUrl))) { url => - ZIO.succeed(String(url.data, url.mediaType.charset)) - } + val parsedUri = Uri.parseTry(uri) + + ZIO.debug(s"Resolving resource from uri: $uri") *> + ZIO.fromTry(parsedUri).mapError(_ => InvalidUri(uri)).flatMap { + case url: Url => + url.schemeOption.fold(ZIO.fail(InvalidUri(uri)))(schema => + resolvers.get(schema).fold(ZIO.fail(UnsupportedUriSchema(schema))) { resolver => + resolver.resolve(uri).flatMap { res => + schema match + case "did" => + res.fromJson[PrismEnvelopeData] match + case Right(env) => + ZIO + .fromTry(Try(Base64Utils.decodeUrlToString(env.resource))) + .mapError(_ => DidUriResponseNotEnvelope(uri)) + case Left(err) => + ZIO.debug(s"Failed to parse response as PrismEnvelope: $err") *> + ZIO.debug("Falling back to returning the response as is") *> + ZIO.succeed(res) + case _ => ZIO.succeed(res) + } + } + ) + + case Urn(path) => ZIO.fail(InvalidUri(uri)) // Must be a URL + } } } -sealed trait GenericUriResolverError { +trait GenericUriResolverError(val statusCode: StatusCode, val userFacingMessage: String) extends Failure { + override val namespace: String = "UriResolver" def toThrowable: Throwable = { this match case InvalidUri(uri) => new RuntimeException(s"Invalid URI: $uri") case UnsupportedUriSchema(schema) => new RuntimeException(s"Unsupported URI schema: $schema") - case SchemaSpecificResolutionError(schema, error) => - new RuntimeException(s"Error resolving ${schema} URL: ${error.getMessage}") } } -case class InvalidUri(uri: String) extends GenericUriResolverError +case class DidUriResponseNotEnvelope(uri: String) + extends GenericUriResolverError( + StatusCode.UnprocessableContent, + s"The response of DID uri resolution was not prism envelope: uri=[$uri]" + ) -case class UnsupportedUriSchema(schema: String) extends GenericUriResolverError +case class InvalidUri(uri: String) + extends GenericUriResolverError(StatusCode.UnprocessableContent, s"The URI to dereference is invalid: uri=[$uri]") -case class SchemaSpecificResolutionError(schema: String, error: Throwable) extends GenericUriResolverError +case class UnsupportedUriSchema(schema: String) + extends GenericUriResolverError(StatusCode.UnprocessableContent, s"Unsupported URI schema: $schema") diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/models/PrismEnvelope.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/models/PrismEnvelope.scala new file mode 100644 index 0000000000..c02df829e3 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/models/PrismEnvelope.scala @@ -0,0 +1,16 @@ +package org.hyperledger.identus.shared.models +import zio.json.* + +trait PrismEnvelope { + val resource: String + val url: String +} + +case class PrismEnvelopeData(resource: String, url: String) extends PrismEnvelope +object PrismEnvelopeData { + given encoder: JsonEncoder[PrismEnvelopeData] = + DeriveJsonEncoder.gen[PrismEnvelopeData] + + given decoder: JsonDecoder[PrismEnvelopeData] = + DeriveJsonDecoder.gen[PrismEnvelopeData] +} diff --git a/tests/integration-tests/build.gradle.kts b/tests/integration-tests/build.gradle.kts index 07fcfb53a3..3efb0d40a8 100644 --- a/tests/integration-tests/build.gradle.kts +++ b/tests/integration-tests/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { testImplementation("io.ktor:ktor-server-netty:2.3.0") testImplementation("io.ktor:ktor-client-apache:2.3.0") // RestAPI client - testImplementation("org.hyperledger.identus:cloud-agent-client-kotlin:1.39.0-e077cdd") + testImplementation("org.hyperledger.identus:cloud-agent-client-kotlin:1.39.1-e8ad592") // Test helpers library testImplementation("io.iohk.atala:atala-automation:0.4.0") // Hoplite for configuration @@ -48,7 +48,7 @@ dependencies { testImplementation("io.iohk.atala.prism.apollo:apollo-jvm:1.3.4") // OID4VCI testImplementation("org.htmlunit:htmlunit:4.3.0") - testImplementation("eu.europa.ec.eudi:eudi-lib-jvm-openid4vci-kt:0.3.2") + testImplementation("eu.europa.ec.eudi:eudi-lib-jvm-openid4vci-kt:0.4.1") } serenity { diff --git a/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt b/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt index 4ea1481a90..e2116d6793 100644 --- a/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt +++ b/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt @@ -15,8 +15,7 @@ import net.serenitybdd.screenplay.Ability import net.serenitybdd.screenplay.Actor import net.serenitybdd.screenplay.HasTeardown import net.serenitybdd.screenplay.Question -import org.hyperledger.identus.client.models.Connection -import org.hyperledger.identus.client.models.IssueCredentialRecord +import org.hyperledger.identus.client.models.* import java.net.URL import java.time.OffsetDateTime @@ -26,7 +25,12 @@ open class ListenToEvents( ) : Ability, HasTeardown { private val server: ApplicationEngine - private val gson = GsonBuilder().registerTypeAdapter(OffsetDateTime::class.java, CustomGsonObjectMapperFactory.OffsetDateTimeTypeAdapter()).create() + private val gson = GsonBuilder() + .registerTypeAdapter( + OffsetDateTime::class.java, + CustomGsonObjectMapperFactory.OffsetDateTimeTypeAdapter(), + ) + .create() var connectionEvents: MutableList = mutableListOf() var credentialEvents: MutableList = mutableListOf() @@ -40,10 +44,34 @@ open class ListenToEvents( val eventString = call.receiveText() val event = gson.fromJson(eventString, Event::class.java) when (event.type) { - TestConstants.EVENT_TYPE_CONNECTION_UPDATED -> connectionEvents.add(gson.fromJson(eventString, ConnectionEvent::class.java)) - TestConstants.EVENT_TYPE_ISSUE_CREDENTIAL_RECORD_UPDATED -> credentialEvents.add(gson.fromJson(eventString, CredentialEvent::class.java)) - TestConstants.EVENT_TYPE_PRESENTATION_UPDATED -> presentationEvents.add(gson.fromJson(eventString, PresentationEvent::class.java)) - TestConstants.EVENT_TYPE_DID_STATUS_UPDATED -> didEvents.add(gson.fromJson(eventString, DidEvent::class.java)) + TestConstants.EVENT_TYPE_CONNECTION_UPDATED -> connectionEvents.add( + gson.fromJson( + eventString, + ConnectionEvent::class.java, + ), + ) + + TestConstants.EVENT_TYPE_ISSUE_CREDENTIAL_RECORD_UPDATED -> credentialEvents.add( + gson.fromJson( + eventString, + CredentialEvent::class.java, + ), + ) + + TestConstants.EVENT_TYPE_PRESENTATION_UPDATED -> presentationEvents.add( + gson.fromJson( + eventString, + PresentationEvent::class.java, + ), + ) + + TestConstants.EVENT_TYPE_DID_STATUS_UPDATED -> didEvents.add( + gson.fromJson( + eventString, + DidEvent::class.java, + ), + ) + else -> { throw IllegalArgumentException("ERROR: unknown event type ${event.type}") } diff --git a/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt index b9589a8754..7b91024a18 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt @@ -1,5 +1,6 @@ package steps.did +import com.google.gson.JsonPrimitive import interactions.Get import interactions.Post import interactions.body @@ -148,7 +149,7 @@ class ManageDidSteps { CreateManagedDidRequestDocumentTemplate( publicKeys = listOf(ManagedDIDKeyTemplate("auth-1", purpose, curve)), services = listOf( - Service("https://foo.bar.com", listOf("LinkedDomains"), Json("https://foo.bar.com/")), + Service("https://foo.bar.com", listOf("LinkedDomains"), JsonPrimitive("https://foo.bar.com/")), ), ), ) diff --git a/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt index e9e590d319..0bbfa21380 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt @@ -1,5 +1,7 @@ package steps.did +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive import interactions.Get import interactions.Post import interactions.body @@ -51,7 +53,7 @@ class UpdateDidSteps { addService = Service( id = serviceId, type = listOf("LinkedDomains"), - serviceEndpoint = Json("https://service.com/"), + serviceEndpoint = JsonPrimitive("https://service.com/") as JsonElement, ), ) actor.remember("newServiceId", serviceId) @@ -77,7 +79,7 @@ class UpdateDidSteps { updateService = UpdateManagedDIDServiceAction( id = serviceId, type = listOf("LinkedDomains"), - serviceEndpoint = Json(newServiceUrl), + serviceEndpoint = JsonPrimitive(newServiceUrl) as JsonElement, ), ) @@ -124,7 +126,8 @@ class UpdateDidSteps { ) val didKey = "${actor.recall("shortFormDid")}#$newDidKeyId" val didDocument = SerenityRest.lastResponse().get().didDocument!! - val verificationMethodNotPresent = didDocument.verificationMethod!!.map { it.id }.none { it == didKey } + val verificationMethodNotPresent = + didDocument.verificationMethod!!.map { it.id }.none { it == didKey } verificationMethodNotPresent && when (purpose) { Purpose.ASSERTION_METHOD -> didDocument.assertionMethod!!.none { it == didKey } @@ -189,7 +192,16 @@ class UpdateDidSteps { Get.resource("/dids/${actor.recall("shortFormDid")}"), ) val service = SerenityRest.lastResponse().get().didDocument!!.service!! - service.any { it.serviceEndpoint.value.contains(serviceUrl) } + service.any { serviceEntry -> + val serviceEndpoint = serviceEntry.serviceEndpoint!! + if (serviceEndpoint.isJsonPrimitive) { + serviceEndpoint.asString.contains(serviceUrl) + } else if (serviceEndpoint.isJsonArray) { + serviceEndpoint.asJsonArray.any { it.asString.contains(serviceUrl) } + } else { + false + } + } }, equalTo(true), ), diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt index 218f233004..3cd1873033 100644 --- a/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt @@ -99,7 +99,7 @@ class IssueCredentialSteps { val issuer = holder.recall("eudiIssuer") val authorizedRequest = holder.recall("eudiAuthorizedRequest") val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialOffer.credentialConfigurationIdentifiers.first(), null) - val submissionOutcome = with(issuer) { + val authRequestAndsubmissionOutcome = with(issuer) { when (authorizedRequest) { is AuthorizedRequest.NoProofRequired -> throw Exception("Not supported yet") is AuthorizedRequest.ProofRequired -> runBlocking { @@ -110,7 +110,7 @@ class IssueCredentialSteps { } }.getOrThrow() } - holder.remember("eudiSubmissionOutcome", submissionOutcome) + holder.remember("eudiSubmissionOutcome", authRequestAndsubmissionOutcome.second) } @Then("{actor} sees credential issued successfully from CredentialEndpoint") diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt index 6fd2f2a3b1..7db69f0b41 100644 --- a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt @@ -50,7 +50,7 @@ class ManageIssuerSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) val matchedIssuers = SerenityRest.lastResponse().get().contents!! - .filter { it.id == credentialIssuer.id } + .filter { it.id.toString() == credentialIssuer.id } issuer.attemptsTo( Ensure.that(matchedIssuers).hasSize(1), ) @@ -102,7 +102,7 @@ class ManageIssuerSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), ) val updatedIssuer = SerenityRest.lastResponse().get().contents!! - .find { it.id == credentialIssuer.id }!! + .find { it.id.toString() == credentialIssuer.id }!! issuer.attemptsTo( Ensure.that(updatedIssuer.authorizationServerUrl).isEqualTo(UPDATE_AUTH_SERVER_URL), ) @@ -129,7 +129,7 @@ class ManageIssuerSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), ) val matchedIssuers = SerenityRest.lastResponse().get().contents!! - .filter { it.id == credentialIssuer.id } + .filter { it.id.toString() == credentialIssuer.id } issuer.attemptsTo( Ensure.that(matchedIssuers).isEmpty(), ) From 9eee9142635af68c6fcde837a501f623f9be5b8f Mon Sep 17 00:00:00 2001 From: Shota Jolbordi Date: Sat, 28 Sep 2024 01:02:23 +0400 Subject: [PATCH 06/13] docs: add prism anoncreds method support docs (#1386) Signed-off-by: Shota Jolbordi Signed-off-by: Pete Vielhaber Co-authored-by: Pete Vielhaber --- .../docusaurus/credentialdefinition/create.md | 33 +++++++++-- docs/docusaurus/credentials/issue.md | 6 +- docs/docusaurus/schemas/create.md | 37 +++++++++++- docs/docusaurus/schemas/credential-schema.md | 58 ++++++++++++++++--- docs/docusaurus/schemas/update.md | 14 ++++- 5 files changed, 130 insertions(+), 18 deletions(-) diff --git a/docs/docusaurus/credentialdefinition/create.md b/docs/docusaurus/credentialdefinition/create.md index 653e7eb6fc..f387e5de33 100644 --- a/docs/docusaurus/credentialdefinition/create.md +++ b/docs/docusaurus/credentialdefinition/create.md @@ -6,6 +6,10 @@ The OpenAPI specification and ReDoc documentation describe the endpoint. In this document, you can find step-by-step instructions for creating the credential definition. +## Prerequisites + +Before creating a credential definition, one must first create and then publish [prism DID](../dids/create.md), and then [create a credential schema](../schemas/create.md) to be used for the credential definition. Credential schema for credential definition **must** have a type of `AnoncredSchemaV1` as shown in [this](../schemas/credential-schema#schema-anoncred-schema) example. + ## Step-by-step guide The following guide demonstrates how to create a birth certificate credential definition. @@ -33,7 +37,9 @@ Here's a sample content of the credential definition: 1. Use your preferred REST API client, such as Postman or Insomnia, or utilize a client stub that's generated based on the OpenAPI specification. -2. In your API client, initiate a new POST request to the `/credential-definition-registry/definitions/` endpoint. +2. In your API client, initiate a new POST request to either `/credential-definition-registry/definitions` or `/credential-definition-registry/definitions/did-url` endpoints. They both take the same payload + 1. `/credential-definition-registry/definitions` creates a credential definition that can later be resolved via HTTP URL + 2. `/credential-definition-registry/definitions/did-url` creates a credential definition that can later be resolved via [DID URL](/docs/concepts/glossary#did-url), the DID includes a service endpoint with the location of the credential definition registry. Please note: The `author` field value should align with the short form of a PRISM DID previously created by the same agent. It's okay if this DID is unpublished. You can refer to the [Create DID](../dids/create.md) documentation for more comprehensive details on crafting a PRISM DID. @@ -55,6 +61,7 @@ Please note: The `author` field value should align with the short form of a PRIS 4. Transmit the POST Request to Create the New Credential Definition Once you've crafted your POST request, send it. Upon success, the server should respond with a GUID that uniquely identifies the new credential definition. +The response bodies will be the same for HTTP URL endpoint and DID URL endpoint, as well as request bodies, the only difference will be the URL, and how this credential definitions will be resolved later, via HTTP URL or DID URL respectivly. For ease of reference, here's a `curl` example: @@ -99,7 +106,7 @@ A potential response could be: ### 3. Retrieve the Created Credential Definition -To obtain details of the newly created credential definition, send a GET request to the `/credential-definition-registry/definitions/{guid}` endpoint. Replace `{guid}` with the unique GUID returned from the previous creation step. +To obtain details of the newly created credential definition, send a GET request to either `/credential-definition-registry/definitions/{guid}` or `/credential-definition-registry/definitions/did-url/{guid}` endpoints. Replace `{guid}` with the unique GUID returned from the previous creation step. Note that if you've created a credential definitoin via HTTP URL endpoint, you can retrieve it via `/credential-definition-registry/definitions/{guid}` and if you've created credential definition via DID URL endpoint, it can only be retrieved via `/credential-definition-registry/definitions/did-url/{guid}` To exemplify this process, use the following `curl` command: @@ -110,6 +117,16 @@ curl -X 'GET' \ -H "apikey: $API_KEY" ``` +or in case of DID URL + + +```shell +curl -X 'GET' \ + 'http://localhost:8080/credential-definition-registry/definitions/did-url/3f86a73f-5b78-39c7-af77-0c16123fa9c2' \ + -H 'accept: application/json' \ + -H "apikey: $API_KEY" +``` + You should receive a response containing the JSON object representing the credential definition you've just established: ```json @@ -131,6 +148,15 @@ You should receive a response containing the JSON object representing the creden } ``` +Or in case of DID URL, the respoinse is [Prism Envelope](/docs/concepts/glossary#prism-envelope) + +```json +{ +"resource": "eyJhdXRob3IiOiJkaWQ6cHJpc206ZDYyYzFlNmJlZDdiYWYzYjgwNzFiZmE5NzUyNDg0Zjg5ODRlNzUzMWZjMmM1MGJiOTQ4OTE4YWYwNWFiMjAxOSIsImF1dGhvcmVkIjoiMjAyNC0wOS0yN1QxMjozNjoxNS42NDcwNTRaIiwiZGVmaW5pdGlvbiI6eyJpc3N1ZXJJZCI6ImRpZDpwcmlzbTpkNjJjMWU2YmVkN2JhZjNiODA3MWJmYTk3NTI0ODRmODk4NGU3NTMxZmMyYzUwYmI5NDg5MThhZjA1YWIyMDE5Iiwic2NoZW1hSWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODUvc2NoZW1hLXJlZ2lzdHJ5L3NjaGVtYXMvZGFjZjNiOGUtODllYi0zYWQ2LWExNDYtMTIyZGRhN2Q2MjY0L3NjaGVtYSIsInRhZyI6IkxpY2VuY2UiLCJ0eXBlIjoiQ0wiLCJ2YWx1ZSI6eyJwcmltYXJ5Ijp7Im4iOiIxMDU2NTQwMzY4NTExMTkxOTg0NDI4MDQ5MDUxOTcxMzg3NzI2NjA5NjEzNjI1NjQ5NDI3MjY0OTIyMjEyMDY1Mzc3ODc0Mjc2NjQ3NjE2MTA1Mjg5MTEyNjg2NTI1ODI5MDI4MzM2MzE4MDQ0NjI5ODg0NDQ3NzI4Mzk5NzY5NDk3Njc3NTg1MDY1NDE2NjA4NDU4ODcxMTEyMDY3NjMwMTU1MjQyNzk5NDcwNzg5MDE3NDUwMjE3NTQ0NzU3NzcyMzc0OTk0MzQ4MTcwODU5MTM3ODQ0NzkwMzY4NzIyOTM2NTY3NzY0Mjk2NTUwMDQ5NzQwODc0NTExMTE4MTQ3ODE0MDIxNzAxNTU2MDY2NDYwMDE2NDQ2NDY5ODY3OTY1MjcyMDMwODEzNDY1ODU3NzQzMjE1NTYwNDUxMjU0NzI0MzU3ODI4NDk0NjAxMzI2MzI2MTczOTgzMzQ3ODk4NTMwODkxMjgzODk2MzY0MjQyNzY0MDY0MzM0MDQ1MTg4Nzk2NzkzMjg3MTgyMjgwMjg0OTI0ODkzMzIwNDg5MjcwMzg0MTg4Nzc3NzQ3MDYwNjg4Mjc1ODg3MzYzMTA1Mzg3MDE3MDAzMzYxMzkxNTcxNjgyMjk4OTY1NDYzNzAxMDYyODcwMTA1MDg5MjkzNTcyMDQ0MzA3ODQ2NzQ3OTQ5NDg3OTMyMTA2MjA2ODIxNzc1MTc1NTA3ODk5MTg2MzMyMTE2NjA5MzU4ODE2OTYzNDUyMzU2NjQwNTMxMTMzNjExMTkyMDY2NDk0MTA1ODEwNzg0NDg2ODAwNjA0Mjc2NDE4MzIxMzciLCJyIjp7ImJpcnRoZGF5IjoiNDk3NzU1NDY2NjkwNTI5MDcxMzE1NTI5NjE1OTY2NTE1MzQ3MzM2NjUyMzExNzg4OTYzODE5MjE4MzU5MTAyOTQ0NzczMjQ1ODgxNzEyMjcxNjAxMTgxNTA2MTQ2MzMwMjYwMTI5MDEyMTUzMzQ1NjU3MzU1NDI5MzM4MDg5ODU5ODc5MDQ5NDczNTAyODUxMjk1NDM4NTIyMzY5NzM3NzQ1NDc3MzkzODI5ODU1NzI0NDk3MzY4NjYyNjI5NTEzNDYzODgwODI3ODI2NjYxNDcyNjc3ODc5NDA2NTE1MDAzMzcwMTA0NjU0MzMyOTYwNzU1NTU0OTY3NDM0NTQwODIzNzcwODg3NTk2MTE4Mjk2NjkyODg1ODY4OTU2MDAwNzc1MjkxNDIyNzQ1ODEzMzUzODMxMzMxNDk4OTk3ODc5MjUzNTIxNTg1Nzg2NDExODk0NTY4MTE5NzI1MjAzNjg3MDg4MDU0NDgzODc3NDEzNDI5MjQ5NDc2MjYwNDU3MjIwMjA1Nzc1MzA1NTA4NzcxMjYyMjM0MTI1ODMyOTA1OTUzMDM4MTg1MDc2NjUzMTQzNDcwNDQzNTQ2Mzc0NDk4MTM3NTc2NzY0Mzk3NjU5NjY4NDc4MTU0Njg5MzY5NzUyMDY0MDg1MTYwMzM0ODQxNTQ2MjYxMzM2MzAyODE5NTI0MDI1MDkwMDcwOTEzNDQwMjA2NzI3NzEyODIzNTIzMjI2MDQ2NTY0Nzk3MTA1MjA2NTU1OTY0ODcxMzMxMzAxNjc2ODQyMTIyODMzMTM5NjEwNjM1ODg5ODkyODg4NTY3OTk5MDUiLCJsb2NhdGlvbiI6Ijk2NTAwODMyMjE1NTE5Mjc1MTAwMDc4MjgzMjkyNTU2NTM2MzYyMjQ3ODM4NzcxNDczMzAzMDMwMTQwMTA0ODM2OTUyODI5ODcxMTYxODUxMTgxODIyMDg1Mzc3Nzg4NTI5NDg4ODg3NTA5OTEyODIzNTE0NzIzOTM0MTg4Mzc1MjAxMjE5NzQ2MDYyMjExNzU0MTgxOTgzNjMxNDIzOTMwNTgwMTM2Njg1ODYwNjc3NzAxNDMwMTIwNjM2NTc5MTUwNzIwMzA5OTg2NDM1NzEzNDIwMzIxMjQyODIwNzI2NzIxNzM4MjE4Mjg1NzM1OTU0NTE0ODI3MTQ2MTk0NjAzMzUyMDExNTg3NzEwNDYxODEwMTcxOTI2NzQ2OTM5ODEzMzYxOTAzODYxNzIwODg0NjcxOTMzMjkwNTk1MDM2NjA2MzYyMTk1NTk4MTQ4NTM3NTkzOTQxNzg1NTI3NTA2Nzg1ODkwMTc4OTU0MTMwMzQ5MTI3NjA0NjQ2NjY3Nzg3Nzc5ODgxMDM0OTYwNjM2ODE4NDU3ODMzMTAxMTM0ODYzNDkxOTA1NDAyMzM1NzIwNjg0MjM4MTM1NjcwNDg4ODgzNzU5Nzk0NTAwMDc4MDkxMzMyNjI3NzEwMzc4NTQ3MTEwMDM5MTk1NTQ2MjE0NDUwMzAzOTc3NTI0NDU4ODgzMTI1NzIyNjY2OTYxMDczODA2MDE2NTg1NjA5Nzg2MTIwODI3MDcwNzY1ODA4MTg0NDAyMzYyMDk1NDkyOTc4MDAxOTU3MDQxMjA2NjU3MTgwMTA0ODA0ODE1NDE5MTg1OTE0MzY1IiwibWFzdGVyX3NlY3JldCI6IjkwMTQyODA1NjM0MTQxNTAwODgxMDI5NzU2NjQ0MDYzMzIyNDAyNTg4OTg5NTMwMzA4NzExNjI1ODk3NDc1MDcyMDUzNDIwMjU0MDQxNTY0MjUzMjA1ODczMjMyNDg0NTc2NDk5OTk0MDUzODg1MjY4NjcxODY0MzU3NTc4NTY5NjU4MTQyMTc3NDEzOTc5MjcyNTU3MDQ0MDAxMzc3MzU0MDQwMTk3ODAxMzgwMzEwMTQ5ODAzMTMxMDc0ODAwOTM2Nzc5NjU5MjU3NTcwNDkxODk3MzI2ODYyNDMxMDUzNDE3NTE0NTEwNzQ3MDI4ODY2OTE3ODA4MDA5NTQyOTM1NDI2NDQ4NzQwMjI0MjAwNDAzMjIwNTY4MDA5ODgxMDI5ODIwNDUzMjE0MTU5MzY2MzU4NTc4NzE3Mzc5NTMwNzQ4NDk1NzM3MDQ5NjMwNDM5NjIwODcxODk2NTMwODQ3ODAzMjE3NDUyODk4OTI2NDYxNjQ1NTAxODU5MzAxMjM5MjAyOTA5NzY5NDUyMTIwNDQ0NDUyNzAzNzgyNDIyOTQ1NjE1MjY3Nzc4MzI0NzkyNTYxNTYzODg5MzQ5NDI3NzU3Mjk5OTc3MTA5MDE5NjMwOTAxMzk5ODg2NTg2NTU3NTE0NTg5MTM0Nzk5MTk3OTY3MzIxMzQ3Nzc1Mjk5OTIxMDU0MjE0NTM5MDQ3MjA5NTAwNjMyNTY5NDM3Njc1MDQ5NzQxMDQxNDE4OTczNDEyMjYwMzUxODI5Nzk0MDk5ODIzMzg5OTU1MDQ1NjU0MjMwMTA5MTU0NzA1NjU3MDk3NjczODc0In0sInJjdHh0IjoiNTQ4NTk3NDYwMTc5NjcxNzIwMjg0MjIxOTMwODU0NjI4MDMyOTQyNDMwMDI2NTM2NDk3NTY3MDcyOTEzMzIyOTc1NzE3NzgwMzY3MjAwOTUyMjEwNzk2NDY3MTUxMjI4OTg1OTU0MTM3MDQzMDIyNTQ4MjA0NzU0NDE4NTc0OTU1NDA0ODgyNjc1MzI2Mzg5NDU0NjAzMzkwNjE3MzAyMzYyMjMwMTM3NDI1ODgyODcxMTA2NjQ1Nzg0OTg3MjgwNDUwNjIxODc0MjQxNDkzOTQzNDkyMzc5NjczMTk3OTYxNzk2Nzc4NzYwNTI4MzI5NDU0MTc4MzM2MDIyMjM4OTYwNjQ1NDgxNzc0NDAyMzY3NDAwMTMwNzk2OTc1MzM2OTU2NjI3MzM3MzMzMjUxMzY4MTY3MjkzMTk1NDU1MTA1MTUyOTM4MTU2MzU1NDg4MDI5ODk4NjUyOTM1Mzc1MTI5NTMyNTI3NjUzNjgyMzE1MzcwNjA2MTkxNzkxMzkyMjUyODI5NzkyMTc3MDc3ODA3MDU1MjAwNzU2NjI5NjI3NDI2NDc5MDg3MTI2NDk3MDQ0NDU5MDY2ODE3MTExODYwMjUzMjQ1NTc5NzMyMDMyNzk3NjQ0NjM0ODY2MTU0NDI2NjIwNzE2ODc3NjcxNDI5NjIyNzAzMzU5NzEzNTM1MjQwNjUzMjcwNzEyOTc4NDk1NDI0NjYyNTkxNDk3MTM0NjkyODM5MDMxMDYwMTMxODkyMjE4ODQ0MDcxNjg2MjIyODE0MzM4NTY2MDIyNjM0MTQ3ODY0MDc5OTg4Mzc4MjQ1OTE4NTkiLCJzIjoiNzkxNDQwOTkzMTkzMjI4NzQ4Nzg3NDA2MTM4MzQ3Nzg1NTI1NjUwODgyNzE3NTg4MzcxODgzMzk4NDg2NTg3OTc1Nzc0ODQ1MDY1NTE3Mjk4NzM2NTAxMjU4MTUyNjg1NDI1MDA1OTU3MTUwNjk1MDE3MDUwMzc2NzE4MDQ2MjUyNTM2NDI4MTQ0MzEyMzMzMDgzMTI5MDMyNDE2NzI3MzI0NzgxNTMwMTI4MTE0MDQ3MTU1ODc3Mzg5Njg1MTE1MTMyODU4Njk3NTYwNzgwMjE2MTAxOTI1MTc3NzYxNzE5OTkxODc3OTE4OTAyMzQ2MDA2MDg5OTU2MTExNTAwNzQ5MDkwMzMxNzY5Njk4NjUzNzIwNTkyNTc5ODEzMzY4Nzk5NzI0MDg3ODAwMjAwNTM3MzYwNTg3NjQwNTc3ODYyMjAwMzE4NTIxODI2NjAyNTA3ODA5Mjc3NzUwMTY0ODUyNTY1MjQwMzgwMDU0MTM1NDY5NzUxNDEwNDk4MTQ3NzMxNDIxNzU0OTE2NDIwMjgzNTM2NzY1NDA3OTE3NDcxOTgyMDQxMDkyOTYwODUxMzA2NjM0NzgzMDY0NzA5MTY2NDc1MDc3MTk4MTAxNDIwODQ5MzA2NzkyMTU3NjMzMTUwNDkwNjgyMDE2NDk1NDMwNDAwMjI5MDEyOTM2MTg2Mjk2MzMyMDcwMzA2OTE2MDM5NDk5ODQ5Njk5MTgxNDQ2ODYxNzgyNTM3OTU5ODc0MDAzNzc3MzE3NjY4MTI3NDg4MjE3MzcxOTIyNDc3ODMzMDAxODMyODAwMjEyNDU5MzkzNTgxNjMiLCJ6IjoiNzI5ODc2NDE0MjA1NzkyNTk2ODc4NTQwMTAzODUxODM5NjY3ODIyMDE5MzQwODMxNjgxMTgxOTI1MTY3ODcwMDU4OTk0ODg4MDE4OTg4Nzc2NTczMDEyMDQ2NTc3ODI4OTY5OTA5NTE4MTIwNzMwMTc4MTQ2ODQ2NDU0NjczNDk3NTQ5MTAyNjAxNzY0NzYzMDUyMjExMTcwNDMyNzQyNTI4Nzg1OTU1NjIyOTE4NzMyMjQwMjk5NDg2NjY5OTc5MjI1ODk5NzMwMzUwMDMyMjM0NTIyMzA5MjU3MjcxNDE5MDc1MDg4MDg5MzkzNjQxOTYxOTA0Njg2MDAwODgwNTQ5ODIxMTEyNTIyMTEyMDA3MzUwODA5NTY2NjYzODM5NjA0NjEwMzQ5NDM4MTgxMzY0MDE5MzU2NTE4NTcyOTA3Mjk5OTY4NzU2NDE4MjIzNDM1ODYxNjE2MzkwNDIzMzYxMzM0NTE1NDk4NDcxMDk4NjA2NjY0NTczOTE1ODQ0NTg2MjYxMTQxNzQ1MzYwMTkzNjc4MDA4MDk3OTE5MTIxNDgxMDgwODM5OTAwMTMyMTY2NTU4NjAxMDAyMzQxMzc5NTczMzc3NjcwNDk2MDc2ODMxMzU0MzE2MjEzNDY3MDA1MDcyMDgxMDgwODcyODIxMzEwMzQ2NTc1MjgzMDYwNDEwMjA2NzUxNzk1NjM1MDY1NDM5MTQ5OTI1NTc5MjIzMjM4NTI0NzA5OTg5NzgzMTA0Njc3NTc0MDE1NjY5MjMxNjMxNDAwMjI5NTc2OTM1NTc3NzE5MTI1MDIwNDczOTcwNjU3MjgifSwicmV2b2NhdGlvbiI6eyJnIjoiMSAxMjI1OTQ3NDcyOTc2QjVBQTgyMjlDMjI4RUFFNUI3NThDMDlGNkIwN0I1NDU4MEJDNzYzNTMwNkJFQjI1ODkyIDEgMDlBMzEzOEIzMzZGMUZCQUJEMEIxM0E5QTNBNzM1RDMyQjRFN0YzREZCNTE1QzkwRkQwNzVGNTY5QTQ5RTUyMSAyIDA5NUU0NURERjQxN0QwNUZCMTA5MzNGRkM2M0Q0NzQ1NDhCN0ZGRkY3ODg4ODAyRjA3RkZGRkZGN0QwN0E4QTgiLCJnX2Rhc2giOiIxIDI0OTA1M0Y0NUJCQTg3MDA3NzhDRjE1ODQxREZERUIwQUY1OTQzRTFFMkYyRUVFMDNFQUI2RDgwOEFDMkYxRUUgMSAwNzQyQ0U3QjM3MjAzNTc1QThDNUQzMjc1NzY3RDkxNUQ2RUVDRkVDMzc5NzkzOTNGNjYxRkY1NDE0QzdENEJBIDEgMThEN0ExMEUzNjJGQTE5MkYxRDJFNTE3OUQ1ODBGMTk0RTM5NDEzMTE1MTE3MDBGQkE3OUE0QTIxMkUxN0Y5RiAxIDA3QzNDQTRFMjBDOTkyQTEwQ0NDNzI0NDlBQUU5NjRFMDFEREZCNTBGQ0VEQkUxRTZDOEQwRUMzOThGOTU3MUMgMiAwOTVFNDVEREY0MTdEMDVGQjEwOTMzRkZDNjNENDc0NTQ4QjdGRkZGNzg4ODgwMkYwN0ZGRkZGRjdEMDdBOEE4IDEgMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsImgiOiIxIDE5RUMwQkExMTRFMTBBN0Y5RkVCMkU3MzMyNTZDQzk4NTU0RkVEQzFGRjgzN0REQUEzNDQxN0U1NzYyNTg0NzkgMSAwMTYzN0VGQTBCQ0E2QjNENjZDMzE1RUU4QUJGNzNCMEU5MUVCNTA1REE1QUZCRDk2MTA5M0QwMjk5RjRGQUVEIDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCIsImgwIjoiMSAxQUMwQUY5NTBEN0UyRkE0MDNDN0UwRkI5RThDNTE4N0Q3QTBBMzFBMzgzQkFFNDVGRUEyMUFGNjZFOEM1MTkxIDEgMTMyNTZGMDBCODQyOTNGMjA3M0VCOUVFRTFCMUY4MDU4MTAyNkMzMkE1RjdGQThDNEExQTEwODMyNkE2Mzc0NSAyIDA5NUU0NURERjQxN0QwNUZCMTA5MzNGRkM2M0Q0NzQ1NDhCN0ZGRkY3ODg4ODAyRjA3RkZGRkZGN0QwN0E4QTgiLCJoMSI6IjEgMUFEMTUwOTJDMTNDNUI0NUY4MDk3OENFQ0ZCM0Y5NUUyMTNBMURBQ0NBNDMxRkEzNkE3MUU4OTg4MUE2RkVDMCAxIDI0NEEwRTMyODkyMjcxNTE5NjYwMEQ2NkYxRTU3MzA4NUE0MURDM0UxRkU4RjZEODU0OTY1RTNFMkFFNkY2RTIgMiAwOTVFNDVEREY0MTdEMDVGQjEwOTMzRkZDNjNENDc0NTQ4QjdGRkZGNzg4ODgwMkYwN0ZGRkZGRjdEMDdBOEE4IiwiaDIiOiIxIDBGMEZDRUJDNTE1MzI3MjEyNDI2QzIwNkY2ODQ4N0U5M0EyNjc1NTdCRjg5QUJENkIxNTBDMzA2MUM1RDNDRjcgMSAwN0I1OUVFMThFREY1RThFQzEzM0ZFMDY1N0RDRjEzMTAxMUYwQzFERUM1RjQ2NDU3MUMxMTIyMDc3RjMwMDVEIDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCIsImhfY2FwIjoiMSAyNDAzOTI2NjBDNjYxMDhCQjgzNDkwMDk1NzBGQTQ0MkFBRDAzQTY1MTU2NkQwMUE4MkVFRDQwMDNEMzY1QzY3IDEgMEMyQTE3Q0Q5MDM4QzE4QzU3Q0QyNDY4RTA2RENFNzU4NEE4Q0I4RTgwMzkzQjNBMjBCRkZERTY5MEQ0QzM2MCAxIDIzQ0ZFREE0NkJDMThEQUMzODdCRDdDMUQ2RjM1MzYyMEY2M0E0REI0OEM5MDBEQ0QxRkYzMEEzNkRDNjQwNEYgMSAwREJCQzQ3MUUyMzUwQ0I1QkU2RkFGN0RDOTBDQkQ3Q0MxRTkyOUUwQ0MzOUJCRDlBOUNBOEJFM0Y3REVCNkIwIDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCAxIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJodGlsZGUiOiIxIDAxQjQ4Mzg3QjlGRDlCQUQwRTg1RTJCMDFCNDI5OEJCNzNDQ0JBMjE5NzdDQTk4RDZFRDQ2MDM2ODNEODFDNjAgMSAxRTQwMzE1RDYyNTY1RDg4MjBFM0RGODRFMEUzRTE4RkI4OTFDNkJDNTJFOTJEMzhCNTIzMDNFOENFNDI4Q0U2IDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCIsInBrIjoiMSAwMjNCRDYzRDM0RkE0NjYwN0REMDgyN0Q2MjlDMzdCOEVBMEZFMjJFOUQwM0JCOTgzNjlBMTI1RjczRDc0OERGIDEgMEZFMEE3Nzg1MEYyRkNBOEFEREFCMTlCQTY0QUYzMEVFM0JBNTNDRTMwRTUxOEY3MzcxRkNFMTQ1NjQ1MTg0MCAyIDA5NUU0NURERjQxN0QwNUZCMTA5MzNGRkM2M0Q0NzQ1NDhCN0ZGRkY3ODg4ODAyRjA3RkZGRkZGN0QwN0E4QTgiLCJ1IjoiMSAwMkY0OUVBNzFGMjEzNkRERjNBNDkyRjMzRjFDRTYzMEFFNzQ1OTc3NDVDQkFEQkZGQzAyQjczNTVEOURBREJGIDEgMEU2OTNENjlFNUUzRDYxRjhGMDlFQ0UwRDJBMDgwOTUzQzkyNzMxOUY2OTQ0NTRGOTJFRjI0RDBBMDIwMjU0RCAxIDIxMTQxRDg0MTU2QkQxMkUzNEFDRDFDMkY3QTA2NzRCQkU0NUMwRjUyRjBFM0M3RUZEMjNDOURCNURDMDc2MjMgMSAxOUNENzY3NUQ5MTk0MzlFMjAxRDQzREFCNDdDREVDQUM1MTE0QkEzQzQyRTE4QkY1REQ5M0Y5N0UxMkNBNkMyIDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCAxIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJ5IjoiMSAwOUU5REJGQkI4QTUxNTZBQjdENjc5QkNCOEZCNkE0RkU3NDJENTVDMURBQzAzNzA2QTQ1QUQyOUFCM0VFNzBGIDEgMTg0OEI3RkYxOTQ4MUFEQjY4RkRFMDI0MEExRkFDOTVBODc0OTg3NUVFOTg4OUZENzlCMjU5NTRERUJDODI4QSAxIDEwODIzRkNDMkI0MTNFRDJFRUEzQkQ4RjJCRjNBMUZGMjc3QjdFQTRGNkI1RjBGMTZDODQ3M0QyRjc1RTc0MTUgMSAxMTJDRjc0NkVDNzVDMEU2MDY3MzJBQTQ1ODZCNDM3MDVDMUY2MEJDREQ5RjBDMTUwQzVGMDYyMkQ3NTNCMTU1IDIgMDk1RTQ1RERGNDE3RDA1RkIxMDkzM0ZGQzYzRDQ3NDU0OEI3RkZGRjc4ODg4MDJGMDdGRkZGRkY3RDA3QThBOCAxIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAifX19LCJkZWZpbml0aW9uSnNvblNjaGVtYUlkIjoiUHVibGljQ3JlZGVudGlhbERlZmluaXRpb25WMSIsImRlc2NyaXB0aW9uIjoiQmlydGggY2VydGlmaWNhdGUgQW5vbmNyZWQgQ3JlZGVudGlhbCBEZWZpbml0aW9uIiwiZ3VpZCI6ImVmNGE1ZGRjLTA2MTItMzRmNi1hZjI4LWM1ZDE0N2FhNDM4MCIsImlkIjoiYjc2YWUwOTMtN2U0ZS00NWE4LThmOTEtMTJhMmMzMGNjYjYzIiwia2V5Q29ycmVjdG5lc3NQcm9vZiI6eyJjIjoiMzA3NDE2NTUyNjQzOTM2NzgxODg3MDMzMzY5MTYxOTk3NTUzNDMyMjQ1OTE3OTA4MDk3Mjg2MTcxMTcwOTYyOTAzNTY5NDQ5MDU3NjYiLCJ4cl9jYXAiOltbImxvY2F0aW9uIiwiMzc2ODI3NzMxMDQ2MzM0MTkzOTYwMzU4OTkwMzgxMDA3NDc5Mzc0MjM2NDQ5OTg5ODA5MDY0MDg3NDg4MDk4MTQ2MDgzMjc5MjI5ODA5NDQ0MjM2OTQzODU2MTk0MjAzMjM0OTE0NTAxMzI4NTc5NDQwMjg3NzM1MTc5Mjk4NjMzNDg5MzIwNTU5NzE1MTk1MzU0MTYwMDg5MzY0MjA3MzMxMzQyNjAwODI2NDAzNDI5MjM0OTUxNjU3MDI4NTI0Nzc3ODc4NzQ2MzAyMDE0OTA3MDg0NTg3MTI1Nzg5NzMyMDY0NDA1NTQ2Mjc5NDczOTIyMTEyNDg2NzU3Nzk2MTQ0MTAwMDcyMjc2NjQ3NTc4NTQ2OTAxMjEwMDAzNzc2NDA4MTg1NTE1NzI5MzM4OTgzODY3NDQyNTEyMzI3NzYxMzYzMDAxODg0Mjc4NjI0MzYyOTQ5ODc3OTQ5MzA0MzAxMjkxODA2NTk5ODkyNzA2OTE3OTQ5OTM0NDI3ODQzODAxMTgxODQxMjcwOTM0ODQ1MTc0NDE2OTIwNzE2MTM3OTU2NjA0NDA4MDc0NTA0OTk4NjIxMjc1NTM0MTIyMjYzMTIyMTUyNDUxMzAyNzAyNzE2ODYwNDc5MDQ3NDk0MTE2NDc3NDAzMjkzOTk4NzUwNTcxNTQ3MjI5NTQ1NTEwNzM1MDYzODUyMDc3Mjg0OTYwMTgwMDU1MDA4NjA5Njg3Mjg0MjE2MzMwMDI4MzY1Njc2OTAzMDIzMzg2NDI3MDk2NTc0MDExNzMzNDAxMTY2NDI5MjA0NzQ2MzI2MDk2MjU2NjQ5MTE1NzUyMjczOTAyMDM5NDM0Mzc2Mjc2MTI3MTMyMzEzOTE3OTI3NTUzOTU4NDUxNDY5ODYwMTk1NjkzNDIwMjIxMzI1NTg1NTg2NzMxIl0sWyJiaXJ0aGRheSIsIjI1NTAwMDM2ODMzNDA5MzQxOTUzOTE4MDAyODI0MTA1MTEwMzA4OTA3NDMzODQzMTQ5OTE3ODc3MTUxNDc5NDY2MzAxOTQ5NjY4MzA1NTgxNTc0NDE2Nzk1OTkyMDYzNzAwMDM3NzY4NDIyMzIxNzk2NDE1OTA3NjY1MzQ4NzI2MTE2OTkzNzgxNDgxODIwNzAzODU4MTA0NDgzOTk0MjI4MDQ1NjUxODMzODY4MjgzMjg1NDkwMzIxMzk3NTQzMjQyNzY5OTU3ODY0ODMzMDAxNjk0OTA2MjUzOTIwOTY1NDU4MzcyNzcxNjczOTExNTk0ODQyNjAzNDM5NTMzMzQwMDc5MDQ2NjM1ODMzNDA1NzcyNDAwMzQyMjQ5MTA4NjA5OTkyMTk3ODY5NjY3MzE5MjA1MzQ5ODg4NTUyNDAzNDc0Mzc0NDgwMzMyMTU2NjQyMjI2OTQ3MDUzNDQwNTEzNzg5NjcwNzY5OTU1MzQ5NjM0NzMyOTkwOTgzMDk5NTkyOTIzNzQ0NTc1OTE5MTE2Mjk3MDY3NjU4OTE0ODgwODAyOTMyODc3OTkwNTQwNDYzMDAxMDE4OTU5MTg0MzA4NzE3OTMwNzQ1OTk2OTE0Mjc2MzA2MzQzNTkwODA3MDk2NTM1MDYxOTI2ODY3MTU0NjQ2NDgzNDU4ODQ1NjY2NjQxNjU4MjA4NDM3MzA1MDM4OTc0NjQ4MDkzMTUyNjg1NTk0NDEwNzk2Mzg2OTI5MDM4MDcwOTQ2ODg1ODYxMzYyMDI2NjE0MTUxMDMxNjQ1OTY1NzMwOTgxNjY5NDc4MTQyOTEyMTMzNzQ1MDc3NTM3NDY4NDQ0MzU0MjA2NTA3NTU4OTE5NTQ5OTUxNTQ0MzI4MTkwMDQwMzA2MzQ3NDc0NzcwNzY1NTU5MDA1MTA4ODgwOSJdLFsibWFzdGVyX3NlY3JldCIsIjQ5MDA3ODIyMDM2OTkwODI2ODQxMTkwMzM3MTU4ODI4Nzc1NzE0NjM3MDc3OTg5Njg0MTA5NTc0NjA4ODQ3MjA2MzIxMTA2ODQxMzcwOTkxNTQ2NTMwMjYwMDIzNDU1MDk4OTcyNjcyNjUyNDQzMDQ1NzI2MTM3MTk2Nzc5NzUxNjQ2MDQyOTk3NjgyMzYyMTQxMTA1MjY5NTQ2MTkxMjMwNzcyODc5MzczNjcxNTY1ODk5NjAxMjQ2MjYwMDMzNjY4OTQyODY1MjgyMTg3NzA3OTc2NDcyNjg3NzU5NTA1NjQ4NjgyNzc0NTI3NzAzNzQyNjQxODc2MTgwOTU4OTI3MzMxNjcwNDAxMzI4ODI4Mzc2MjMxMTk4NzI3ODUyMzU4NTUxNzUzNTEwNTk1MzQwNjI4MTA0MjkxMDU5ODI4NDU2NTUwOTQyNjI0MjA3OTA4ODMyNzQ5NjM5ODQwODIxNzEyNTU1Mjg2MDE5Mjk2MTY4OTQ0NjEzNjg3NDM5MzU5NTMxNjA2NzQ3MDc3Mjc3MDE4MDE5MzU1MjM4NzUyMzUwMTYzODE3MzQ5ODI1ODY4ODQ0NTk0ODU5NDQ3Mzg0NDA0NjMxMDk3NjkzNTQ5NTk0NjkwNDcyMTI3Nzg1MTg4NDI5MTAxNjE5MDI4MTc1NjI3MzIzOTUzNTAyOTUwNzk3NTIwMDQ0OTE2MDY5MDg4MjczODU3NzYwNzU5MDkxNTQyNDQ4ODc4MzU5NTc1NDk5MjM1MzIzODc4NDA1MjIwNDc5MzMxMzE5MDE0ODg2NDk5NjE0MzI2MTA3MTM2MjA1ODg4Njg3ODU4Njg4NTgxNTI4NjQxMzI4ODg1NzkwNTc0MjAwNjUxMzA0NzU4NTE0MTAzNzAzNDM2NDAwMDM0NTQ4MjAxMDc5NDM4MzQ2NjU5OCJdXSwieHpfY2FwIjoiNDk2Njg1Mjk0NTI0Mjc4Njg0NDc0OTQ1MDQ0MzI5OTU5MjI0NTA4NzY1NjMzNTg4MDQzNDUwNDgxNTE4MzQ5ODk4NDAxMjk3MzQ2OTk0MjQ5ODEwMjIzODM3MzE0MDc0NDI1NDQ4NzU1OTYwMjg2MTU3ODY4NTk0NDU2MDE2Mjc3NTcyMjAxMDgwNDQzMTA3NjQxNjM5MzExNDAyMDUyOTM3NjczMzI5MDM4NjU0Nzc2MDI1MTA0OTYyMTEwMTQzNTg5NjA3NjEyOTM2NDI4MDc3MjQ1Nzg4Mjk2OTc2MzU5ODk5MjMwMjQwNTEyMjc5MzU0MDg2MTMwNjIyNjYzMjgzMTA4Njk4MTM0NjU0NTMzMTQ1NjY4NDU1NjExODU2MDY2Mjc2OTgxMjUwMDAyMTQ2ODgwNTk0NzM3MzU2MjA2MDE1NzUyMDAxNjE3ODU2OTI0MDk3NjU3ODU2NjkzNTYwODA1ODQ0MDQ3NTY3ODE2NTczNDUxNTg1NTUzODMzMzQ0ODE0NDMzMzMxNzk2ODc5Mzg5NjgyODM3MjQ3OTU0MDUwOTcwNzE0Mjg2NDQwNDg2NjY5MDE4MjMzODkxNTA0ODE0ODk3NDAyODYxNzMxMDg2NjEzMTU5MDM0NDEzNTIzMzk2NjE0NjE4NjQ3NjczNzgzMDIyMTAzMTQxNTg0MTAwMDY0NTE0NjA4ODYxMTY2Nzg3NjYzODMxMjE5MTM0NTYwNDAyNTEzMjQ5MDg0NzA5NDM2ODM5NTM5NTQ3MzQwNzMyNDYyOTE4MDg3NzU3MTYyOTQ2Mzk5MTMzNzA0MTA0MzMwNzUxNzU3OTMxMTE0OTkwNzA5NjU3NDQ3MjU0OTM1OTc3MDExNjQzMTc5NjU4MTg5MzI3MDg4NDY5NjExMzcxMTg1OTY2MzIwNzU3ODM1In0sImtleUNvcnJlY3RuZXNzUHJvb2ZKc29uU2NoZW1hSWQiOiJQcm9vZktleUNyZWRlbnRpYWxEZWZpbml0aW9uVjEiLCJuYW1lIjoiQmlydGggQ2VydGlmaWNhdGUgbG9jYXRpb24iLCJyZXNvbHV0aW9uTWV0aG9kIjoiZGlkIiwic2NoZW1hSWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODUvc2NoZW1hLXJlZ2lzdHJ5L3NjaGVtYXMvZGFjZjNiOGUtODllYi0zYWQ2LWExNDYtMTIyZGRhN2Q2MjY0L3NjaGVtYSIsInNpZ25hdHVyZVR5cGUiOiJDTCIsInN1cHBvcnRSZXZvY2F0aW9uIjp0cnVlLCJ0YWciOiJMaWNlbmNlIiwidmVyc2lvbiI6IjEuMC4wIn0=", +"url": "did:prism:d62c1e6bed7baf3b8071bfa9752484f8984e7531fc2c50bb948918af05ab2019?resourceService=agent-base-url&resourcePath=credential-definition-registry/definitions/did-url/ef4a5ddc-0612-34f6-af28-c5d147aa4380?resourceHash=ca8ea2c80ff1e07978e2ed59245186fbb9992daedbecb651535ad5996be372c1" + +``` + Remember, in the Cloud Agent, the combination of author, id, and version uniquely identifies each credential definition. Thus, using the same agent DID as the author, you cannot establish another credential definition with identical id and version values. ### 4. Update the Credential Definition @@ -141,5 +167,4 @@ To update or upgrade an existing credential definition, follow the steps outline 2. Update the `version` value to reflect the changes made. This is important to ensure that each version of the credential definition remains distinct. 3. Create a new credential definition entry with the updated version and schema. -Note: When you make changes to an existing credential definition, it's essential to version the new entry accurately. This ensures clarity and avoids potential conflicts or misunderstandings among different versions of the same definition. - +Note: When you make changes to an existing credential definition, it's essential to version the new entry accurately. This ensures clarity and avoids potential conflicts or misunderstandings among different versions of the same definition. \ No newline at end of file diff --git a/docs/docusaurus/credentials/issue.md b/docs/docusaurus/credentials/issue.md index b392d310c2..ff9beeae99 100644 --- a/docs/docusaurus/credentials/issue.md +++ b/docs/docusaurus/credentials/issue.md @@ -134,8 +134,11 @@ curl -X 'POST' \ 1. `claims`: The data stored in a verifiable credential. AnonCreds claims get expressed in a flat, "string -> string", key-value pair format. The claims contain the data that the issuer attests to, such as name, address, date of birth, and so on. 2. `connectionId`: The unique ID of the connection between the holder and the issuer to offer this credential over. 3. `credentialDefinitionId`: The unique ID of the [credential definition](../credentialdefinition/credential-definition.md) that has been created by the issuer as a prerequisite. Please refer to the [Create AnonCreds Credential Definition](../credentialdefinition/credential-definition.md) doc for details on how to create a credential definition. +:::note +📌 Note: If the credential definition was created via HTTP URL endpoint, then this credential definition will be referenced to that credential via HTTP URL, and if this credential definition was created via DID URL endpoint, then it will be referenced via DID URL, How to create credential definition for HTTP URL or DID URL is explained in [credential definition creation guide](../credentialdefinition/create.md) +::: 4. `credentialFormat`: The format of the credential that will be issued - `AnonCreds` in this case. - +5. `issuingDID`: The DID referring to the issuer to issue this credential from :::note The `connectionId` and `credentialDefinitionId` properties come from completing the pre-requisite steps listed above ::: @@ -159,6 +162,7 @@ curl -X 'POST' \ "drivingClass": "3" }, "credentialFormat": "AnonCreds", + "issuingDID": "did:prism:9f847f8bbb66c112f71d08ab39930d468ccbfe1e0e1d002be53d46c431212c26", "connectionId": "9d075518-f97e-4f11-9d10-d7348a7a0fda", "credentialDefinitionId": "5d737816-8fe8-3492-bfe3-1b3e2b67220b" }' diff --git a/docs/docusaurus/schemas/create.md b/docs/docusaurus/schemas/create.md index edfd7454f3..ad34b1f2c4 100644 --- a/docs/docusaurus/schemas/create.md +++ b/docs/docusaurus/schemas/create.md @@ -79,7 +79,9 @@ Specification. 1. Open your preferred REST API client, such as Postman or Insomnia, or use the client stub generated based on the OpenAPI specification. -2. In the client, create a new POST request to the `/cloud-agent/schema-registry/schemas` endpoint. +2. In the client, create a new POST request to either `/cloud-agent/schema-registry/schemas` or `/cloud-agent/schema-registry/schemas/did-url` endpoints. They both take the same payload. + 1. `/cloud-agent/schema-registry/schemas` creates a schema that can later be resolved via HTTP URL + 2. `/cloud-agent/schema-registry/schemas/did-url` creates a schema that can later be resolved via [DID URL](/docs/concepts/glossary#did-url), the DID includes a service endpoint with the location of the schema registry. Note that the value of the `author` field must match the short form of a PRISM DID that has been created using the same agent. An unpublished DID is sufficient. Please refer to the [Create DID](../dids/create.md) documentation page for more details on how to create a PRISM DID. @@ -252,6 +254,15 @@ curl -X 'POST' \ } ``` +or in case of DID url, the response will be created schema wrapped in [Prism Envelope](/docs/concepts/glossary#prism-envelope) + +```json +{ + "resource":"eyJhdXRob3IiOiJkaWQ6cHJpc206ZTAyNjZlZThkODBhMDAxNjNlNWY5MjJkYzI1NjdhYjk2MTE3MjRhMDBkYjkyNDIzMzAxMTU0MjgyMTY5ZGZmOSIsImF1dGhvcmVkIjoiMjAyNC0wOS0yNVQxMDozNzoxNi4wOTM2MDlaIiwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UgU2NoZW1hIiwiZ3VpZCI6IjVjOTNmYTAwLWUwM2UtMzlkZC05NDdmLTI2NWI4YzFlYWQ4YiIsImlkIjoiNjhmMGQ4MDctYTcyYi00OTY2LTg1NWItMmIzNGJjMjYzNzAyIiwibmFtZSI6ImRyaXZpbmctbGljZW5zZSIsInJlc29sdXRpb25NZXRob2QiOiJkaWQiLCJzY2hlbWEiOnsiJGlkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9kcml2aW5nLWxpY2Vuc2UtMS4wLjAiLCIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6dHJ1ZSwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UiLCJwcm9wZXJ0aWVzIjp7ImRhdGVPZklzc3VhbmNlIjp7ImZvcm1hdCI6ImRhdGUtdGltZSIsInR5cGUiOiJzdHJpbmcifSwiZHJpdmluZ0NsYXNzIjp7InR5cGUiOiJpbnRlZ2VyIn0sImRyaXZpbmdMaWNlbnNlSUQiOnsidHlwZSI6InN0cmluZyJ9LCJlbWFpbEFkZHJlc3MiOnsiZm9ybWF0IjoiZW1haWwiLCJ0eXBlIjoic3RyaW5nIn0sImZhbWlseU5hbWUiOnsidHlwZSI6InN0cmluZyJ9LCJnaXZlbk5hbWUiOnsidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsiZW1haWxBZGRyZXNzIiwiZmFtaWx5TmFtZSIsImRhdGVPZklzc3VhbmNlIiwiZHJpdmluZ0xpY2Vuc2VJRCIsImRyaXZpbmdDbGFzcyJdLCJ0eXBlIjoib2JqZWN0In0sInRhZ3MiOlsiZHJpdmluZyIsImxpY2Vuc2UiXSwidHlwZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtanNvbi1zY2hlbWFzL3NjaGVtYS8yLjAvc2NoZW1hLmpzb24iLCJ2ZXJzaW9uIjoiMS4wLjAifQ==", + "url":"did:prism:e0266ee8d80a00163e5f922dc2567ab9611724a00db92423301154282169dff9?resourceService=agent-base-url&resourcePath=schema-registry/schemas/did-url/5c93fa00-e03e-39dd-947f-265b8c1ead8b?resourceHash=d1557ede168f0f91097933aa2080edaf2f14fddd8a7362a22add97e431c4efe2" +} +``` + ### 3. Retrieve the created schema To retrieve the newly created schema, create a new GET request to the `/cloud-agent/schema-registry/schemas/{guid}` @@ -265,7 +276,17 @@ curl -X 'GET' \ -H "apikey: $API_KEY" ``` -The response should contain the JSON object representing the schema you just created. +or if you need to resolve a schema created via DID url, the endpoint will look like this `/cloud-agent/schema-registry/schemas/did-url/{guid}` + +```schell +curl -X 'GET' \ + 'http://localhost:8080/cloud-agent/schema-registry/schemas/did-url/3f86a73f-5b78-39c7-af77-0c16123fa9c2' \ + -H 'accept: application/json' \ + -H "apikey: $API_KEY" + +``` + +The response for HTTP URL request should contain the JSON object representing the schema you just created. ```json { @@ -323,6 +344,18 @@ The response should contain the JSON object representing the schema you just cre } ``` +and for DID URL request, response will include the same schema wrapped in [Prism envelope](/docs/concepts/glossary#prism-envelope) response + +```json +{ + "resource":"eyJhdXRob3IiOiJkaWQ6cHJpc206ZTAyNjZlZThkODBhMDAxNjNlNWY5MjJkYzI1NjdhYjk2MTE3MjRhMDBkYjkyNDIzMzAxMTU0MjgyMTY5ZGZmOSIsImF1dGhvcmVkIjoiMjAyNC0wOS0yNVQxMDozNzoxNi4wOTM2MDlaIiwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UgU2NoZW1hIiwiZ3VpZCI6IjVjOTNmYTAwLWUwM2UtMzlkZC05NDdmLTI2NWI4YzFlYWQ4YiIsImlkIjoiNjhmMGQ4MDctYTcyYi00OTY2LTg1NWItMmIzNGJjMjYzNzAyIiwibmFtZSI6ImRyaXZpbmctbGljZW5zZSIsInJlc29sdXRpb25NZXRob2QiOiJkaWQiLCJzY2hlbWEiOnsiJGlkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9kcml2aW5nLWxpY2Vuc2UtMS4wLjAiLCIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6dHJ1ZSwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UiLCJwcm9wZXJ0aWVzIjp7ImRhdGVPZklzc3VhbmNlIjp7ImZvcm1hdCI6ImRhdGUtdGltZSIsInR5cGUiOiJzdHJpbmcifSwiZHJpdmluZ0NsYXNzIjp7InR5cGUiOiJpbnRlZ2VyIn0sImRyaXZpbmdMaWNlbnNlSUQiOnsidHlwZSI6InN0cmluZyJ9LCJlbWFpbEFkZHJlc3MiOnsiZm9ybWF0IjoiZW1haWwiLCJ0eXBlIjoic3RyaW5nIn0sImZhbWlseU5hbWUiOnsidHlwZSI6InN0cmluZyJ9LCJnaXZlbk5hbWUiOnsidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsiZW1haWxBZGRyZXNzIiwiZmFtaWx5TmFtZSIsImRhdGVPZklzc3VhbmNlIiwiZHJpdmluZ0xpY2Vuc2VJRCIsImRyaXZpbmdDbGFzcyJdLCJ0eXBlIjoib2JqZWN0In0sInRhZ3MiOlsiZHJpdmluZyIsImxpY2Vuc2UiXSwidHlwZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtanNvbi1zY2hlbWFzL3NjaGVtYS8yLjAvc2NoZW1hLmpzb24iLCJ2ZXJzaW9uIjoiMS4wLjAifQ==", + "url":"did:prism:e0266ee8d80a00163e5f922dc2567ab9611724a00db92423301154282169dff9?resourceService=agent-base-url&resourcePath=schema-registry/schemas/did-url/5c93fa00-e03e-39dd-947f-265b8c1ead8b?resourceHash=d1557ede168f0f91097933aa2080edaf2f14fddd8a7362a22add97e431c4efe2" +} +``` + +Schemas created for HTTP URL (`/cloud-agent/schema-registry/schemas`) will not be resolvable by endpoint that returns schemas created for DID URL (`/cloud-agent/schema-registry/schemas/did-url`) and vice verca. + + The Cloud Agent instance's triple `author`, `id`, and `version` are unique. So, having a single [DID](/docs/concepts/glossary#decentralized-identifier) reference that the author uses, creating the credential schema with the same `id` and `version` is impossible. diff --git a/docs/docusaurus/schemas/credential-schema.md b/docs/docusaurus/schemas/credential-schema.md index 56aff05830..641de1011f 100644 --- a/docs/docusaurus/schemas/credential-schema.md +++ b/docs/docusaurus/schemas/credential-schema.md @@ -219,14 +219,56 @@ A valid [ANONCRED-SCHEMA](https://hyperledger.github.io/anoncreds-spec/#term:sch ```json { - "name": "Birth Certificate Schema", - "version": "1.0", - "attrNames": [ - "location", - "birthday" - ], - "issuerId": "did:prism:4a5b5cf0a513e83b598bbea25cd6196746747f361a73ef77068268bc9bd732ff" -} + "name":"anoncred-birthday-cert", + "version":"1.0.0", + "description":"Birthday certificate", + "type":"AnoncredSchemaV1", + "author":"did:prism:e0266ee8d80a00163e5f922dc2567ab9611724a00db92423301154282169dff9", + "tags":[ + "birth", + "certificate" + ], + "schema":{ + "$schema":"https://json-schema.org/draft/2020-12/schema", + "type":"object", + "properties":{ + "name":{ + "type":"string", + "minLength":1 + }, + "version":{ + "type":"string", + "minLength":1 + }, + "attrNames":{ + "type":"array", + "items":{ + "type":"string", + "minLength":1 + }, + "minItems":1, + "maxItems":125, + "uniqueItems":true + }, + "issuerId":{ + "type":"string", + "minLength":1 + } + }, + "name":"Birth Certificate Schema", + "version":"1.0", + "attrNames":[ + "location", + "birthday" + ], + "issuerId":"did:prism:e0266ee8d80a00163e5f922dc2567ab9611724a00db92423301154282169dff9" + }, + "required":[ + "name", + "version" + ], + "additionalProperties":true + } ``` --- diff --git a/docs/docusaurus/schemas/update.md b/docs/docusaurus/schemas/update.md index 17010d13cc..9235b3499d 100644 --- a/docs/docusaurus/schemas/update.md +++ b/docs/docusaurus/schemas/update.md @@ -108,8 +108,8 @@ The JSON Schema changes must be defined as follows: 1. Open your preferred REST API client, such as Postman or Insomnia, or use the client stub generated based on the OpenAPI specification. -2. In the client, create a new PUT request to the `/cloud-agent/schema-registry/schemas/{id}` endpoint, where `id` is a - locally unique credential schema id, formatted as a URL. +2. In the client, create a new PUT request to either `/cloud-agent/schema-registry/schemas/{id}` or `/cloud-agent/schema-registry/schemas/did-url/{id}` endpoint, where `id` is a locally unique credential schema id, formatted as a URL, they both take same payload. + 1. When updating a schema, it is imporant to consider if the schema is `HTTP URL` resolvable or `DID URL` resolvable, the `id` in this endpoint needs to be used accordingly, if schema is `HTTP URL` resolvable, then only `/cloud-agent/schema-registry/schemas/{id}` can be used to update it, and same with regards to `DID URL` Note that the value of the `author` field must match the short form of a PRISM DID that has been created using the same agent. An unpublished DID is sufficient. Please refer to the [Create DID](../dids/create.md) documentation page for more details on how to create a PRISM DID. @@ -323,4 +323,12 @@ curl -X 'PUT' \ "kind": "CredentialSchema", "self": "/schema-registry/schemas/3f86a73f-5b78-39c7-af77-0c16123fa9c2" } -``` \ No newline at end of file +``` + +If you are updating schema that is DID URL resolvable, the response will be in a forom of [Prism Envelope](/docs/concepts/glossary#prism-envelope), like this: + +```json +{ + "resource":"eyJhdXRob3IiOiJkaWQ6cHJpc206ZTAyNjZlZThkODBhMDAxNjNlNWY5MjJkYzI1NjdhYjk2MTE3MjRhMDBkYjkyNDIzMzAxMTU0MjgyMTY5ZGZmOSIsImF1dGhvcmVkIjoiMjAyNC0wOS0yNVQxMDozNzoxNi4wOTM2MDlaIiwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UgU2NoZW1hIiwiZ3VpZCI6IjVjOTNmYTAwLWUwM2UtMzlkZC05NDdmLTI2NWI4YzFlYWQ4YiIsImlkIjoiNjhmMGQ4MDctYTcyYi00OTY2LTg1NWItMmIzNGJjMjYzNzAyIiwibmFtZSI6ImRyaXZpbmctbGljZW5zZSIsInJlc29sdXRpb25NZXRob2QiOiJkaWQiLCJzY2hlbWEiOnsiJGlkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9kcml2aW5nLWxpY2Vuc2UtMS4wLjAiLCIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6dHJ1ZSwiZGVzY3JpcHRpb24iOiJEcml2aW5nIExpY2Vuc2UiLCJwcm9wZXJ0aWVzIjp7ImRhdGVPZklzc3VhbmNlIjp7ImZvcm1hdCI6ImRhdGUtdGltZSIsInR5cGUiOiJzdHJpbmcifSwiZHJpdmluZ0NsYXNzIjp7InR5cGUiOiJpbnRlZ2VyIn0sImRyaXZpbmdMaWNlbnNlSUQiOnsidHlwZSI6InN0cmluZyJ9LCJlbWFpbEFkZHJlc3MiOnsiZm9ybWF0IjoiZW1haWwiLCJ0eXBlIjoic3RyaW5nIn0sImZhbWlseU5hbWUiOnsidHlwZSI6InN0cmluZyJ9LCJnaXZlbk5hbWUiOnsidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsiZW1haWxBZGRyZXNzIiwiZmFtaWx5TmFtZSIsImRhdGVPZklzc3VhbmNlIiwiZHJpdmluZ0xpY2Vuc2VJRCIsImRyaXZpbmdDbGFzcyJdLCJ0eXBlIjoib2JqZWN0In0sInRhZ3MiOlsiZHJpdmluZyIsImxpY2Vuc2UiXSwidHlwZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtanNvbi1zY2hlbWFzL3NjaGVtYS8yLjAvc2NoZW1hLmpzb24iLCJ2ZXJzaW9uIjoiMS4wLjAifQ==", + "url":"did:prism:e0266ee8d80a00163e5f922dc2567ab9611724a00db92423301154282169dff9?resourceService=agent-base-url&resourcePath=schema-registry/schemas/did-url/5c93fa00-e03e-39dd-947f-265b8c1ead8b?resourceHash=d1557ede168f0f91097933aa2080edaf2f14fddd8a7362a22add97e431c4efe2" +} \ No newline at end of file From 658e094a6c493b5e33c383c508e8439f6c0047bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 01:31:08 +0000 Subject: [PATCH 07/13] build: zio dependency updates (#1369) Signed-off-by: Hyperledger Bot Co-authored-by: Hyperledger Bot Co-authored-by: Yurii Shynbuiev --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index e96b09c440..ca50625deb 100644 --- a/build.sbt +++ b/build.sbt @@ -49,7 +49,7 @@ lazy val V = new { val zioConfig = "4.0.2" val zioLogging = "2.3.1" val zioJson = "0.7.3" - val zioHttp = "3.0.0" + val zioHttp = "3.0.1" val zioCatsInterop = "3.3.0" // TODO "23.1.0.2" // https://mvnrepository.com/artifact/dev.zio/zio-interop-cats val zioMetricsConnector = "2.3.1" val zioMock = "1.0.0-RC12" From 65cc9a712af722f5cb3dd36e78b088c20723097b Mon Sep 17 00:00:00 2001 From: patlo-iog Date: Mon, 30 Sep 2024 17:43:18 +0700 Subject: [PATCH 08/13] fix: oid4vci endpoints error statuses and negative input validation (#1384) Signed-off-by: Pat Losoponkul --- .../oid4vci/CredentialIssuerEndpoints.scala | 10 +++++++- .../CredentialIssuerController.scala | 10 ++++---- ...dDIDServiceWithEventNotificationImpl.scala | 3 +-- .../util/ManagedDIDTemplateValidator.scala | 7 ++++-- .../model/error/CredentialSchemaError.scala | 7 ++++++ .../core/model/schema/CredentialSchema.scala | 16 +++++++++---- .../core/service/CredentialServiceImpl.scala | 1 - .../core/service/GenericUriResolverImpl.scala | 3 +-- .../OID4VCIIssuerMetadataService.scala | 24 ++++++++++++++++--- .../service/uriResolvers/DidUrlResolver.scala | 3 +-- ...ID4VCIIssuerMetadataServiceSpecSuite.scala | 2 +- .../identus/pollux/sql/model/db/package.scala | 6 ++--- .../shared/http/GenericUriResolver.scala | 3 +-- .../identus/shared/models/Failure.scala | 1 + 14 files changed, 69 insertions(+), 27 deletions(-) diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala index b49c11068a..1920718961 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.oid4vci import org.hyperledger.identus.api.http.{EndpointOutputs, ErrorResponse, RequestContext} +import org.hyperledger.identus.api.http.EndpointOutputs.FailureVariant import org.hyperledger.identus.iam.authentication.apikey.ApiKeyCredentials import org.hyperledger.identus.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader import org.hyperledger.identus.iam.authentication.oidc.JwtCredentials @@ -197,7 +198,14 @@ object CredentialIssuerEndpoints { statusCode(StatusCode.Created).description("Credential configuration created successfully") ) .out(jsonBody[CredentialConfiguration]) - .errorOut(EndpointOutputs.basicFailureAndNotFoundAndForbidden) + .errorOut( + EndpointOutputs.basicFailuresWith( + FailureVariant.notFound, + FailureVariant.unauthorized, + FailureVariant.forbidden, + FailureVariant.conflict + ) + ) .name("createCredentialConfiguration") .summary("Create a new credential configuration") .description( diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala index 69300821ec..c60a371342 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala @@ -125,10 +125,12 @@ case class CredentialIssuerControllerImpl( import CredentialIssuerController.Errors.* import OIDCCredentialIssuerService.Errors.* - private def parseURL(url: String): IO[ErrorResponse, URL] = + private def parseAbsoluteURL(url: String): IO[ErrorResponse, URL] = ZIO - .attempt(URI.create(url).toURL()) + .attempt(URI.create(url)) .mapError(ue => badRequest(detail = Some(s"Invalid URL: $url"))) + .filterOrFail(_.isAbsolute())(badRequest(detail = Some(s"Relative URL '$url' is not allowed"))) + .map(_.toURL()) private def baseCredentialIssuerUrl(issuerId: UUID): URL = URI(s"$agentBaseUrl/oid4vci/issuers/$issuerId").toURL() @@ -255,7 +257,7 @@ case class CredentialIssuerControllerImpl( request: CreateCredentialIssuerRequest ): ZIO[WalletAccessContext, ErrorResponse, CredentialIssuer] = for { - authServerUrl <- parseURL(request.authorizationServer.url) + authServerUrl <- parseAbsoluteURL(request.authorizationServer.url) id = request.id.getOrElse(UUID.randomUUID()) issuerToCreate = PolluxCredentialIssuer( id, @@ -287,7 +289,7 @@ case class CredentialIssuerControllerImpl( maybeAuthServerUrl <- ZIO .succeed(request.authorizationServer.flatMap(_.url)) .flatMap { - case Some(url) => parseURL(url).asSome + case Some(url) => parseAbsoluteURL(url).asSome case None => ZIO.none } issuer <- issuerMetadataService.updateCredentialIssuer( diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala index 6938ef3547..795e6d6199 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala @@ -3,8 +3,7 @@ package org.hyperledger.identus.agent.walletapi.service import org.hyperledger.identus.agent.walletapi.model.error.CommonWalletStorageError import org.hyperledger.identus.agent.walletapi.model.ManagedDIDDetail import org.hyperledger.identus.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage, WalletSecretStorage} -import org.hyperledger.identus.castor.core.model.did.CanonicalPrismDID -import org.hyperledger.identus.castor.core.model.did.Service as DidDocumentService +import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, Service as DidDocumentService} import org.hyperledger.identus.castor.core.model.error.DIDOperationError import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.castor.core.util.DIDOperationValidator diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala index 3e9385ae86..dad26a4b10 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/ManagedDIDTemplateValidator.scala @@ -2,8 +2,11 @@ package org.hyperledger.identus.agent.walletapi.util import org.hyperledger.identus.agent.walletapi.model.ManagedDIDTemplate import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService -import org.hyperledger.identus.castor.core.model.did.{EllipticCurve, VerificationRelationship} -import org.hyperledger.identus.castor.core.model.did.Service as DidDocumentService +import org.hyperledger.identus.castor.core.model.did.{ + EllipticCurve, + Service as DidDocumentService, + VerificationRelationship +} object ManagedDIDTemplateValidator { diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala index e1b1acd1f2..f2eddcf151 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.core.model.error +import org.hyperledger.identus.shared.http.GenericUriResolverError import org.hyperledger.identus.shared.json.JsonSchemaError import org.hyperledger.identus.shared.models.{Failure, StatusCode} @@ -46,4 +47,10 @@ object CredentialSchemaError { StatusCode.BadRequest, s"Unsupported credential schema type: ${`type`}" ) + + final case class SchemaDereferencingError(cause: GenericUriResolverError) + extends CredentialSchemaError( + StatusCode.InternalServerError, + s"The schema was not successfully dereferenced: cause=[${cause.userFacingMessage}]" + ) } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala index d81c821369..092fb1043b 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala @@ -120,9 +120,14 @@ object CredentialSchema { given JsonEncoder[CredentialSchema] = DeriveJsonEncoder.gen[CredentialSchema] given JsonDecoder[CredentialSchema] = DeriveJsonDecoder.gen[CredentialSchema] - def resolveJWTSchema(uri: URI, uriResolver: UriResolver): IO[CredentialSchemaParsingError, Json] = { + def resolveJWTSchema( + uri: URI, + uriResolver: UriResolver + ): IO[CredentialSchemaParsingError | SchemaDereferencingError, Json] = { for { - content <- uriResolver.resolve(uri.toString).orDieAsUnmanagedFailure + content <- uriResolver + .resolve(uri.toString) + .mapError(SchemaDereferencingError(_)) json <- ZIO .fromEither(content.fromJson[Json]) .mapError(error => CredentialSchemaParsingError(error)) @@ -132,7 +137,7 @@ object CredentialSchema { def validSchemaValidator( schemaId: String, uriResolver: UriResolver - ): IO[InvalidURI | CredentialSchemaParsingError, JsonSchemaValidator] = { + ): IO[InvalidURI | CredentialSchemaParsingError | SchemaDereferencingError, JsonSchemaValidator] = { for { uri <- ZIO.attempt(new URI(schemaId)).mapError(_ => InvalidURI(schemaId)) json <- resolveJWTSchema(uri, uriResolver) @@ -153,7 +158,10 @@ object CredentialSchema { schemaId: String, credentialSubject: String, uriResolver: UriResolver - ): IO[InvalidURI | CredentialSchemaParsingError | CredentialSchemaValidationError, Unit] = { + ): IO[ + InvalidURI | CredentialSchemaParsingError | CredentialSchemaValidationError | SchemaDereferencingError, + Unit + ] = { for { schemaValidator <- validSchemaValidator(schemaId, uriResolver) _ <- schemaValidator.validate(credentialSubject).mapError(CredentialSchemaValidationError.apply) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala index a95ead74b0..4e059f8e05 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala @@ -4,7 +4,6 @@ import cats.implicits.* import io.circe.* import io.circe.parser.* import io.circe.syntax.* -import io.circe.Json import org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, PublicationState} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala index 74d6fa0131..4a0113d0ad 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/GenericUriResolverImpl.scala @@ -2,8 +2,7 @@ package org.hyperledger.identus.pollux.core.service import org.hyperledger.identus.pollux.core.service.uriResolvers.* import org.hyperledger.identus.pollux.vc.jwt.DidResolver -import org.hyperledger.identus.shared.http.{GenericUriResolver, GenericUriResolverError, UriResolver} -import org.hyperledger.identus.shared.http.DataUrlResolver +import org.hyperledger.identus.shared.http.{DataUrlResolver, GenericUriResolver, GenericUriResolverError, UriResolver} import zio.* import zio.http.* diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala index 532000c850..e9be6a2cb5 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala @@ -1,12 +1,17 @@ package org.hyperledger.identus.pollux.core.service -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.{CredentialSchemaParsingError, InvalidURI} +import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.{ + CredentialSchemaParsingError, + InvalidURI, + SchemaDereferencingError +} import org.hyperledger.identus.pollux.core.model.oid4vci.{CredentialConfiguration, CredentialIssuer} import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema import org.hyperledger.identus.pollux.core.model.CredentialFormat import org.hyperledger.identus.pollux.core.repository.OID4VCIIssuerMetadataRepository import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataServiceError.{ CredentialConfigurationNotFound, + DuplicateCredentialConfigId, InvalidSchemaId, IssuerIdNotFound, UnsupportedCredentialFormat @@ -45,6 +50,12 @@ object OID4VCIIssuerMetadataServiceError { s"Invalid schemaId $schemaId. $msg" ) + final case class DuplicateCredentialConfigId(id: String) + extends OID4VCIIssuerMetadataServiceError( + StatusCode.Conflict, + s"Duplicated credential configuration id: $id" + ) + final case class UnsupportedCredentialFormat(format: CredentialFormat) extends OID4VCIIssuerMetadataServiceError( StatusCode.BadRequest, @@ -68,7 +79,11 @@ trait OID4VCIIssuerMetadataService { format: CredentialFormat, configurationId: String, schemaId: String - ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat | IssuerIdNotFound, CredentialConfiguration] + ): ZIO[ + WalletAccessContext, + InvalidSchemaId | UnsupportedCredentialFormat | IssuerIdNotFound | DuplicateCredentialConfigId, + CredentialConfiguration + ] def getCredentialConfigurations( issuerId: UUID ): IO[IssuerIdNotFound, Seq[CredentialConfiguration]] @@ -130,11 +145,13 @@ class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataReposito schemaId: String ): ZIO[ WalletAccessContext, - InvalidSchemaId | UnsupportedCredentialFormat | IssuerIdNotFound, + InvalidSchemaId | UnsupportedCredentialFormat | IssuerIdNotFound | DuplicateCredentialConfigId, CredentialConfiguration ] = { for { _ <- getCredentialIssuer(issuerId) + _ <- getCredentialConfigurationById(issuerId, configurationId).flip + .mapError(_ => DuplicateCredentialConfigId(configurationId)) _ <- format match { case CredentialFormat.JWT => ZIO.unit case f => ZIO.fail(UnsupportedCredentialFormat(f)) @@ -144,6 +161,7 @@ class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataReposito .validSchemaValidator(schemaUri.toString(), uriResolver) .catchAll { case e: InvalidURI => ZIO.fail(InvalidSchemaId(schemaId, e.userFacingMessage)) + case e: SchemaDereferencingError => ZIO.fail(InvalidSchemaId(schemaId, e.userFacingMessage)) case e: CredentialSchemaParsingError => ZIO.fail(InvalidSchemaId(schemaId, e.cause)) } now <- ZIO.clockWith(_.instant) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala index b252d35b1a..8e3f779ce5 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/uriResolvers/DidUrlResolver.scala @@ -5,8 +5,7 @@ import org.hyperledger.identus.pollux.vc.jwt import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.crypto.Sha256Hash import org.hyperledger.identus.shared.http.{GenericUriResolverError, UriResolver} -import org.hyperledger.identus.shared.models.PrismEnvelopeData -import org.hyperledger.identus.shared.models.StatusCode +import org.hyperledger.identus.shared.models.{PrismEnvelopeData, StatusCode} import zio.* import zio.json.* diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala index 8d2d3614bb..082d107fd4 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala @@ -96,7 +96,7 @@ object OID4VCIIssuerMetadataServiceSpecSuite { exit1 <- createCredConfig("not a uri").exit exit2 <- createCredConfig("http://localhost/schema").exit } yield assert(exit1)(failsWithA[InvalidSchemaId]) && - assert(exit2)(dies(anything)) + assert(exit2)(failsWithA[InvalidSchemaId]) }, test("list credential configurations for non-existing issuer should fail") { for { diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala index fd9392204d..4e4c2bf491 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/model/db/package.scala @@ -1,8 +1,8 @@ package org.hyperledger.identus.pollux.sql.model -import doobie._ -import doobie.postgres._ -import doobie.postgres.implicits._ +import doobie.* +import doobie.postgres.* +import doobie.postgres.implicits.* import io.getquill.doobie.DoobieContext import io.getquill.MappedEncoding import org.hyperledger.identus.pollux.core.model.ResourceResolutionMethod diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala index 25be250c52..7b628d66f8 100644 --- a/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/http/GenericUriResolver.scala @@ -1,8 +1,7 @@ package org.hyperledger.identus.shared.http import io.lemonlabs.uri.{Uri, Url, Urn} -import org.hyperledger.identus.shared.models.{Failure, StatusCode} -import org.hyperledger.identus.shared.models.PrismEnvelopeData +import org.hyperledger.identus.shared.models.{Failure, PrismEnvelopeData, StatusCode} import org.hyperledger.identus.shared.utils.Base64Utils import zio.* import zio.json.* diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/models/Failure.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/models/Failure.scala index c2ea5dc53f..b41611d07e 100644 --- a/shared/core/src/main/scala/org/hyperledger/identus/shared/models/Failure.scala +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/models/Failure.scala @@ -38,6 +38,7 @@ object StatusCode { val Unauthorized: StatusCode = StatusCode(401) val Forbidden: StatusCode = StatusCode(403) val NotFound: StatusCode = StatusCode(404) + val Conflict: StatusCode = StatusCode(409) val UnprocessableContent: StatusCode = StatusCode(422) val InternalServerError: StatusCode = StatusCode(500) From d81b4f06a149704f9cfb9d80e64f064a4f7aecf2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:24:48 +0000 Subject: [PATCH 09/13] build(deps): bump cryptography from 42.0.8 to 43.0.1 in /examples/st-oid4vci/demo in the pip group across 1 directory (#1346) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: patlo-iog --- examples/st-oid4vci/demo/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/st-oid4vci/demo/requirements.txt b/examples/st-oid4vci/demo/requirements.txt index 791e544886..1fe6463e94 100644 --- a/examples/st-oid4vci/demo/requirements.txt +++ b/examples/st-oid4vci/demo/requirements.txt @@ -1,7 +1,7 @@ certifi==2024.7.4 cffi==1.16.0 charset-normalizer==3.3.2 -cryptography==42.0.8 +cryptography==43.0.1 idna==3.7 pycparser==2.22 PyJWT==2.8.0 From ad946cf3f635b882d772a00b0202b957a1cb82cb Mon Sep 17 00:00:00 2001 From: Bassam Date: Mon, 30 Sep 2024 11:40:16 -0400 Subject: [PATCH 10/13] feat: VC support for Array of credential Status (#1383) Signed-off-by: Bassam Riman --- .../vc/jwt/VerifiableCredentialPayload.scala | 56 ++++++++++---- .../pollux/vc/jwt/JWTVerificationTest.scala | 77 ++++++++++++++++++- 2 files changed, 116 insertions(+), 17 deletions(-) diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala index 3d24747bd1..5638ba028a 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala @@ -79,7 +79,7 @@ sealed trait CredentialPayload { def issuer: String | CredentialIssuer - def maybeCredentialStatus: Option[CredentialStatus] + def maybeCredentialStatus: Option[CredentialStatus | List[CredentialStatus]] def maybeRefreshService: Option[RefreshService] @@ -145,7 +145,7 @@ case class JwtVc( maybeValidFrom: Option[Instant], maybeValidUntil: Option[Instant], maybeIssuer: Option[String | CredentialIssuer], - maybeCredentialStatus: Option[CredentialStatus], + maybeCredentialStatus: Option[CredentialStatus | List[CredentialStatus]], maybeRefreshService: Option[RefreshService], maybeEvidence: Option[Json], maybeTermsOfUse: Option[Json] @@ -182,7 +182,7 @@ case class W3cCredentialPayload( maybeExpirationDate: Option[Instant], override val maybeCredentialSchema: Option[CredentialSchema | List[CredentialSchema]], override val credentialSubject: Json, - override val maybeCredentialStatus: Option[CredentialStatus], + override val maybeCredentialStatus: Option[CredentialStatus | List[CredentialStatus]], override val maybeRefreshService: Option[RefreshService], override val maybeEvidence: Option[Json], override val maybeTermsOfUse: Option[Json], @@ -239,6 +239,11 @@ object CredentialPayload { ("statusListCredential", credentialStatus.statusListCredential.asJson) ) + implicit val credentialStatusOrListEncoder: Encoder[CredentialStatus | List[CredentialStatus]] = Encoder.instance { + case status: CredentialStatus => Encoder[CredentialStatus].apply(status) + case statusList: List[CredentialStatus] => Encoder[List[CredentialStatus]].apply(statusList) + } + implicit val stringOrCredentialIssuerEncoder: Encoder[String | CredentialIssuer] = Encoder.instance { case string: String => Encoder[String].apply(string) case credentialIssuer: CredentialIssuer => Encoder[CredentialIssuer].apply(credentialIssuer) @@ -383,6 +388,11 @@ object CredentialPayload { .map(schema => schema: CredentialSchema | List[CredentialSchema]) .or(Decoder[List[CredentialSchema]].map(schema => schema: CredentialSchema | List[CredentialSchema])) + implicit val credentialStatusOrListDecoder: Decoder[CredentialStatus | List[CredentialStatus]] = + Decoder[CredentialStatus] + .map(status => status: CredentialStatus | List[CredentialStatus]) + .or(Decoder[List[CredentialStatus]].map(status => status: CredentialStatus | List[CredentialStatus])) + implicit val w3cCredentialPayloadDecoder: Decoder[W3cCredentialPayload] = (c: HCursor) => for { @@ -404,7 +414,7 @@ object CredentialPayload { .downField("credentialSchema") .as[Option[CredentialSchema | List[CredentialSchema]]] credentialSubject <- c.downField("credentialSubject").as[Json] - maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus]] + maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus | List[CredentialStatus]]] maybeRefreshService <- c.downField("refreshService").as[Option[RefreshService]] maybeEvidence <- c.downField("evidence").as[Option[Json]] maybeTermsOfUse <- c.downField("termsOfUse").as[Option[Json]] @@ -443,7 +453,7 @@ object CredentialPayload { .downField("credentialSchema") .as[Option[CredentialSchema | List[CredentialSchema]]] credentialSubject <- c.downField("credentialSubject").as[Json] - maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus]] + maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus | List[CredentialStatus]]] maybeRefreshService <- c.downField("refreshService").as[Option[RefreshService]] maybeEvidence <- c.downField("evidence").as[Option[Json]] maybeTermsOfUse <- c.downField("termsOfUse").as[Option[Json]] @@ -837,7 +847,7 @@ object JwtCredential { } yield Validation.validateWith(signatureValidation, dateVerification, revocationVerification)((a, _, _) => a) } - private def verifyRevocationStatusJwt(jwt: JWT)(uriResolver: UriResolver): IO[String, Validation[String, Unit]] = { + def verifyRevocationStatusJwt(jwt: JWT)(uriResolver: UriResolver): IO[String, Validation[String, Unit]] = { val decodeJWT = ZIO .fromTry(JwtCirce.decodeRaw(jwt.value, options = JwtOptions(false, false, false))) @@ -847,12 +857,19 @@ object JwtCredential { decodedJWT <- decodeJWT jwtCredentialPayload <- ZIO.fromEither(decode[JwtCredentialPayload](decodedJWT)).mapError(_.getMessage) credentialStatus = jwtCredentialPayload.vc.maybeCredentialStatus - result = credentialStatus.fold(ZIO.succeed(Validation.unit))(status => - CredentialVerification.verifyCredentialStatus(status)(uriResolver) + .map { + { + case status: CredentialStatus => List(status) + case statusList: List[CredentialStatus] => statusList + } + } + .getOrElse(List.empty) + results <- ZIO.collectAll( + credentialStatus.map(status => CredentialVerification.verifyCredentialStatus(status)(uriResolver)) ) + result = Validation.validateAll(results).flatMap(_ => Validation.unit) } yield result - - res.flatten + res } } @@ -927,11 +944,20 @@ object W3CCredential { private def verifyRevocationStatusW3c( w3cPayload: W3cVerifiableCredentialPayload, )(uriResolver: UriResolver): IO[String, Validation[String, Unit]] = { - // If credential does not have credential status list, it does not support revocation - // and we assume revocation status is valid. - w3cPayload.payload.maybeCredentialStatus.fold(ZIO.succeed(Validation.unit))(status => - CredentialVerification.verifyCredentialStatus(status)(uriResolver) - ) + val credentialStatus = w3cPayload.payload.maybeCredentialStatus + .map { + { + case status: CredentialStatus => List(status) + case statusList: List[CredentialStatus] => statusList + } + } + .getOrElse(List.empty) + for { + results <- ZIO.collectAll( + credentialStatus.map(status => CredentialVerification.verifyCredentialStatus(status)(uriResolver)) + ) + result = Validation.validateAll(results).flatMap(_ => Validation.unit) + } yield result } def verify(w3cPayload: W3cVerifiableCredentialPayload, options: CredentialVerification.CredentialVerificationOptions)( diff --git a/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala b/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala index e09f9e6e96..8222a4fe64 100644 --- a/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala +++ b/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala @@ -7,6 +7,7 @@ import io.circe.* import io.circe.syntax.* import org.hyperledger.identus.castor.core.model.did.{DID, VerificationRelationship} import org.hyperledger.identus.pollux.vc.jwt.CredentialPayload.Implicits.* +import org.hyperledger.identus.pollux.vc.jwt.StatusPurpose.Revocation import org.hyperledger.identus.shared.http.* import zio.* import zio.prelude.Validation @@ -62,7 +63,11 @@ object JWTVerificationTest extends ZIOSpecDefault { |} |""".stripMargin - private def createJwtCredential(issuer: IssuerWithKey, issuerAsObject: Boolean = false): JWT = { + private def createJwtCredential( + issuer: IssuerWithKey, + issuerAsObject: Boolean = false, + credentialStatus: Option[CredentialStatus | List[CredentialStatus]] = None + ): JWT = { val validFrom = Instant.parse("2010-01-05T00:00:00Z") // ISSUANCE DATE val jwtCredentialNbf = Instant.parse("2010-01-01T00:00:00Z") // ISSUANCE DATE val validUntil = Instant.parse("2010-01-09T00:00:00Z") // EXPIRATION DATE @@ -75,7 +80,7 @@ object JWTVerificationTest extends ZIOSpecDefault { `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), maybeCredentialSchema = None, credentialSubject = Json.obj("id" -> Json.fromString("1")), - maybeCredentialStatus = None, + maybeCredentialStatus = credentialStatus, maybeRefreshService = None, maybeEvidence = None, maybeTermsOfUse = None, @@ -190,6 +195,51 @@ object JWTVerificationTest extends ZIOSpecDefault { ) ) }, + test("fail verification if proof is valid but credential is revoked at the give status list index given list") { + val revokedStatus: List[CredentialStatus] = List( + org.hyperledger.identus.pollux.vc.jwt.CredentialStatus( + id = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9#1", + statusPurpose = StatusPurpose.Revocation, + `type` = "StatusList2021Entry", + statusListCredential = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9", + statusListIndex = 1 + ), + org.hyperledger.identus.pollux.vc.jwt.CredentialStatus( + id = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9#2", + statusPurpose = StatusPurpose.Suspension, + `type` = "StatusList2021Entry", + statusListCredential = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9", + statusListIndex = 1 + ) + ) + + val urlResolver = new UriResolver { + override def resolve(uri: String): IO[GenericUriResolverError, String] = { + ZIO.succeed(statusListCredentialString) + } + } + + val genericUriResolver = GenericUriResolver( + Map( + "data" -> DataUrlResolver(), + "http" -> urlResolver, + "https" -> urlResolver + ) + ) + val issuer = createUser("did:prism:issuer") + val jwtCredential = createJwtCredential(issuer, credentialStatus = Some(revokedStatus)) + + for { + validation <- JwtCredential.verifyRevocationStatusJwt(jwtCredential)(genericUriResolver) + } yield assertTrue( + validation.fold( + chunk => + chunk.length == 2 && chunk.head.contentEquals("Credential is revoked") && chunk.tail.head + .contentEquals("Credential is revoked"), + _ => false + ) + ) + }, test("validate dates happy path") { val issuer = createUser("did:prism:issuer") val jwtCredential = createJwtCredential(issuer) @@ -223,6 +273,29 @@ object JWTVerificationTest extends ZIOSpecDefault { jwtWithObjectIssuerIssuer.equals(jwtIssuer) ) }, + test("validate credential status list") { + val issuer = createUser("did:prism:issuer") + val status = CredentialStatus(id = "id", `type` = "type", statusPurpose = Revocation, 1, "1") + val encodedJwtWithStatusList = createJwtCredential( + issuer, + false, + Some(List(status)) + ) + val econdedJwtWithStatusObject = createJwtCredential(issuer, true, Some(status)) + for { + decodeJwtWithStatusList <- JwtCredential + .decodeJwt(encodedJwtWithStatusList) + decodeJwtWithStatusObject <- JwtCredential + .decodeJwt(econdedJwtWithStatusObject) + statusFromList = decodeJwtWithStatusList.vc.maybeCredentialStatus.map { + case list: List[CredentialStatus] => list.head + case _: CredentialStatus => throw new IllegalStateException("List expected") + }.get + statusFromObjet = decodeJwtWithStatusObject.vc.maybeCredentialStatus.get + } yield assertTrue( + statusFromList.equals(statusFromObjet) + ) + }, test("validate dates should fail given after valid until") { val issuer = createUser("did:prism:issuer") val jwtCredential = createJwtCredential(issuer) From 7b4a9c1f8e1cb0b16d2da9b57e80d41cb2b5d478 Mon Sep 17 00:00:00 2001 From: Allain Magyar Date: Wed, 2 Oct 2024 16:00:34 -0300 Subject: [PATCH 11/13] test: add oid4vci negative scenario tests (#1380) Signed-off-by: Allain Magyar Signed-off-by: Hyperledger Bot Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Hyperledger Bot --- tests/integration-tests/README.md | 4 - .../src/test/kotlin/config/services/Agent.kt | 1 - .../test/kotlin/config/services/Keycloak.kt | 1 - .../kotlin/config/services/ServiceBase.kt | 5 +- .../src/test/kotlin/config/services/Vault.kt | 1 - .../config/services/VerifiableDataRegistry.kt | 1 - .../steps/oid4vci/IssueCredentialSteps.kt | 18 ++- .../oid4vci/ManageCredentialConfigSteps.kt | 125 ++++++++++++++--- .../kotlin/steps/oid4vci/ManageIssuerSteps.kt | 129 +++++++++++++++--- .../features/oid4vci/issue_jwt.feature | 6 +- .../oid4vci/manage_credential_config.feature | 31 ++++- .../features/oid4vci/manage_issuer.feature | 27 +++- 12 files changed, 280 insertions(+), 69 deletions(-) diff --git a/tests/integration-tests/README.md b/tests/integration-tests/README.md index 2fef57bf71..4b0e925bcd 100644 --- a/tests/integration-tests/README.md +++ b/tests/integration-tests/README.md @@ -116,10 +116,6 @@ The configuration files are divided into the following sections: * `agents`: contains the configuration for the agents (ICA) that will be started. By default, all agents will be destroyed after the test run is finished. * `roles`: contains the configuration for the roles (Issuer, Holder, Verifier, Admin). A role can be assigned to one or more agents that we set in `agents` section or already running locally or in the cloud. -> You could keep services and agents running for debugging purposes -> by specifying `keep_running = true` for the service or agent -> in the configuration file and setting `TESTCONTAINERS_RYUK_DISABLED` variable to `true`. - Please, check [test/resources/configs/basic.conf](./src/test/resources/configs/basic.conf) for a quick example of a basic configuration. You could explore the `configs` directory for more complex examples. diff --git a/tests/integration-tests/src/test/kotlin/config/services/Agent.kt b/tests/integration-tests/src/test/kotlin/config/services/Agent.kt index f4d7cc0962..a4e97a603d 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Agent.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Agent.kt @@ -16,7 +16,6 @@ data class Agent( @ConfigAlias("prism_node") val prismNode: VerifiableDataRegistry?, val keycloak: Keycloak?, val vault: Vault?, - @ConfigAlias("keep_running") override val keepRunning: Boolean = false, ) : ServiceBase() { override val logServices = listOf("identus-cloud-agent") diff --git a/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt b/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt index d73a8814b5..86027fe3ad 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt @@ -17,7 +17,6 @@ data class Keycloak( val realm: String = "atala-demo", @ConfigAlias("client_id") val clientId: String = "cloud-agent", @ConfigAlias("client_secret") val clientSecret: String = "cloud-agent-secret", - @ConfigAlias("keep_running") override val keepRunning: Boolean = false, @ConfigAlias("compose_file") val keycloakComposeFile: String = "src/test/resources/containers/keycloak.yml", @ConfigAlias("logger_name") val loggerName: String = "keycloak", @ConfigAlias("extra_envs") val extraEnvs: Map = emptyMap(), diff --git a/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt b/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt index 8c598b61d6..9c2483084a 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt @@ -15,7 +15,6 @@ abstract class ServiceBase : Startable { } abstract val container: ComposeContainer - abstract val keepRunning: Boolean open val logServices: List = emptyList() private val logWriters: MutableList = mutableListOf() @@ -41,8 +40,6 @@ abstract class ServiceBase : Startable { logWriters.forEach { it.close() } - if (!keepRunning) { - container.stop() - } + container.stop() } } diff --git a/tests/integration-tests/src/test/kotlin/config/services/Vault.kt b/tests/integration-tests/src/test/kotlin/config/services/Vault.kt index 85f1a02b27..a14a44620b 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Vault.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Vault.kt @@ -14,7 +14,6 @@ import java.io.File data class Vault( @ConfigAlias("http_port") val httpPort: Int, @ConfigAlias("vault_auth_type") val authType: VaultAuthType = VaultAuthType.APP_ROLE, - @ConfigAlias("keep_running") override val keepRunning: Boolean = false, ) : ServiceBase() { private val logger = Logger.get() override val logServices: List = listOf("vault") diff --git a/tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt b/tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt index 2997f567cc..f4fbcdba66 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt @@ -8,7 +8,6 @@ import java.io.File data class VerifiableDataRegistry( @ConfigAlias("http_port") val httpPort: Int, val version: String, - @ConfigAlias("keep_running") override val keepRunning: Boolean = false, ) : ServiceBase() { override val logServices: List = listOf("prism-node") private val vdrComposeFile = "src/test/resources/containers/vdr.yml" diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt index 3cd1873033..6a2e906361 100644 --- a/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/IssueCredentialSteps.kt @@ -5,6 +5,7 @@ import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.JWK import eu.europa.ec.eudi.openid4vci.* import interactions.Post +import interactions.body import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get @@ -40,16 +41,13 @@ class IssueCredentialSteps { issuer.recall("longFormDid") } issuer.attemptsTo( - Post.to("/oid4vci/issuers/${credentialIssuer.id}/credential-offers") - .with { - it.body( - CredentialOfferRequest( - credentialConfigurationId = configurationId, - issuingDID = did, - claims = claims, - ), - ) - }, + Post.to("/oid4vci/issuers/${credentialIssuer.id}/credential-offers").body( + CredentialOfferRequest( + credentialConfigurationId = configurationId, + issuingDID = did, + claims = claims, + ), + ), Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_CREATED), ) val offerUri = SerenityRest.lastResponse().get().credentialOffer diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt index 470babb703..8a53c311f9 100644 --- a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt @@ -1,20 +1,37 @@ package steps.oid4vci +import com.google.gson.JsonObject import common.CredentialSchema -import interactions.* -import io.cucumber.java.en.* +import interactions.Delete +import interactions.Get +import interactions.Post +import interactions.body +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus -import org.hyperledger.identus.client.models.* +import org.apache.http.HttpStatus.SC_OK +import org.hyperledger.identus.client.models.CreateCredentialConfigurationRequest +import org.hyperledger.identus.client.models.CredentialFormat +import org.hyperledger.identus.client.models.CredentialIssuer +import org.hyperledger.identus.client.models.IssuerMetadata +import java.util.UUID class ManageCredentialConfigSteps { @Given("{actor} has {string} credential configuration created from {}") fun issuerHasExistingCredentialConfig(issuer: Actor, configurationId: String, schema: CredentialSchema) { ManageIssuerSteps().issuerHasExistingCredentialIssuer(issuer) - issuerCreateCredentialConfiguration(issuer, schema, configurationId) + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Get("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations/$configurationId"), + ) + if (SerenityRest.lastResponse().statusCode != SC_OK) { + issuerCreateCredentialConfiguration(issuer, schema, configurationId) + } } @When("{actor} uses {} to create a credential configuration {string}") @@ -22,17 +39,15 @@ class ManageCredentialConfigSteps { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") val schemaGuid = issuer.recall(schema.name) val baseUrl = issuer.recall("baseUrl") + issuer.attemptsTo( - Post.to("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations") - .with { - it.body( - CreateCredentialConfigurationRequest( - configurationId = configurationId, - format = CredentialFormat.JWT_VC_JSON, - schemaId = "$baseUrl/schema-registry/schemas/$schemaGuid/schema", - ), - ) - }, + Post.to("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations").body( + CreateCredentialConfigurationRequest( + configurationId = configurationId, + format = CredentialFormat.JWT_VC_JSON, + schemaId = "$baseUrl/schema-registry/schemas/$schemaGuid/schema", + ), + ), Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_CREATED), ) } @@ -42,7 +57,75 @@ class ManageCredentialConfigSteps { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") issuer.attemptsTo( Delete("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations/$configurationId"), - Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @When("{actor} deletes a non existent {} credential configuration") + fun issuerDeletesANonExistentCredentialConfiguration(issuer: Actor, configurationId: String) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Delete("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations/$configurationId"), + ) + } + + @When("{actor} creates a new credential configuration request") + fun issuerCreatesANewConfigurationRequest(issuer: Actor) { + val credentialConfiguration = JsonObject() + issuer.remember("credentialConfiguration", credentialConfiguration) + } + + @When("{actor} uses {} issuer id for credential configuration") + fun issuerUsesIssuerId(issuer: Actor, issuerId: String) { + if (issuerId == "existing") { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.remember("credentialConfigurationId", credentialIssuer.id) + } else if (issuerId == "wrong") { + issuer.remember("credentialConfigurationId", UUID.randomUUID().toString()) + } + } + + @When("{actor} adds '{}' configuration id for credential configuration request") + fun issuerAddsConfigurationIdToCredentialConfigurationRequest(issuer: Actor, configurationId: String) { + val credentialIssuer = issuer.recall("credentialConfiguration") + val configurationIdProperty = if (configurationId == "null") { + null + } else { + configurationId + } + credentialIssuer.addProperty("configurationId", configurationIdProperty) + } + + @When("{actor} adds '{}' format for credential configuration request") + fun issuerAddsFormatToCredentialConfigurationRequest(issuer: Actor, format: String) { + val credentialIssuer = issuer.recall("credentialConfiguration") + val formatProperty = if (format == "null") { + null + } else { + format + } + credentialIssuer.addProperty("format", formatProperty) + } + + @When("{actor} adds '{}' schemaId for credential configuration request") + fun issuerAddsSchemaIdToCredentialConfigurationRequest(issuer: Actor, schema: String) { + val credentialIssuer = issuer.recall("credentialConfiguration") + val schemaIdProperty = if (schema == "null") { + null + } else { + val baseUrl = issuer.recall("baseUrl") + val schemaGuid = issuer.recall(schema) + "$baseUrl/schema-registry/schemas/$schemaGuid/schema" + } + credentialIssuer.addProperty("schemaId", schemaIdProperty) + } + + @When("{actor} sends the create a credential configuration request") + fun issuerSendsTheCredentialConfigurationRequest(issuer: Actor) { + val credentialConfiguration = issuer.recall("credentialConfiguration") + val credentialIssuerId = issuer.recall("credentialConfigurationId").toString() + issuer.attemptsTo( + Post.to("/oid4vci/issuers/$credentialIssuerId/credential-configurations").body(credentialConfiguration), ) } @@ -51,7 +134,7 @@ class ManageCredentialConfigSteps { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") issuer.attemptsTo( Get("/oid4vci/issuers/${credentialIssuer.id}/.well-known/openid-credential-issuer"), - Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) val metadata = SerenityRest.lastResponse().get() val credConfig = metadata.credentialConfigurationsSupported[configurationId]!! @@ -65,11 +148,19 @@ class ManageCredentialConfigSteps { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") issuer.attemptsTo( Get("/oid4vci/issuers/${credentialIssuer.id}/.well-known/openid-credential-issuer"), - Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) val metadata = SerenityRest.lastResponse().get() issuer.attemptsTo( Ensure.that(metadata.credentialConfigurationsSupported.keys).doesNotContain(configurationId), ) } + + @Then("{actor} should see that create credential configuration has failed with '{}' status code and '{}' detail") + fun issuerShouldSeeCredentialConfigurationRequestHasFailed(issuer: Actor, statusCode: Int, errorDetail: String) { + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(statusCode), + Ensure.that(SerenityRest.lastResponse().body.asString()).contains(errorDetail), + ) + } } diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt index 7db69f0b41..e78408078f 100644 --- a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt @@ -1,7 +1,15 @@ package steps.oid4vci -import interactions.* -import io.cucumber.java.en.* +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import interactions.Delete +import interactions.Get +import interactions.Patch +import interactions.Post +import interactions.body +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure import net.serenitybdd.rest.SerenityRest @@ -9,16 +17,26 @@ import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus import org.apache.http.HttpStatus.SC_CREATED import org.apache.http.HttpStatus.SC_OK -import org.hyperledger.identus.client.models.* +import org.hyperledger.identus.client.models.AuthorizationServer +import org.hyperledger.identus.client.models.CreateCredentialIssuerRequest +import org.hyperledger.identus.client.models.CredentialIssuer +import org.hyperledger.identus.client.models.CredentialIssuerPage +import org.hyperledger.identus.client.models.IssuerMetadata +import org.hyperledger.identus.client.models.PatchAuthorizationServer +import org.hyperledger.identus.client.models.PatchCredentialIssuerRequest class ManageIssuerSteps { - private val UPDATE_AUTH_SERVER_URL = "http://example.com" - private val UPDATE_AUTH_SERVER_CLIENT_ID = "foo" - private val UPDATE_AUTH_SERVER_CLIENT_SECRET = "bar" + companion object { + private const val UPDATE_AUTH_SERVER_URL = "http://example.com" + private const val UPDATE_AUTH_SERVER_CLIENT_ID = "foo" + private const val UPDATE_AUTH_SERVER_CLIENT_SECRET = "bar" + } @Given("{actor} has an existing oid4vci issuer") fun issuerHasExistingCredentialIssuer(issuer: Actor) { - issuerCreateCredentialIssuer(issuer) + if (!issuer.recallAll().containsKey("oid4vciCredentialIssuer")) { + issuerCreateCredentialIssuer(issuer) + } } @When("{actor} creates an oid4vci issuer") @@ -69,19 +87,29 @@ class ManageIssuerSteps { fun issuerUpdateCredentialIssuer(issuer: Actor) { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") issuer.attemptsTo( - Patch.to("/oid4vci/issuers/${credentialIssuer.id}") - .with { - it.body( - PatchCredentialIssuerRequest( - authorizationServer = PatchAuthorizationServer( - url = UPDATE_AUTH_SERVER_URL, - clientId = UPDATE_AUTH_SERVER_CLIENT_ID, - clientSecret = UPDATE_AUTH_SERVER_CLIENT_SECRET, - ), - ), - ) - }, - Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + Patch.to("/oid4vci/issuers/${credentialIssuer.id}").body( + PatchCredentialIssuerRequest( + authorizationServer = PatchAuthorizationServer( + url = UPDATE_AUTH_SERVER_URL, + clientId = UPDATE_AUTH_SERVER_CLIENT_ID, + clientSecret = UPDATE_AUTH_SERVER_CLIENT_SECRET, + ), + ), + ), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @When("{actor} tries to update the oid4vci issuer '{}' property using '{}' value") + fun issuerTriesToUpdateTheOID4VCIIssuer(issuer: Actor, property: String, value: String) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + val body = JsonObject() + val propertyValue = if (value == "null") { null } else { value } + body.addProperty(property, propertyValue) + + val gson = GsonBuilder().serializeNulls().create() + issuer.attemptsTo( + Patch.to("/oid4vci/issuers/${credentialIssuer.id}").body(gson.toJson(body)), ) } @@ -94,6 +122,60 @@ class ManageIssuerSteps { ) } + @When("{actor} tries to create oid4vci issuer with '{}', '{}', '{}' and '{}'") + fun issuerTriesToCreateOIDCIssuer( + issuer: Actor, + id: String, + url: String, + clientId: String, + clientSecret: String, + ) { + val idProperty = if (id == "null") { + null + } else { + id + } + val urlProperty = if (url == "null") { + null + } else { + url + } + val clientIdProperty = if (clientId == "null") { + null + } else { + clientId + } + val clientSecretProperty = if (clientSecret == "null") { + null + } else { + clientSecret + } + + val body = JsonObject() + val authorizationServer = JsonObject() + + body.addProperty("id", idProperty) + body.add("authorizationServer", authorizationServer) + + authorizationServer.addProperty("url", urlProperty) + authorizationServer.addProperty("clientId", clientIdProperty) + authorizationServer.addProperty("clientSecret", clientSecretProperty) + + val gson = GsonBuilder().serializeNulls().create() + issuer.attemptsTo( + Post.to("/oid4vci/issuers").body(gson.toJson(body)), + ) + } + + @Then("{actor} should see the oid4vci '{}' http status response with '{}' detail") + fun issuerShouldSeeTheOIDC4VCIError(issuer: Actor, httpStatus: Int, errorDetail: String) { + SerenityRest.lastResponse().body.prettyPrint() + issuer.attemptsTo( + Ensure.that(SerenityRest.lastResponse().statusCode).isEqualTo(httpStatus), + Ensure.that(SerenityRest.lastResponse().body.asString()).contains(errorDetail), + ) + } + @Then("{actor} sees the oid4vci issuer updated with new values") fun issuerSeesUpdatedCredentialIssuer(issuer: Actor) { val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") @@ -143,4 +225,11 @@ class ManageIssuerSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_NOT_FOUND), ) } + + @Then("{actor} should see the update oid4vci issuer returned '{}' http status") + fun issuerShouldSeeTheUpdateOID4VCIIssuerReturnedHttpStatus(issuer: Actor, statusCode: Int) { + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(statusCode), + ) + } } diff --git a/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature b/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature index 2f30658ad8..53d010c2c5 100644 --- a/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature +++ b/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature @@ -1,20 +1,20 @@ @oid4vci Feature: Issue JWT Credentials using OID4VCI authorization code flow -Background: + Background: Given Issuer has a published DID for JWT And Issuer has published STUDENT_SCHEMA schema And Issuer has an existing oid4vci issuer And Issuer has "StudentProfile" credential configuration created from STUDENT_SCHEMA -Scenario: Issuing credential with published PRISM DID + Scenario: Issuing credential with published PRISM DID When Issuer creates an offer using "StudentProfile" configuration with "short" form DID And Holder receives oid4vci offer from Issuer And Holder resolves oid4vci issuer metadata and login via front-end channel And Holder presents the access token with JWT proof on CredentialEndpoint Then Holder sees credential issued successfully from CredentialEndpoint -Scenario: Issuing credential with unpublished PRISM DID + Scenario: Issuing credential with unpublished PRISM DID When Issuer creates an offer using "StudentProfile" configuration with "long" form DID And Holder receives oid4vci offer from Issuer And Holder resolves oid4vci issuer metadata and login via front-end channel diff --git a/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature b/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature index 3253069abe..cacdeb6cdb 100644 --- a/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature +++ b/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature @@ -1,16 +1,39 @@ @oid4vci Feature: Manage OID4VCI credential configuration -Background: + Background: Given Issuer has a published DID for JWT And Issuer has published STUDENT_SCHEMA schema And Issuer has an existing oid4vci issuer -Scenario: Successfully create credential configuration - When Issuer uses STUDENT_SCHEMA to create a credential configuration "StudentProfile" + Scenario: Successfully create credential configuration + Given Issuer has "StudentProfile" credential configuration created from STUDENT_SCHEMA Then Issuer sees the "StudentProfile" configuration on IssuerMetadata endpoint -Scenario: Successfully delete credential configuration + Scenario: Successfully delete credential configuration Given Issuer has "StudentProfile" credential configuration created from STUDENT_SCHEMA When Issuer deletes "StudentProfile" credential configuration Then Issuer cannot see the "StudentProfile" configuration on IssuerMetadata endpoint + + Scenario Outline: Create configuration with expect code + When Issuer creates a new credential configuration request + And Issuer uses issuer id for credential configuration + And Issuer adds '' configuration id for credential configuration request + And Issuer adds '' format for credential configuration request + And Issuer adds '' schemaId for credential configuration request + And Issuer sends the create a credential configuration request + Then Issuer should see that create credential configuration has failed with '' status code and '' detail + Examples: + | issuerId | configurationId | format | schemaId | httpStatus | errorDetail | description | + | wrong | StudentProfile | jwt_vc_json | STUDENT_SCHEMA | 404 | There is no credential issue | wrong issuer id | + | existing | null | jwt_vc_json | STUDENT_SCHEMA | 400 | configurationId | null configuration id | + | existing | StudentProfile | null | STUDENT_SCHEMA | 400 | format | null format | + | existing | StudentProfile | wrong-format | STUDENT_SCHEMA | 400 | format | wrong format | + | existing | StudentProfile | jwt_vc_json | null | 400 | schemaId | null schema | + | existing | StudentProfile | jwt_vc_json | malformed-schema | 400 | | malformed schema | + | existing | StudentProfile | jwt_vc_json | STUDENT_SCHEMA | 201 | | right values | + | existing | StudentProfile | jwt_vc_json | STUDENT_SCHEMA | 409 | Duplicated credential | duplicated configuration id | + + Scenario: Delete non existent credential configuration + When Issuer deletes a non existent "NonExistentProfile" credential configuration + Then Issuer should see that create credential configuration has failed with '404' status code and 'There is no credential configuration' detail diff --git a/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature b/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature index d2b6bd4aa6..6259934824 100644 --- a/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature +++ b/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature @@ -1,19 +1,40 @@ @oid4vci Feature: Manage OID4VCI credential issuer -Scenario: Successfully create credential issuer + Scenario: Successfully create credential issuer When Issuer creates an oid4vci issuer Then Issuer sees the oid4vci issuer exists on the agent And Issuer sees the oid4vci issuer on IssuerMetadata endpoint -Scenario: Successfully update credential issuer + Scenario: Successfully update credential issuer Given Issuer has an existing oid4vci issuer When Issuer updates the oid4vci issuer Then Issuer sees the oid4vci issuer updated with new values And Issuer sees the oid4vci IssuerMetadata endpoint updated with new values -Scenario: Successfully delete credential issuer + Scenario: Successfully delete credential issuer Given Issuer has an existing oid4vci issuer When Issuer deletes the oid4vci issuer Then Issuer cannot see the oid4vci issuer on the agent And Issuer cannot see the oid4vci IssuerMetadata endpoint + + Scenario Outline: Create issuer with expect response + When Issuer tries to create oid4vci issuer with '', '', '' and '' + Then Issuer should see the oid4vci '' http status response with '' detail + Examples: + | id | url | clientId | clientSecret | httpStatus | errorDetail | description | + | null | null | null | null | 400 | authorizationServer.url | null values | + | null | malformed | id | secret | 400 | Relative URL 'malformed' is not | malformed url | + | null | http://example.com | id | null | 400 | authorizationServer.clientSecret | null client secret | + | null | http://example.com | null | secret | 400 | authorizationServer.clientId | null client id | + | null | null | id | secret | 400 | authorizationServer.url | null url | + | 4048ef76-749d-4296-8c6c-07c8a20733a0 | http://example.com | id | secret | 201 | | right values | + | 4048ef76-749d-4296-8c6c-07c8a20733a0 | http://example.com | id | secret | 500 | | duplicated id | + + Scenario Outline: Update issuer with expect response + Given Issuer has an existing oid4vci issuer + When Issuer tries to update the oid4vci issuer '' property using '' value + Then Issuer should see the oid4vci '' http status response with '' detail + Examples: + | property | value | httpStatus | errorDetail | description | + | url | malformed | 404 | | Invalid URL | From c2da492131e5c545b0fefb101246c48684bc9433 Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev Date: Wed, 9 Oct 2024 14:05:13 +0700 Subject: [PATCH 12/13] =?UTF-8?q?fix:=20adjust=20Kotlin=20and=20TypeScript?= =?UTF-8?q?=20HTTP=20client=20to=20use=20the=20`schemaId`=20f=E2=80=A6=20(?= =?UTF-8?q?#1388)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Yurii Shynbuiev --- .github/workflows/lint.yml | 2 +- .mega-linter.yml | 2 + .../client/generator/openapitools.json | 2 +- cloud-agent/client/generator/package.json | 2 +- cloud-agent/client/generator/yarn.lock | 254 +++++++------ .../client/kotlin/.openapi-generator-ignore | 7 + .../kotlin/gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 3 +- cloud-agent/client/kotlin/gradlew | 176 ++++----- cloud-agent/client/kotlin/gradlew.bat | 341 +++++++++++++----- cloud-agent/client/kotlin/settings.gradle | 3 +- .../adapters/StringOrStringArrayAdapter.kt | 33 ++ .../CreateIssueCredentialRecordRequest.kt | 86 +++++ .../identus/client/models/Service.kt | 31 +- .../typescript/.openapi-generator-ignore | 1 + .../CreateIssueCredentialRecordRequest.ts | 135 +++++++ tests/integration-tests/build.gradle.kts | 2 +- .../steps/credentials/JwtCredentialSteps.kt | 2 +- .../agent-performance-tests-k6/.env | 4 +- .../agent-performance-tests-k6/package.json | 2 +- .../agent-performance-tests-k6/yarn.lock | 8 +- 21 files changed, 768 insertions(+), 328 deletions(-) create mode 100644 cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/adapters/StringOrStringArrayAdapter.kt create mode 100644 cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequest.kt create mode 100644 cloud-agent/client/typescript/models/CreateIssueCredentialRecordRequest.ts diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b14905dbbf..4ec94cfb7a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,7 +36,7 @@ jobs: - name: MegaLinter id: ml - uses: oxsecurity/megalinter@v7.1.0 + uses: oxsecurity/megalinter@v8 - name: Archive production artifacts if: success() || failure() diff --git a/.mega-linter.yml b/.mega-linter.yml index 2e4d3c64b2..d065f68b12 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -12,6 +12,7 @@ DISABLE_LINTERS: - REPOSITORY_CHECKOV - REPOSITORY_SECRETLINT - REPOSITORY_KICS + - REPOSITORY_GRYPE - SCALA_SCALAFIX - SQL_TSQLLINT - C_CPPLINT # For pollux/lib/anoncreds/src/main/c @@ -30,6 +31,7 @@ DISABLE_LINTERS: DISABLE_ERRORS_LINTERS: - KOTLIN_KTLINT + - KOTLIN_DETEKT - PROTOBUF_PROTOLINT - MARKDOWN_MARKDOWN_LINK_CHECK - ACTION_ACTIONLINT diff --git a/cloud-agent/client/generator/openapitools.json b/cloud-agent/client/generator/openapitools.json index 5571688218..f227cf2df3 100644 --- a/cloud-agent/client/generator/openapitools.json +++ b/cloud-agent/client/generator/openapitools.json @@ -2,6 +2,6 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "7.4.0" + "version": "7.7.0" } } diff --git a/cloud-agent/client/generator/package.json b/cloud-agent/client/generator/package.json index 79c2bf504e..f9fb3d43dc 100644 --- a/cloud-agent/client/generator/package.json +++ b/cloud-agent/client/generator/package.json @@ -13,7 +13,7 @@ "publish:clients": "./publish-clients.sh" }, "dependencies": { - "@openapitools/openapi-generator-cli": "2.7.0", + "@openapitools/openapi-generator-cli": "2.13.13", "npm-run-all": "^4.1.5" } } diff --git a/cloud-agent/client/generator/yarn.lock b/cloud-agent/client/generator/yarn.lock index faf654ad56..b9236b8e57 100644 --- a/cloud-agent/client/generator/yarn.lock +++ b/cloud-agent/client/generator/yarn.lock @@ -14,33 +14,31 @@ resolved "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz" integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== -"@nestjs/axios@0.1.0": - version "0.1.0" - resolved "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.0.tgz" - integrity sha512-b2TT2X6BFbnNoeteiaxCIiHaFcSbVW+S5yygYqiIq5i6H77yIU3IVuLdpQkHq8/EqOWFwMopLN8jdkUT71Am9w== - dependencies: - axios "0.27.2" +"@nestjs/axios@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-3.0.3.tgz#a663cb13cff07ea6b9a7107263de2ae472d41118" + integrity sha512-h6TCn3yJwD6OKqqqfmtRS5Zo4E46Ip2n+gK1sqwzNBC+qxQ9xpCu+ODVRFur6V3alHSCSBxb3nNtt73VEdluyA== -"@nestjs/common@9.3.11": - version "9.3.11" - resolved "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz" - integrity sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A== +"@nestjs/common@10.4.3": + version "10.4.3" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.3.tgz#b9059313d928aea335a4a185a621e32c1858c845" + integrity sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg== dependencies: - uid "2.0.1" + uid "2.0.2" iterare "1.2.1" - tslib "2.5.0" + tslib "2.7.0" -"@nestjs/core@9.3.11": - version "9.3.11" - resolved "https://registry.npmjs.org/@nestjs/core/-/core-9.3.11.tgz" - integrity sha512-CI27a2JFd5rvvbgkalWqsiwQNhcP4EAG5BUK8usjp29wVp1kx30ghfBT8FLqIgmkRVo65A0IcEnWsxeXMntkxQ== +"@nestjs/core@10.4.3": + version "10.4.3" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.3.tgz#b2a3dcfc6a948a74618feeee8affc3186afe52da" + integrity sha512-6OQz+5C8mT8yRtfvE5pPCq+p6w5jDot+oQku1KzQ24ABn+lay1KGuJwcKZhdVNuselx+8xhdMxknZTA8wrGLIg== dependencies: - uid "2.0.1" + uid "2.0.2" "@nuxtjs/opencollective" "0.3.2" fast-safe-stringify "2.1.1" iterare "1.2.1" - path-to-regexp "3.2.0" - tslib "2.5.0" + path-to-regexp "3.3.0" + tslib "2.7.0" "@nuxtjs/opencollective@0.3.2": version "0.3.2" @@ -51,27 +49,36 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@openapitools/openapi-generator-cli@2.7.0": - version "2.7.0" - resolved "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.7.0.tgz" - integrity sha512-ieEpHTA/KsDz7ANw03lLPYyjdedDEXYEyYoGBRWdduqXWSX65CJtttjqa8ZaB1mNmIjMtchUHwAYQmTLVQ8HYg== +"@openapitools/openapi-generator-cli@2.13.13": + version "2.13.13" + resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.13.13.tgz#380fd9556500b558f066a9ee0c46678f7803422b" + integrity sha512-uioqbxB6TfiLoOEE3T8kqTn/ffaRzOwS3ATMQnoMvh2lwADKMT6bDLfE3YO3XTEj+HflXcsLXQGK6PLiqa8Mmw== dependencies: - "@nestjs/axios" "0.1.0" - "@nestjs/common" "9.3.11" - "@nestjs/core" "9.3.11" + "@nestjs/axios" "3.0.3" + "@nestjs/common" "10.4.3" + "@nestjs/core" "10.4.3" "@nuxtjs/opencollective" "0.3.2" + axios "1.7.7" chalk "4.1.2" commander "8.3.0" compare-versions "4.1.4" concurrently "6.5.1" console.table "0.10.0" fs-extra "10.1.0" - glob "7.1.6" - inquirer "8.2.5" + glob "9.3.5" + https-proxy-agent "7.0.5" + inquirer "8.2.6" lodash "4.17.21" reflect-metadata "0.1.13" - rxjs "7.8.0" - tslib "2.0.3" + rxjs "7.8.1" + tslib "2.7.0" + +agent-base@^7.0.2: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" ansi-escapes@^4.2.1: version "4.3.2" @@ -129,13 +136,14 @@ available-typed-arrays@^1.0.5: resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -axios@0.27.2: - version "0.27.2" - resolved "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== +axios@1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== dependencies: - follow-redirects "^1.14.9" + follow-redirects "^1.15.6" form-data "^4.0.0" + proxy-from-env "^1.1.0" balanced-match@^1.0.0: version "1.0.2" @@ -164,6 +172,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + buffer@^5.5.0: version "5.7.1" resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" @@ -323,6 +338,13 @@ date-fns@^2.16.1: dependencies: "@babel/runtime" "^7.21.0" +debug@4, debug@^4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + defaults@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz" @@ -456,10 +478,10 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" -follow-redirects@^1.14.9: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== for-each@^0.3.3: version "0.3.3" @@ -534,17 +556,15 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" -glob@7.1.6: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@9.3.5: + version "9.3.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" + integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== dependencies: fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" + minimatch "^8.0.2" + minipass "^4.2.4" + path-scurry "^1.6.1" globalthis@^1.0.3: version "1.0.3" @@ -616,6 +636,14 @@ hosted-git-info@^2.1.4: resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +https-proxy-agent@7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" @@ -628,23 +656,15 @@ ieee754@^1.1.13: resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3, inherits@^2.0.4: +inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inquirer@8.2.5: - version "8.2.5" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz" - integrity sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ== +inquirer@8.2.6: + version "8.2.6" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" + integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg== dependencies: ansi-escapes "^4.2.1" chalk "^4.1.1" @@ -660,7 +680,7 @@ inquirer@8.2.5: string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" - wrap-ansi "^7.0.0" + wrap-ansi "^6.0.1" internal-slot@^1.0.5: version "1.0.5" @@ -841,6 +861,11 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz" @@ -870,6 +895,28 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^8.0.2: + version "8.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" + integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== + dependencies: + brace-expansion "^2.0.1" + +minipass@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + mute-stream@0.0.8: version "0.0.8" resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" @@ -932,13 +979,6 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -once@^1.3.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - onetime@^5.1.0: version "5.1.2" resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" @@ -974,11 +1014,6 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - path-key@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz" @@ -989,10 +1024,18 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz" - integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== +path-scurry@^1.6.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== path-type@^3.0.0: version "3.0.0" @@ -1011,6 +1054,11 @@ pify@^3.0.0: resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz" @@ -1075,10 +1123,10 @@ run-async@^2.4.0: resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== -rxjs@7.8.0: - version "7.8.0" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz" - integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== +rxjs@7.8.1, rxjs@^7.5.5: + version "7.8.1" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" @@ -1089,13 +1137,6 @@ rxjs@^6.6.3: dependencies: tslib "^1.9.0" -rxjs@^7.5.5: - version "7.8.1" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - safe-array-concat@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz" @@ -1304,15 +1345,10 @@ tree-kill@^1.2.2: resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -tslib@2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz" - integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== - -tslib@2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== +tslib@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== tslib@^1.9.0: version "1.14.1" @@ -1368,10 +1404,10 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -uid@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/uid/-/uid-2.0.1.tgz" - integrity sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A== +uid@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/uid/-/uid-2.0.2.tgz#4b5782abf0f2feeefc00fa88006b2b3b7af3e3b9" + integrity sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g== dependencies: "@lukeed/csprng" "^1.0.0" @@ -1452,6 +1488,15 @@ which@^1.2.9: dependencies: isexe "^2.0.0" +wrap-ansi@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" @@ -1461,11 +1506,6 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" diff --git a/cloud-agent/client/kotlin/.openapi-generator-ignore b/cloud-agent/client/kotlin/.openapi-generator-ignore index d9ad2d6500..658834d27c 100644 --- a/cloud-agent/client/kotlin/.openapi-generator-ignore +++ b/cloud-agent/client/kotlin/.openapi-generator-ignore @@ -17,6 +17,9 @@ src/main/kotlin/org/hyperledger/identus/client/models/DateTimeParameter.kt src/main/kotlin/org/hyperledger/identus/client/models/DidParameter.kt src/main/kotlin/org/hyperledger/identus/client/models/VcVerificationParameter.kt +src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequest.kt +src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequestSchemaId.kt + src/test/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceActionTest.kt src/test/kotlin/org/hyperledger/identus/client/models/UpdateManagedDIDServiceActionTypeTest.kt @@ -25,3 +28,7 @@ src/test/kotlin/org/hyperledger/identus/client/models/ServiceTypeTest.kt src/test/kotlin/org/hyperledger/identus/client/models/StatusPurposeTest.kt src/test/kotlin/org/hyperledger/identus/client/models/CredentialSubjectTest.kt + +src/test/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequestTest.kt +src/test/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequestSchemaIdTest.kt + diff --git a/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.jar b/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 62076 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 diff --git a/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.properties b/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.properties index 8707e8b506..e7646dead0 100644 --- a/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.properties +++ b/cloud-agent/client/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/cloud-agent/client/kotlin/gradlew b/cloud-agent/client/kotlin/gradlew index aeb74cbb43..9d0ce634cb 100755 --- a/cloud-agent/client/kotlin/gradlew +++ b/cloud-agent/client/kotlin/gradlew @@ -69,34 +69,35 @@ app_path=$0 # Need this for daisy-chained symlinks. while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] +APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path +[ -h "$app_path" ] do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac +ls=$( ls -ld "$app_path" ) +link=${ls#*' -> '} +case $link in #( +/*) app_path=$link ;; #( +*) app_path=$APP_HOME$link ;; +esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { - echo "$*" +echo "$*" } >&2 die () { - echo - echo "$*" - echo - exit 1 +echo +echo "$*" +echo +exit 1 } >&2 # OS specific support (must be 'true' or 'false'). @@ -105,10 +106,10 @@ msys=false darwin=false nonstop=false case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; +CYGWIN* ) cygwin=true ;; #( +Darwin* ) darwin=true ;; #( +MSYS* | MINGW* ) msys=true ;; #( +NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -116,43 +117,46 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME +if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# IBM's JDK on AIX uses strange locations for the executables +JAVACMD=$JAVA_HOME/jre/sh/java +else +JAVACMD=$JAVA_HOME/bin/java +fi +if [ ! -x "$JAVACMD" ] ; then +die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi +fi else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +JAVACMD=java +if ! command -v java >/dev/null 2>&1 +then +die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi +fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac +case $MAX_FD in #( +max*) +# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +MAX_FD=$( ulimit -H -n ) || +warn "Could not query maximum file descriptor limit" +esac +case $MAX_FD in #( +'' | soft) :;; #( +*) +# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +ulimit -n "$MAX_FD" || +warn "Could not set maximum file descriptor limit to $MAX_FD" +esac fi # Collect all arguments for the java command, stacking in reverse order: @@ -165,55 +169,55 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done +APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) +CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + +JAVACMD=$( cygpath --unix "$JAVACMD" ) + +# Now convert the arguments - kludge to limit ourselves to /bin/sh +for arg do +if +case $arg in #( +-*) false ;; # don't mess with options #( +/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath +[ -e "$t" ] ;; #( +*) false ;; +esac +then +arg=$( cygpath --path --ignore --mixed "$arg" ) +fi +# Roll the args list around exactly as many times as the number of +# args, so each arg winds up back in the position where it started, but +# possibly modified. +# +# NB: a `for` loop captures its iteration list before it begins, so +# changing the positional parameters here affects neither the number of +# iterations, nor the values presented in `arg`. +shift # remove old arg +set -- "$@" "$arg" # push replacement arg +done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" +"-Dorg.gradle.appname=$APP_BASE_NAME" \ +-classpath "$CLASSPATH" \ +org.gradle.wrapper.GradleWrapperMain \ +"$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then - die "xargs is not available" +die "xargs is not available" fi # Use "xargs" to parse quoted args. @@ -236,10 +240,10 @@ fi # eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' +printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | +xargs -n1 | +sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | +tr '\n' ' ' +)" '"$@"' exec "$JAVACMD" "$@" diff --git a/cloud-agent/client/kotlin/gradlew.bat b/cloud-agent/client/kotlin/gradlew.bat index 93e3f59f13..9d0ce634cb 100644 --- a/cloud-agent/client/kotlin/gradlew.bat +++ b/cloud-agent/client/kotlin/gradlew.bat @@ -1,92 +1,249 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while +APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path +[ -h "$app_path" ] +do +ls=$( ls -ld "$app_path" ) +link=${ls#*' -> '} +case $link in #( +/*) app_path=$link ;; #( +*) app_path=$APP_HOME$link ;; +esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { +echo "$*" +} >&2 + +die () { +echo +echo "$*" +echo +exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( +CYGWIN* ) cygwin=true ;; #( +Darwin* ) darwin=true ;; #( +MSYS* | MINGW* ) msys=true ;; #( +NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then +if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# IBM's JDK on AIX uses strange locations for the executables +JAVACMD=$JAVA_HOME/jre/sh/java +else +JAVACMD=$JAVA_HOME/bin/java +fi +if [ ! -x "$JAVACMD" ] ; then +die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +else +JAVACMD=java +if ! command -v java >/dev/null 2>&1 +then +die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then +case $MAX_FD in #( +max*) +# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +MAX_FD=$( ulimit -H -n ) || +warn "Could not query maximum file descriptor limit" +esac +case $MAX_FD in #( +'' | soft) :;; #( +*) +# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +ulimit -n "$MAX_FD" || +warn "Could not set maximum file descriptor limit to $MAX_FD" +esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then +APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) +CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + +JAVACMD=$( cygpath --unix "$JAVACMD" ) + +# Now convert the arguments - kludge to limit ourselves to /bin/sh +for arg do +if +case $arg in #( +-*) false ;; # don't mess with options #( +/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath +[ -e "$t" ] ;; #( +*) false ;; +esac +then +arg=$( cygpath --path --ignore --mixed "$arg" ) +fi +# Roll the args list around exactly as many times as the number of +# args, so each arg winds up back in the position where it started, but +# possibly modified. +# +# NB: a `for` loop captures its iteration list before it begins, so +# changing the positional parameters here affects neither the number of +# iterations, nor the values presented in `arg`. +shift # remove old arg +set -- "$@" "$arg" # push replacement arg +done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ +"-Dorg.gradle.appname=$APP_BASE_NAME" \ +-classpath "$CLASSPATH" \ +org.gradle.wrapper.GradleWrapperMain \ +"$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then +die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( +printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | +xargs -n1 | +sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | +tr '\n' ' ' +)" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/cloud-agent/client/kotlin/settings.gradle b/cloud-agent/client/kotlin/settings.gradle index b5dc286913..4765fb4704 100644 --- a/cloud-agent/client/kotlin/settings.gradle +++ b/cloud-agent/client/kotlin/settings.gradle @@ -1,2 +1 @@ - -rootProject.name = 'cloud-agent-client-kotlin' \ No newline at end of file +rootProject.name = 'cloud-agent-client-kotlin' diff --git a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/adapters/StringOrStringArrayAdapter.kt b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/adapters/StringOrStringArrayAdapter.kt new file mode 100644 index 0000000000..cebdbbe604 --- /dev/null +++ b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/adapters/StringOrStringArrayAdapter.kt @@ -0,0 +1,33 @@ +package org.hyperledger.identus.client.adapters + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonSerializer +import com.google.gson.JsonNull +import com.google.gson.JsonParseException +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import java.lang.reflect.Type + +class StringOrStringArrayAdapter : JsonSerializer>, JsonDeserializer> { + + // Deserialize logic: String or Array of Strings to List + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): List { + return when { + json.isJsonArray -> context.deserialize(json, typeOfT) + json.isJsonPrimitive -> listOf(json.asString) + json.isJsonNull -> emptyList() + else -> throw JsonParseException("Unexpected type for field") + } + } + + // Serialize logic: List to String or Array of Strings + override fun serialize(src: List?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { + return when { + src.isNullOrEmpty() -> JsonNull.INSTANCE + src.size == 1 -> JsonPrimitive(src[0]) // If only one string, serialize as a single string + else -> context!!.serialize(src) // Otherwise, serialize as a list + } + } +} \ No newline at end of file diff --git a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequest.kt b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequest.kt new file mode 100644 index 0000000000..7518b6a52e --- /dev/null +++ b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/CreateIssueCredentialRecordRequest.kt @@ -0,0 +1,86 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package org.hyperledger.identus.client.models + +import com.google.gson.annotations.JsonAdapter +import com.google.gson.annotations.SerializedName +import org.hyperledger.identus.client.adapters.StringOrStringArrayAdapter + +/** + * + * + * @param claims The set of claims that will be included in the issued credential. The JSON object should comply with the schema applicable for this offer (i.e. 'schemaId' or 'credentialDefinitionId'). + * @param issuingDID The issuer Prism DID by which the verifiable credential will be issued. DID can be short for or long form. + * @param validityPeriod The validity period in seconds of the verifiable credential that will be issued. + * @param schemaId + * @param credentialDefinitionId The unique identifier (UUID) of the credential definition that will be used for this offer. It should be the identifier of a credential definition that exists in the issuer agent's database. Note that this parameter only applies when the offer is of type 'AnonCreds'. + * @param credentialFormat The credential format for this offer (defaults to 'JWT') + * @param automaticIssuance Specifies whether or not the credential should be automatically generated and issued when receiving the `CredentialRequest` from the holder. If set to `false`, a manual approval by the issuer via another API call will be required for the VC to be issued. + * @param issuingKid Specified the key ID (kid) of the DID, it will be used to sign credential. User should specify just the partial identifier of the key. The full id of the kid MUST be \"#\" Note the cryto algorithm used with depend type of the key. + * @param connectionId The unique identifier of a DIDComm connection that already exists between the this issuer agent and the holder cloud or edeg agent. It should be the identifier of a connection that exists in the issuer agent's database. This connection will be used to execute the issue credential protocol. Note: connectionId is only required when the offer is from existing connection. connectionId is not required when the offer is from invitation for connectionless issuance. + * @param goalCode A self-attested code the receiver may want to display to the user or use in automatically deciding what to do with the out-of-band message. goalcode is optional and can be provided when the offer is from invitation for connectionless issuance. + * @param goal A self-attested string that the receiver may want to display to the user about the context-specific goal of the out-of-band message. goal is optional and can be provided when the offer is from invitation for connectionless issuance. + */ + + +data class CreateIssueCredentialRecordRequest( + + /* The set of claims that will be included in the issued credential. The JSON object should comply with the schema applicable for this offer (i.e. 'schemaId' or 'credentialDefinitionId'). */ + @SerializedName("claims") + val claims: kotlin.Any?, + + /* The issuer Prism DID by which the verifiable credential will be issued. DID can be short for or long form. */ + @SerializedName("issuingDID") + val issuingDID: kotlin.String, + + /* The validity period in seconds of the verifiable credential that will be issued. */ + @SerializedName("validityPeriod") + val validityPeriod: kotlin.Double? = null, + + @SerializedName("schemaId") + @JsonAdapter(StringOrStringArrayAdapter::class) + val schemaId: kotlin.collections.List? = null, + + /* The unique identifier (UUID) of the credential definition that will be used for this offer. It should be the identifier of a credential definition that exists in the issuer agent's database. Note that this parameter only applies when the offer is of type 'AnonCreds'. */ + @SerializedName("credentialDefinitionId") + val credentialDefinitionId: java.util.UUID? = null, + + /* The credential format for this offer (defaults to 'JWT') */ + @SerializedName("credentialFormat") + val credentialFormat: kotlin.String? = null, + + /* Specifies whether or not the credential should be automatically generated and issued when receiving the `CredentialRequest` from the holder. If set to `false`, a manual approval by the issuer via another API call will be required for the VC to be issued. */ + @SerializedName("automaticIssuance") + val automaticIssuance: kotlin.Boolean? = null, + + /* Specified the key ID (kid) of the DID, it will be used to sign credential. User should specify just the partial identifier of the key. The full id of the kid MUST be \"#\" Note the cryto algorithm used with depend type of the key. */ + @SerializedName("issuingKid") + val issuingKid: kotlin.String? = null, + + /* The unique identifier of a DIDComm connection that already exists between the this issuer agent and the holder cloud or edeg agent. It should be the identifier of a connection that exists in the issuer agent's database. This connection will be used to execute the issue credential protocol. Note: connectionId is only required when the offer is from existing connection. connectionId is not required when the offer is from invitation for connectionless issuance. */ + @SerializedName("connectionId") + val connectionId: java.util.UUID? = null, + + /* A self-attested code the receiver may want to display to the user or use in automatically deciding what to do with the out-of-band message. goalcode is optional and can be provided when the offer is from invitation for connectionless issuance. */ + @SerializedName("goalCode") + val goalCode: kotlin.String? = null, + + /* A self-attested string that the receiver may want to display to the user about the context-specific goal of the out-of-band message. goal is optional and can be provided when the offer is from invitation for connectionless issuance. */ + @SerializedName("goal") + val goal: kotlin.String? = null + +) + diff --git a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt index a331caee07..0ec683b89b 100644 --- a/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt +++ b/cloud-agent/client/kotlin/src/main/kotlin/org/hyperledger/identus/client/models/Service.kt @@ -15,35 +15,10 @@ package org.hyperledger.identus.client.models -import com.google.gson.* +import com.google.gson.JsonElement import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName -import java.lang.reflect.Type - -class StringOrStringArrayAdapter : JsonSerializer>, JsonDeserializer> { - - // Deserialize logic: String or Array of Strings to List - override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): List { - return when { - json?.isJsonArray == true -> { - context!!.deserialize(json, typeOfT) - } - json?.isJsonPrimitive == true -> { - listOf(json.asString) - } - else -> throw JsonParseException("Unexpected type for field") - } - } - - // Serialize logic: List to String or Array of Strings - override fun serialize(src: List?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - return when { - src == null -> JsonNull.INSTANCE - src.size == 1 -> JsonPrimitive(src[0]) // If only one string, serialize as a single string - else -> context!!.serialize(src) // Otherwise, serialize as a list - } - } -} +import org.hyperledger.identus.client.adapters.StringOrStringArrayAdapter data class Service( @@ -58,4 +33,4 @@ data class Service( @SerializedName("serviceEndpoint") val serviceEndpoint: JsonElement? = null, -) + ) diff --git a/cloud-agent/client/typescript/.openapi-generator-ignore b/cloud-agent/client/typescript/.openapi-generator-ignore index af11cd214d..cf11c2202e 100644 --- a/cloud-agent/client/typescript/.openapi-generator-ignore +++ b/cloud-agent/client/typescript/.openapi-generator-ignore @@ -6,3 +6,4 @@ models/CredentialRequest.ts models/Proof2.ts models/Service.ts models/UpdateManagedDIDServiceAction.ts +models/CreateIssueCredentialRecordRequest.ts diff --git a/cloud-agent/client/typescript/models/CreateIssueCredentialRecordRequest.ts b/cloud-agent/client/typescript/models/CreateIssueCredentialRecordRequest.ts new file mode 100644 index 0000000000..14190cd10d --- /dev/null +++ b/cloud-agent/client/typescript/models/CreateIssueCredentialRecordRequest.ts @@ -0,0 +1,135 @@ +/** + * Identus Cloud Agent API Reference + * The Identus Cloud Agent API facilitates the integration and management of self-sovereign identity capabilities within applications. It supports DID (Decentralized Identifiers) management, verifiable credential exchange, and secure messaging based on DIDComm standards. The API is designed to be interoperable with various blockchain and DLT (Distributed Ledger Technology) platforms, ensuring wide compatibility and flexibility. Key features include connection management, credential issuance and verification, and secure, privacy-preserving communication between entities. Additional information and the full list of capabilities can be found in the [Open Enterprise Agent documentation](https://docs.atalaprism.io/docs/category/prism-cloud-agent) + * + * OpenAPI spec version: 1.39.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { HttpFile } from '../http/http'; + +export class CreateIssueCredentialRecordRequest { + /** + * The validity period in seconds of the verifiable credential that will be issued. + */ + 'validityPeriod'?: number; + 'schemaId'?: string | Array; + /** + * The unique identifier (UUID) of the credential definition that will be used for this offer. It should be the identifier of a credential definition that exists in the issuer agent\'s database. Note that this parameter only applies when the offer is of type \'AnonCreds\'. + */ + 'credentialDefinitionId'?: string; + /** + * The credential format for this offer (defaults to \'JWT\') + */ + 'credentialFormat'?: string; + /** + * The set of claims that will be included in the issued credential. The JSON object should comply with the schema applicable for this offer (i.e. \'schemaId\' or \'credentialDefinitionId\'). + */ + 'claims': any | null; + /** + * Specifies whether or not the credential should be automatically generated and issued when receiving the `CredentialRequest` from the holder. If set to `false`, a manual approval by the issuer via another API call will be required for the VC to be issued. + */ + 'automaticIssuance'?: boolean; + /** + * The issuer Prism DID by which the verifiable credential will be issued. DID can be short for or long form. + */ + 'issuingDID': string; + /** + * Specified the key ID (kid) of the DID, it will be used to sign credential. User should specify just the partial identifier of the key. The full id of the kid MUST be \"#\" Note the cryto algorithm used with depend type of the key. + */ + 'issuingKid'?: string; + /** + * The unique identifier of a DIDComm connection that already exists between the this issuer agent and the holder cloud or edeg agent. It should be the identifier of a connection that exists in the issuer agent\'s database. This connection will be used to execute the issue credential protocol. Note: connectionId is only required when the offer is from existing connection. connectionId is not required when the offer is from invitation for connectionless issuance. + */ + 'connectionId'?: string; + /** + * A self-attested code the receiver may want to display to the user or use in automatically deciding what to do with the out-of-band message. goalcode is optional and can be provided when the offer is from invitation for connectionless issuance. + */ + 'goalCode'?: string; + /** + * A self-attested string that the receiver may want to display to the user about the context-specific goal of the out-of-band message. goal is optional and can be provided when the offer is from invitation for connectionless issuance. + */ + 'goal'?: string; + + static readonly discriminator: string | undefined = undefined; + + static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [ + { + "name": "validityPeriod", + "baseName": "validityPeriod", + "type": "number", + "format": "double" + }, + { + "name": "schemaId", + "baseName": "schemaId", + "type": "CreateIssueCredentialRecordRequestSchemaId", + "format": "" + }, + { + "name": "credentialDefinitionId", + "baseName": "credentialDefinitionId", + "type": "string", + "format": "uuid" + }, + { + "name": "credentialFormat", + "baseName": "credentialFormat", + "type": "string", + "format": "" + }, + { + "name": "claims", + "baseName": "claims", + "type": "any", + "format": "" + }, + { + "name": "automaticIssuance", + "baseName": "automaticIssuance", + "type": "boolean", + "format": "" + }, + { + "name": "issuingDID", + "baseName": "issuingDID", + "type": "string", + "format": "" + }, + { + "name": "issuingKid", + "baseName": "issuingKid", + "type": "string", + "format": "" + }, + { + "name": "connectionId", + "baseName": "connectionId", + "type": "string", + "format": "uuid" + }, + { + "name": "goalCode", + "baseName": "goalCode", + "type": "string", + "format": "" + }, + { + "name": "goal", + "baseName": "goal", + "type": "string", + "format": "" + } ]; + + static getAttributeTypeMap() { + return CreateIssueCredentialRecordRequest.attributeTypeMap; + } + + public constructor() { + } +} + diff --git a/tests/integration-tests/build.gradle.kts b/tests/integration-tests/build.gradle.kts index 3efb0d40a8..7a653b4398 100644 --- a/tests/integration-tests/build.gradle.kts +++ b/tests/integration-tests/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { testImplementation("io.ktor:ktor-server-netty:2.3.0") testImplementation("io.ktor:ktor-client-apache:2.3.0") // RestAPI client - testImplementation("org.hyperledger.identus:cloud-agent-client-kotlin:1.39.1-e8ad592") + testImplementation("org.hyperledger.identus:cloud-agent-client-kotlin:1.39.1-bbcedb1") // Test helpers library testImplementation("io.iohk.atala:atala-automation:0.4.0") // Hoplite for configuration diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt index b5026a7163..a25abc6ade 100644 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt @@ -35,7 +35,7 @@ class JwtCredentialSteps { } val credentialOfferRequest = CreateIssueCredentialRecordRequest( - schemaId = schemaId, + schemaId = schemaId?.let { listOf(it) }, claims = claims, issuingDID = did, connectionId = issuer.recall("connection-with-${holder.name}").connectionId, diff --git a/tests/performance-tests/agent-performance-tests-k6/.env b/tests/performance-tests/agent-performance-tests-k6/.env index 5d67c0931e..24d8296cbe 100644 --- a/tests/performance-tests/agent-performance-tests-k6/.env +++ b/tests/performance-tests/agent-performance-tests-k6/.env @@ -1,3 +1,3 @@ -AGENT_VERSION=1.39.0-SNAPSHOT -PRISM_NODE_VERSION=2.3.0 +AGENT_VERSION=1.39.1-SNAPSHOT +PRISM_NODE_VERSION=2.5.0 VAULT_DEV_ROOT_TOKEN_ID=root diff --git a/tests/performance-tests/agent-performance-tests-k6/package.json b/tests/performance-tests/agent-performance-tests-k6/package.json index 3286629382..65d5b5f020 100644 --- a/tests/performance-tests/agent-performance-tests-k6/package.json +++ b/tests/performance-tests/agent-performance-tests-k6/package.json @@ -26,7 +26,7 @@ "webpack": "webpack" }, "dependencies": { - "@hyperledger/identus-cloud-agent-client-ts": "^1.39.0-e077cdd", + "@hyperledger/identus-cloud-agent-client-ts": "^1.39.1-bbcedb1", "uuid": "^9.0.0" } } diff --git a/tests/performance-tests/agent-performance-tests-k6/yarn.lock b/tests/performance-tests/agent-performance-tests-k6/yarn.lock index e743505b63..736d3f9bc0 100644 --- a/tests/performance-tests/agent-performance-tests-k6/yarn.lock +++ b/tests/performance-tests/agent-performance-tests-k6/yarn.lock @@ -993,10 +993,10 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@hyperledger/identus-cloud-agent-client-ts@^1.39.0-e077cdd": - version "1.39.0-e077cdd" - resolved "https://npm.pkg.github.com/download/@hyperledger/identus-cloud-agent-client-ts/1.39.0-e077cdd/06803b9bd2fa7d63805f83df22250882e84c94dd#06803b9bd2fa7d63805f83df22250882e84c94dd" - integrity sha512-3FSz2WlrykyF5LqnrI+wcbrY33i8CeyBNQmEYZ9fp84JL3qhWvnF92dBIS9qpgAQY/qIg5vHvL7RIRNrAxQfOw== +"@hyperledger/identus-cloud-agent-client-ts@^1.39.1-bbcedb1": + version "1.39.1-bbcedb1" + resolved "https://npm.pkg.github.com/download/@hyperledger/identus-cloud-agent-client-ts/1.39.1-bbcedb1/88aaeabfc4d2d8949e21014c2a5c9297ed055d42#88aaeabfc4d2d8949e21014c2a5c9297ed055d42" + integrity sha512-FjYV4HN5H/LD/v6dOw/vMaqU3f8v1IKzEMtUfj9qmLHVmr1FwxkZWSj6wE27I+sY/0sGAPCF/rPbvO27UWdtYQ== dependencies: es6-promise "^4.2.4" url-parse "^1.4.3" From 19ab426a191eec575ffebe6a2417f3fce538969c Mon Sep 17 00:00:00 2001 From: bvoiturier Date: Wed, 9 Oct 2024 09:49:36 +0200 Subject: [PATCH 13/13] feat: ATL-6983 ZIO Stream Kafka PoC in background jobs (#1339) Signed-off-by: Benjamin Voiturier Signed-off-by: mineme0110 Signed-off-by: Hyperledger Bot Co-authored-by: mineme0110 Co-authored-by: Hyperledger Bot --- build.sbt | 13 +- .../src/main/resources/application.conf | 67 +++-- .../identus/agent/server/CloudAgentApp.scala | 94 +----- .../identus/agent/server/MainApp.scala | 15 +- .../agent/server/config/AppConfig.scala | 18 +- .../http/CustomServerInterceptors.scala | 93 +++--- .../server/http/ZHttp4sBlazeServer.scala | 7 +- .../server/jobs/BackgroundJobsHelper.scala | 22 +- .../server/jobs/ConnectBackgroundJobs.scala | 84 +++--- .../jobs/DIDStateSyncBackgroundJobs.scala | 51 +++- .../server/jobs/IssueBackgroundJobs.scala | 100 +++---- .../server/jobs/PresentBackgroundJobs.scala | 80 +++--- .../agent/server/jobs/StatusListJobs.scala | 268 +++++++++++------- .../controller/IssueControllerTestTools.scala | 6 +- .../OIDCCredentialIssuerServiceSpec.scala | 5 +- .../CredentialDefinitionTestTools.scala | 6 +- .../schema/CredentialSchemaTestTools.scala | 6 +- .../SystemControllerTestTools.scala | 6 +- .../VcVerificationControllerTestTools.scala | 6 +- .../sql/agent/V15__add_did_index_table.sql | 19 ++ .../service/ManagedDIDServiceImpl.scala | 19 +- ...dDIDServiceWithEventNotificationImpl.scala | 5 +- .../service/handler/DIDCreateHandler.scala | 7 +- .../sql/JdbcDIDNonSecretStorage.scala | 53 +++- .../storage/DIDNonSecretStorage.scala | 2 + .../storage/MockDIDNonSecretStorage.scala | 4 + .../core/model/WalletIdAndRecordId.scala | 19 ++ .../core/service/ConnectionServiceImpl.scala | 19 +- .../service/ConnectionServiceImplSpec.scala | 12 +- .../ConnectionServiceNotifierSpec.scala | 8 +- .../messaging/MessagingServiceTest.scala | 49 ++++ .../kafka/InMemoryMessagingServiceSpec.scala | 66 +++++ .../shared/docker-compose-with-kafka.yml | 256 +++++++++++++++++ infrastructure/shared/nginx/nginx.conf | 42 +++ .../identus/pollux/core/model/DidCommID.scala | 4 +- .../CredentialStatusListRepository.scala | 47 ++- .../core/service/CredentialServiceImpl.scala | 72 +++-- .../service/CredentialStatusListService.scala | 6 +- .../CredentialStatusListServiceImpl.scala | 9 +- .../core/service/PresentationService.scala | 2 +- .../service/PresentationServiceImpl.scala | 49 +++- .../service/PresentationServiceNotifier.scala | 3 +- ...edentialStatusListRepositoryInMemory.scala | 182 ++++++------ .../service/CredentialServiceSpecHelper.scala | 4 +- .../service/MockPresentationService.scala | 5 +- .../PresentationServiceSpecHelper.scala | 6 +- .../JdbcCredentialStatusListRepository.scala | 213 +++++++------- .../JdbcPresentationRepository.scala | 1 - .../shared/messaging/MessagingService.scala | 106 +++++++ .../messaging/MessagingServiceConfig.scala | 58 ++++ .../identus/shared/messaging/Serde.scala | 55 ++++ .../messaging/WalletIdAndRecordId.scala | 20 ++ .../kafka/InMemoryMessagingService.scala | 146 ++++++++++ .../kafka/ZKafkaMessagingServiceImpl.scala | 136 +++++++++ .../src/test/resources/containers/agent.yml | 94 ++++++ 55 files changed, 2031 insertions(+), 714 deletions(-) create mode 100644 cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql create mode 100644 connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala create mode 100644 event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala create mode 100644 event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala create mode 100644 infrastructure/shared/docker-compose-with-kafka.yml create mode 100644 infrastructure/shared/nginx/nginx.conf create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala create mode 100644 shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala diff --git a/build.sbt b/build.sbt index ca50625deb..0df784ff60 100644 --- a/build.sbt +++ b/build.sbt @@ -36,7 +36,8 @@ inThisBuild( // scalacOptions += "-Yexplicit-nulls", // scalacOptions += "-Ysafe-init", // scalacOptions += "-Werror", // <=> "-Xfatal-warnings" - scalacOptions += "-Dquill.macro.log=false", // disable quill macro logs // TODO https://github.com/zio/zio-protoquill/issues/470 + scalacOptions += "-Dquill.macro.log=false", // disable quill macro logs // TODO https://github.com/zio/zio-protoquill/issues/470, + scalacOptions ++= Seq("-Xmax-inlines", "50") // manually increase max-inlines above 32 (https://github.com/circe/circe/issues/2162) ) ) @@ -53,6 +54,7 @@ lazy val V = new { val zioCatsInterop = "3.3.0" // TODO "23.1.0.2" // https://mvnrepository.com/artifact/dev.zio/zio-interop-cats val zioMetricsConnector = "2.3.1" val zioMock = "1.0.0-RC12" + val zioKafka = "2.7.5" val mockito = "3.2.18.0" val monocle = "3.2.0" @@ -102,7 +104,11 @@ lazy val D = new { val zioLog: ModuleID = "dev.zio" %% "zio-logging" % V.zioLogging val zioSLF4J: ModuleID = "dev.zio" %% "zio-logging-slf4j" % V.zioLogging val zioJson: ModuleID = "dev.zio" %% "zio-json" % V.zioJson + val zioConcurrent: ModuleID = "dev.zio" %% "zio-concurrent" % V.zio val zioHttp: ModuleID = "dev.zio" %% "zio-http" % V.zioHttp + val zioKafka: ModuleID = "dev.zio" %% "zio-kafka" % V.zioKafka excludeAll ( + ExclusionRule("dev.zio", "zio_3"), ExclusionRule("dev.zio", "zio-streams_3") + ) val zioCatsInterop: ModuleID = "dev.zio" %% "zio-interop-cats" % V.zioCatsInterop val zioMetricsConnectorMicrometer: ModuleID = "dev.zio" %% "zio-metrics-connectors-micrometer" % V.zioMetricsConnector val tapirPrometheusMetrics: ModuleID = "com.softwaremill.sttp.tapir" %% "tapir-prometheus-metrics" % V.tapir @@ -185,7 +191,9 @@ lazy val D_Shared = new { D.typesafeConfig, D.scalaPbGrpc, D.zio, + D.zioConcurrent, D.zioHttp, + D.zioKafka, D.scalaUri, D.zioPrelude, // FIXME: split shared DB stuff as subproject? @@ -341,12 +349,11 @@ lazy val D_Pollux_VC_JWT = new { lazy val D_EventNotification = new { val zio = "dev.zio" %% "zio" % V.zio - val zioConcurrent = "dev.zio" %% "zio-concurrent" % V.zio val zioTest = "dev.zio" %% "zio-test" % V.zio % Test val zioTestSbt = "dev.zio" %% "zio-test-sbt" % V.zio % Test val zioTestMagnolia = "dev.zio" %% "zio-test-magnolia" % V.zio % Test - val zioDependencies: Seq[ModuleID] = Seq(zio, zioConcurrent, zioTest, zioTestSbt, zioTestMagnolia) + val zioDependencies: Seq[ModuleID] = Seq(zio, zioTest, zioTestSbt, zioTestMagnolia) val baseDependencies: Seq[ModuleID] = zioDependencies } diff --git a/cloud-agent/service/server/src/main/resources/application.conf b/cloud-agent/service/server/src/main/resources/application.conf index 13f2a0b4bb..7b5bebb01a 100644 --- a/cloud-agent/service/server/src/main/resources/application.conf +++ b/cloud-agent/service/server/src/main/resources/application.conf @@ -34,22 +34,10 @@ pollux { publicEndpointUrl = "http://localhost:"${agent.httpEndpoint.http.port} publicEndpointUrl = ${?POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL} } - issueBgJobRecordsLimit = 25 - issueBgJobRecordsLimit = ${?ISSUE_BG_JOB_RECORDS_LIMIT} - issueBgJobRecurrenceDelay = 2 seconds - issueBgJobRecurrenceDelay = ${?ISSUE_BG_JOB_RECURRENCE_DELAY} - issueBgJobProcessingParallelism = 5 - issueBgJobProcessingParallelism = ${?ISSUE_BG_JOB_PROCESSING_PARALLELISM} - presentationBgJobRecordsLimit = 25 - presentationBgJobRecordsLimit = ${?PRESENTATION_BG_JOB_RECORDS_LIMIT} - presentationBgJobRecurrenceDelay = 2 seconds - presentationBgJobRecurrenceDelay = ${?PRESENTATION_BG_JOB_RECURRENCE_DELAY} - presentationBgJobProcessingParallelism = 5 - presentationBgJobProcessingParallelism = ${?PRESENTATION_BG_JOB_PROCESSING_PARALLELISM} - syncRevocationStatusesBgJobRecurrenceDelay = 2 seconds - syncRevocationStatusesBgJobRecurrenceDelay = ${?SYNC_REVOCATION_STATUSES_BG_JOB_RECURRENCE_DELAY} - syncRevocationStatusesBgJobProcessingParallelism = 5 - syncRevocationStatusesBgJobProcessingParallelism = ${?SYNC_REVOCATION_STATUSES_BG_JOB_PROCESSING_PARALLELISM} + statusListSyncTriggerRecurrenceDelay = 30 seconds + statusListSyncTriggerRecurrenceDelay = ${?STATUS_LIST_SYNC_TRIGGER_RECURRENCE_DELAY} + didStateSyncTriggerRecurrenceDelay = 30 seconds + didStateSyncTriggerRecurrenceDelay = ${?DID_STATE_SYNC_TRIGGER_RECURRENCE_DELAY} credential.sdJwt.expiry = 30 days credential.sdJwt.expiry = ${?CREDENTIAL_SD_JWT_EXPIRY} presentationInvitationExpiry = 300 seconds @@ -81,8 +69,6 @@ connect { connectBgJobRecordsLimit = ${?CONNECT_BG_JOB_RECORDS_LIMIT} connectBgJobRecurrenceDelay = 2 seconds connectBgJobRecurrenceDelay = ${?CONNECT_BG_JOB_RECURRENCE_DELAY} - connectBgJobProcessingParallelism = 5 - connectBgJobProcessingParallelism = ${?CONNECT_BG_JOB_PROCESSING_PARALLELISM} connectInvitationExpiry = 300 seconds connectInvitationExpiry = ${?CONNECT_INVITATION_EXPIRY} } @@ -262,4 +248,49 @@ agent { authApiKey = "default" authApiKey = ${?DEFAULT_WALLET_AUTH_API_KEY} } + messagingService { + connectFlow { + consumerCount = 5 + retryStrategy { + maxRetries = 4 + initialDelay = 5.seconds + maxDelay = 40.seconds + } + } + issueFlow { + consumerCount = 5 + retryStrategy { + maxRetries = 4 + initialDelay = 5.seconds + maxDelay = 40.seconds + } + } + presentFlow { + consumerCount = 5 + retryStrategy { + maxRetries = 4 + initialDelay = 5.seconds + maxDelay = 40.seconds + } + } + didStateSync { + consumerCount = 5 + } + statusListSync { + consumerCount = 5 + } + inMemoryQueueCapacity = 1000 + kafkaEnabled = false + kafkaEnabled = ${?DEFAULT_KAFKA_ENABLED} + kafka { + bootstrapServers = "kafka:9092" + consumers { + autoCreateTopics = false, + maxPollRecords = 500 + maxPollInterval = 5.minutes + pollTimeout = 50.millis + rebalanceSafeCommits = true + } + } + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala index d3af09cf2b..d010d4c5f6 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala @@ -5,12 +5,9 @@ import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.http.{ZHttp4sBlazeServer, ZHttpEndpoints} import org.hyperledger.identus.agent.server.jobs.* import org.hyperledger.identus.agent.walletapi.model.{Entity, Wallet, WalletSeed} -import org.hyperledger.identus.agent.walletapi.service.{EntityService, ManagedDIDService, WalletManagementService} -import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage +import org.hyperledger.identus.agent.walletapi.service.{EntityService, WalletManagementService} import org.hyperledger.identus.castor.controller.{DIDRegistrarServerEndpoints, DIDServerEndpoints} -import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.connect.controller.ConnectionServerEndpoints -import org.hyperledger.identus.connect.core.service.ConnectionService import org.hyperledger.identus.credentialstatus.controller.CredentialStatusServiceEndpoints import org.hyperledger.identus.event.controller.EventServerEndpoints import org.hyperledger.identus.event.notification.EventNotificationConfig @@ -18,108 +15,35 @@ import org.hyperledger.identus.iam.authentication.apikey.ApiKeyAuthenticator import org.hyperledger.identus.iam.entity.http.EntityServerEndpoints import org.hyperledger.identus.iam.wallet.http.WalletManagementServerEndpoints import org.hyperledger.identus.issue.controller.IssueServerEndpoints -import org.hyperledger.identus.mercury.{DidOps, HttpClient} import org.hyperledger.identus.oid4vci.CredentialIssuerServerEndpoints -import org.hyperledger.identus.pollux.core.service.{CredentialService, PresentationService} import org.hyperledger.identus.pollux.credentialdefinition.CredentialDefinitionRegistryServerEndpoints import org.hyperledger.identus.pollux.credentialschema.{ SchemaRegistryServerEndpoints, VerificationPolicyServerEndpoints } import org.hyperledger.identus.pollux.prex.PresentationExchangeServerEndpoints -import org.hyperledger.identus.pollux.vc.jwt.DidResolver as JwtDidResolver import org.hyperledger.identus.presentproof.controller.PresentProofServerEndpoints -import org.hyperledger.identus.resolvers.DIDResolver -import org.hyperledger.identus.shared.http.UriResolver -import org.hyperledger.identus.shared.models.{HexString, WalletAccessContext, WalletAdministrationContext, WalletId} -import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds +import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.system.controller.SystemServerEndpoints import org.hyperledger.identus.verification.controller.VcVerificationServerEndpoints import zio.* -import zio.metrics.* - object CloudAgentApp { def run = for { _ <- AgentInitialization.run - _ <- issueCredentialDidCommExchangesJob.debug.fork - _ <- presentProofExchangeJob.debug.fork - _ <- connectDidCommExchangesJob.debug.fork - _ <- syncDIDPublicationStateFromDltJob.debug.fork - _ <- syncRevocationStatusListsJob.debug.fork + _ <- ConnectBackgroundJobs.connectFlowsHandler + _ <- IssueBackgroundJobs.issueFlowsHandler + _ <- PresentBackgroundJobs.presentFlowsHandler + _ <- DIDStateSyncBackgroundJobs.didStateSyncTrigger + _ <- DIDStateSyncBackgroundJobs.didStateSyncHandler + _ <- StatusListJobs.statusListsSyncTrigger + _ <- StatusListJobs.statusListSyncHandler _ <- AgentHttpServer.run.tapDefect(e => ZIO.logErrorCause("Agent HTTP Server failure", e)).fork fiber <- DidCommHttpServer.run.tapDefect(e => ZIO.logErrorCause("DIDComm HTTP Server failure", e)).fork _ <- WebhookPublisher.layer.build.map(_.get[WebhookPublisher]).flatMap(_.run.fork) _ <- fiber.join *> ZIO.log(s"Server End") _ <- ZIO.never } yield () - - private val issueCredentialDidCommExchangesJob: RIO[ - AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & CredentialService & DIDNonSecretStorage & - DIDService & ManagedDIDService & PresentationService & WalletManagementService, - Unit - ] = - for { - config <- ZIO.service[AppConfig] - _ <- (IssueBackgroundJobs.issueCredentialDidCommExchanges @@ Metric - .gauge("issuance_flow_did_com_exchange_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.pollux.issueBgJobRecurrenceDelay)) - .unit - } yield () - - private val presentProofExchangeJob: RIO[ - AppConfig & DidOps & UriResolver & DIDResolver & JwtDidResolver & HttpClient & PresentationService & - CredentialService & DIDNonSecretStorage & DIDService & ManagedDIDService, - Unit - ] = - for { - config <- ZIO.service[AppConfig] - _ <- (PresentBackgroundJobs.presentProofExchanges @@ Metric - .gauge("present_proof_flow_did_com_exchange_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.pollux.presentationBgJobRecurrenceDelay)) - .unit - } yield () - - private val connectDidCommExchangesJob: RIO[ - AppConfig & DidOps & DIDResolver & HttpClient & ConnectionService & ManagedDIDService & DIDNonSecretStorage & - WalletManagementService, - Unit - ] = - for { - config <- ZIO.service[AppConfig] - _ <- (ConnectBackgroundJobs.didCommExchanges @@ Metric - .gauge("connection_flow_did_com_exchange_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.connect.connectBgJobRecurrenceDelay)) - .unit - } yield () - - private val syncRevocationStatusListsJob = { - for { - config <- ZIO.service[AppConfig] - _ <- (StatusListJobs.syncRevocationStatuses @@ Metric - .gauge("revocation_status_list_sync_job_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .repeat(Schedule.spaced(config.pollux.syncRevocationStatusesBgJobRecurrenceDelay)) - } yield () - } - - private val syncDIDPublicationStateFromDltJob: URIO[ManagedDIDService & WalletManagementService, Unit] = - ZIO - .serviceWithZIO[WalletManagementService](_.listWallets().map(_._1)) - .flatMap { wallets => - ZIO.foreach(wallets) { wallet => - DIDStateSyncBackgroundJobs.syncDIDPublicationStateFromDlt - .provideSomeLayer(ZLayer.succeed(WalletAccessContext(wallet.id))) - } - } - .catchAll(e => ZIO.logError(s"error while syncing DID publication state: $e")) - .repeat(Schedule.spaced(10.seconds)) - .unit - .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) - } object AgentHttpServer { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala index 9f163a0a11..922593389a 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala @@ -7,7 +7,6 @@ import org.hyperledger.identus.agent.server.http.ZioHttpClient import org.hyperledger.identus.agent.server.sql.Migrations as AgentMigrations import org.hyperledger.identus.agent.walletapi.service.{ EntityServiceImpl, - ManagedDIDService, ManagedDIDServiceWithEventNotificationImpl, WalletManagementServiceImpl } @@ -16,7 +15,6 @@ import org.hyperledger.identus.agent.walletapi.sql.{ JdbcEntityRepository, JdbcWalletNonSecretStorage } -import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage import org.hyperledger.identus.castor.controller.{DIDControllerImpl, DIDRegistrarControllerImpl} import org.hyperledger.identus.castor.core.model.did.{ Service as DidDocumentService, @@ -36,7 +34,7 @@ import org.hyperledger.identus.iam.authentication.{DefaultAuthenticator, Oid4vci import org.hyperledger.identus.iam.authentication.apikey.JdbcAuthenticationRepository import org.hyperledger.identus.iam.authorization.core.EntityPermissionManagementService import org.hyperledger.identus.iam.authorization.DefaultPermissionManagementService -import org.hyperledger.identus.iam.entity.http.controller.{EntityController, EntityControllerImpl} +import org.hyperledger.identus.iam.entity.http.controller.EntityControllerImpl import org.hyperledger.identus.iam.wallet.http.controller.WalletManagementControllerImpl import org.hyperledger.identus.issue.controller.IssueControllerImpl import org.hyperledger.identus.mercury.* @@ -47,7 +45,6 @@ import org.hyperledger.identus.pollux.core.service.* import org.hyperledger.identus.pollux.core.service.verification.VcVerificationServiceImpl import org.hyperledger.identus.pollux.credentialdefinition.controller.CredentialDefinitionControllerImpl import org.hyperledger.identus.pollux.credentialschema.controller.{ - CredentialSchemaController, CredentialSchemaControllerImpl, VerificationPolicyControllerImpl } @@ -66,6 +63,9 @@ import org.hyperledger.identus.pollux.sql.repository.{ } import org.hyperledger.identus.presentproof.controller.PresentProofControllerImpl import org.hyperledger.identus.resolvers.DIDResolver +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.WalletIdAndRecordId +import org.hyperledger.identus.shared.models.WalletId import org.hyperledger.identus.system.controller.SystemControllerImpl import org.hyperledger.identus.verification.controller.VcVerificationControllerImpl import zio.* @@ -77,6 +77,7 @@ import zio.metrics.connectors.micrometer.MicrometerConfig import zio.metrics.jvm.DefaultJvmMetrics import java.security.Security +import java.util.UUID object MainApp extends ZIOAppDefault { @@ -167,7 +168,6 @@ object MainApp extends ZIOAppDefault { ) _ <- preMigrations _ <- migrations - app <- CloudAgentApp.run .provide( DidCommX.liveLayer, @@ -252,6 +252,11 @@ object MainApp extends ZIOAppDefault { // HTTP client SystemModule.zioHttpClientLayer, Scope.default, + // Messaging Service + ZLayer.fromZIO(ZIO.service[AppConfig].map(_.agent.messagingService)), + messaging.MessagingService.serviceLayer, + messaging.MessagingService.producerLayer[UUID, WalletIdAndRecordId], + messaging.MessagingService.producerLayer[WalletId, WalletId] ) } yield app diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala index 0f75561812..364ff510bc 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/config/AppConfig.scala @@ -4,7 +4,7 @@ import org.hyperledger.identus.castor.core.model.did.VerificationRelationship import org.hyperledger.identus.iam.authentication.AuthenticationConfig import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.db.DbConfig -import zio.config.* +import org.hyperledger.identus.shared.messaging.MessagingServiceConfig import zio.config.magnolia.* import zio.Config @@ -70,22 +70,13 @@ final case class PolluxConfig( database: DatabaseConfig, credentialSdJwtExpirationTime: Duration, statusListRegistry: StatusListRegistryConfig, - issueBgJobRecordsLimit: Int, - issueBgJobRecurrenceDelay: Duration, - issueBgJobProcessingParallelism: Int, - presentationBgJobRecordsLimit: Int, - presentationBgJobRecurrenceDelay: Duration, - presentationBgJobProcessingParallelism: Int, - syncRevocationStatusesBgJobRecurrenceDelay: Duration, - syncRevocationStatusesBgJobProcessingParallelism: Int, + statusListSyncTriggerRecurrenceDelay: Duration, + didStateSyncTriggerRecurrenceDelay: Duration, presentationInvitationExpiry: Duration, issuanceInvitationExpiry: Duration, ) final case class ConnectConfig( database: DatabaseConfig, - connectBgJobRecordsLimit: Int, - connectBgJobRecurrenceDelay: Duration, - connectBgJobProcessingParallelism: Int, connectInvitationExpiry: Duration, ) @@ -173,7 +164,8 @@ final case class AgentConfig( verification: VerificationConfig, secretStorage: SecretStorageConfig, webhookPublisher: WebhookPublisherConfig, - defaultWallet: DefaultWalletConfig + defaultWallet: DefaultWalletConfig, + messagingService: MessagingServiceConfig ) { def validate: Either[String, Unit] = for { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala index 0d73369e82..44ffa1cea8 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/CustomServerInterceptors.scala @@ -1,9 +1,11 @@ package org.hyperledger.identus.agent.server.http +import org.http4s.{MediaType, Request, Response, Status} +import org.http4s.headers.`Content-Type` +import org.http4s.server.ServiceErrorHandler import org.hyperledger.identus.api.http.ErrorResponse import org.hyperledger.identus.shared.models.{Failure, StatusCode, UnmanagedFailureException} import org.log4s.* -import sttp.tapir.* import sttp.tapir.json.zio.jsonBody import sttp.tapir.server.interceptor.* import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, DefaultDecodeFailureHandler} @@ -11,6 +13,7 @@ import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler.F import sttp.tapir.server.interceptor.exception.ExceptionHandler import sttp.tapir.server.interceptor.reject.RejectHandler import sttp.tapir.server.model.ValuedEndpointOutput +import zio.{Task, ZIO} import scala.language.implicitConversions @@ -19,7 +22,7 @@ object CustomServerInterceptors { private val logger: Logger = getLogger private val endpointOutput = jsonBody[ErrorResponse] - private def defectHandler(response: ErrorResponse, maybeCause: Option[Throwable] = None) = { + private def tapirDefectHandler(response: ErrorResponse, maybeCause: Option[Throwable] = None) = { val statusCode = sttp.model.StatusCode(response.status) // Log defect as 'error' when status code matches a server error (5xx). Log other defects as 'debug'. (statusCode, maybeCause) match @@ -27,39 +30,43 @@ object CustomServerInterceptors { case (sc, None) if sc.isServerError => logger.error(endpointOutput.codec.encode(response)) case (_, Some(cause)) => logger.debug(cause)(endpointOutput.codec.encode(response)) case (_, None) => logger.debug(endpointOutput.codec.encode(response)) - Some(ValuedEndpointOutput(endpointOutput, response).prepend(sttp.tapir.statusCode, statusCode)) + ValuedEndpointOutput(endpointOutput, response).prepend(sttp.tapir.statusCode, statusCode) } - def exceptionHandler[F[_]]: ExceptionHandler[F] = ExceptionHandler.pure[F](ctx => + def tapirExceptionHandler[F[_]]: ExceptionHandler[F] = ExceptionHandler.pure[F](ctx => ctx.e match - case UnmanagedFailureException(failure: Failure) => defectHandler(failure) + case UnmanagedFailureException(failure: Failure) => Some(tapirDefectHandler(failure)) case e => - defectHandler( - ErrorResponse( - StatusCode.InternalServerError.code, - s"error:InternalServerError", - "Internal Server Error", - Some( - s"An unexpected error occurred when processing the request: " + - s"path=['${ctx.request.showShort}']" - ) - ), - Some(ctx.e) + Some( + tapirDefectHandler( + ErrorResponse( + StatusCode.InternalServerError.code, + s"error:InternalServerError", + "Internal Server Error", + Some( + s"An unexpected error occurred when processing the request: " + + s"path=['${ctx.request.showShort}']" + ) + ), + Some(ctx.e) + ) ) ) - def rejectHandler[F[_]]: RejectHandler[F] = RejectHandler.pure[F](resultFailure => - defectHandler( - ErrorResponse( - StatusCode.NotFound.code, - s"error:ResourcePathNotFound", - "Resource Path Not Found", - Some(s"The requested resource path doesn't exist.") + def tapirRejectHandler[F[_]]: RejectHandler[F] = RejectHandler.pure[F](resultFailure => + Some( + tapirDefectHandler( + ErrorResponse( + StatusCode.NotFound.code, + s"error:ResourcePathNotFound", + "Resource Path Not Found", + Some(s"The requested resource path doesn't exist.") + ) ) ) ) - def decodeFailureHandler: DecodeFailureHandler = (ctx: DecodeFailureContext) => { + def tapirDecodeFailureHandler: DecodeFailureHandler = (ctx: DecodeFailureContext) => { /** As per the Tapir Decode Failures documentation: * @@ -79,17 +86,39 @@ object CustomServerInterceptors { DefaultDecodeFailureHandler.respond(ctx) match case Some((sc, _)) => val details = FailureMessages.failureMessage(ctx) - defectHandler( - ErrorResponse( - sc.code, - s"error:RequestBodyDecodingFailure", - "Request Body Decoding Failure", - Some( - s"An error occurred when decoding the request body: " + - s"path=['${ctx.request.showShort}'], details=[$details]" + Some( + tapirDefectHandler( + ErrorResponse( + sc.code, + s"error:RequestBodyDecodingFailure", + "Request Body Decoding Failure", + Some( + s"An error occurred when decoding the request body: " + + s"path=['${ctx.request.showShort}'], details=[$details]" + ) ) ) ) case None => None } + + def http4sServiceErrorHandler: ServiceErrorHandler[Task] = (req: Request[Task]) => { case t: Throwable => + val res = tapirDefectHandler( + ErrorResponse( + StatusCode.InternalServerError.code, + s"error:InternalServerError", + "Internal Server Error", + Some( + s"An unexpected error occurred when servicing the request: " + + s"path=['${req.method.name} ${req.uri.copy(scheme = None, authority = None, fragment = None).toString}']" + ) + ), + Some(t) + ) + ZIO.succeed( + Response(Status.InternalServerError) + .withEntity(endpointOutput.codec.encode(res.value._2)) + .withContentType(`Content-Type`(MediaType.application.json)) + ) + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala index 05d56eb62b..1293185891 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala @@ -93,9 +93,9 @@ class ZHttp4sBlazeServer(micrometerRegistry: PrometheusMeterRegistry, metricsNam options <- ZIO.attempt { Http4sServerOptions .customiseInterceptors[Task] - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) .serverLog(None) .metricsInterceptor( srv.metricsInterceptor( @@ -123,6 +123,7 @@ class ZHttp4sBlazeServer(micrometerRegistry: PrometheusMeterRegistry, metricsNam ZIO.executor.flatMap(executor => BlazeServerBuilder[Task] .withExecutionContext(executor.asExecutionContext) + .withServiceErrorHandler(CustomServerInterceptors.http4sServiceErrorHandler) .bindHttp(port, "0.0.0.0") .withHttpApp(Router("/" -> http4sEndpoints).orNotFound) .serve diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala index 1708ca1517..67867bb2fb 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/BackgroundJobsHelper.scala @@ -19,7 +19,6 @@ import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation import org.hyperledger.identus.pollux.core.model.error.{CredentialServiceError, PresentationError} import org.hyperledger.identus.pollux.core.model.DidCommID import org.hyperledger.identus.pollux.core.service.CredentialService -import org.hyperledger.identus.pollux.sdjwt.SDJWT.* import org.hyperledger.identus.pollux.vc.jwt.{ DIDResolutionFailed, DIDResolutionSucceeded, @@ -29,8 +28,11 @@ import org.hyperledger.identus.pollux.vc.jwt.{ * } import org.hyperledger.identus.shared.crypto.* +import org.hyperledger.identus.shared.messaging.ConsumerJobConfig +import org.hyperledger.identus.shared.messaging.MessagingService.RetryStep import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext} -import zio.{ZIO, ZLayer} +import zio.{durationInt, Duration, ZIO, ZLayer} +import zio.prelude.OrdOps import java.time.Instant import java.util.Base64 @@ -229,4 +231,20 @@ trait BackgroundJobsHelper { case _ => ZIO.unit } } + + def retryStepsFromConfig(topicName: String, jobConfig: ConsumerJobConfig): Seq[RetryStep] = { + val retryTopics = jobConfig.retryStrategy match + case None => Seq.empty + case Some(rs) => + (1 to rs.maxRetries).map(i => + ( + s"$topicName-retry-$i", + rs.initialDelay.multipliedBy(Math.pow(2, i - 1).toLong).min(rs.maxDelay) + ) + ) + val topics = retryTopics prepended (topicName, 0.seconds) appended (s"$topicName-DLQ", Duration.Infinity) + (0 until topics.size - 1).map { i => + RetryStep(topics(i)._1, jobConfig.consumerCount, topics(i)._2, topics(i + 1)._1) + } + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala index 46335fd059..07cfd05a22 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/ConnectBackgroundJobs.scala @@ -2,49 +2,63 @@ package org.hyperledger.identus.agent.server.jobs import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.jobs.BackgroundJobError.ErrorResponseReceivedFromPeerAgent -import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError -import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError.{KeyNotFoundError, WalletNotFoundError} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage -import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.{ - InvalidStateForOperation, - RecordIdNotFound -} import org.hyperledger.identus.connect.core.model.ConnectionRecord import org.hyperledger.identus.connect.core.model.ConnectionRecord.* import org.hyperledger.identus.connect.core.service.ConnectionService import org.hyperledger.identus.mercury.* -import org.hyperledger.identus.mercury.model.error.SendMessageError import org.hyperledger.identus.resolvers.DIDResolver -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* import zio.metrics.* +import java.util.UUID + object ConnectBackgroundJobs extends BackgroundJobsHelper { - val didCommExchanges = { - for { - connectionService <- ZIO.service[ConnectionService] - config <- ZIO.service[AppConfig] - records <- connectionService - .findRecordsByStatesForAllWallets( - ignoreWithZeroRetries = true, - limit = config.connect.connectBgJobRecordsLimit, - ConnectionRecord.ProtocolState.ConnectionRequestPending, - ConnectionRecord.ProtocolState.ConnectionResponsePending - ) - _ <- ZIO.foreachPar(records)(performExchange).withParallelism(config.connect.connectBgJobProcessingParallelism) - } yield () - } + private val TOPIC_NAME = "connect" + + val connectFlowsHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + ConnectBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.connectFlow) + ) + } yield () - private def performExchange( - record: ConnectionRecord - ): URIO[ + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ DidOps & DIDResolver & HttpClient & ConnectionService & ManagedDIDService & DIDNonSecretStorage & AppConfig, Unit - ] = { + ] = + (for { + _ <- ZIO.logDebug(s"!!! Handling recordId: ${message.value} via Kafka queue") + connectionService <- ZIO.service[ConnectionService] + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + record <- connectionService + .findRecordById(message.value.recordId) + .provideSome(ZLayer.succeed(walletAccessContext)) + .someOrElseZIO(ZIO.dieMessage("Record Not Found")) + _ <- performExchange(record) + .tapSomeError { case (walletAccessContext, errorResponse) => + for { + connectService <- ZIO.service[ConnectionService] + _ <- connectService + .reportProcessingFailure(record.id, Some(errorResponse)) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + } yield () + } + .catchAll { e => ZIO.fail(RuntimeException(s"Attempt failed with: ${e}")) } + } yield ()) @@ Metric + .gauge("connection_flow_did_com_exchange_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + + private def performExchange(record: ConnectionRecord) = { import ProtocolState.* import Role.* @@ -179,26 +193,10 @@ object ConnectBackgroundJobs extends BackgroundJobsHelper { @@ Metric .gauge("connection_flow_inviter_process_connection_record_ms_gauge") .trackDurationWith(_.toMetricsSeconds) - case _ => ZIO.unit + case r => ZIO.logWarning(s"Invalid candidate record received for processing: $r") *> ZIO.unit } exchange - .tapError({ - case walletNotFound: WalletNotFoundError => - ZIO.logErrorCause( - s"Connect - Error processing record: ${record.id}", - Cause.fail(walletNotFound) - ) - case ((walletAccessContext, errorResponse)) => - for { - connectService <- ZIO.service[ConnectionService] - _ <- connectService - .reportProcessingFailure(record.id, Some(errorResponse)) - .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - } yield () - }) - .catchAll(e => ZIO.logErrorCause(s"Connect - Error processing record: ${record.id} ", Cause.fail(e))) - .catchAllDefect(d => ZIO.logErrorCause(s"Connect - Defect processing record: ${record.id}", Cause.fail(d))) } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala index 5d4ff494ea..2ee44e91bc 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala @@ -1,17 +1,54 @@ package org.hyperledger.identus.agent.server.jobs -import org.hyperledger.identus.agent.walletapi.model.error.GetManagedDIDError -import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.agent.server.config.AppConfig +import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, WalletManagementService} +import org.hyperledger.identus.shared.messaging.{Message, MessagingService, Producer} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletAdministrationContext, WalletId} +import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* +import zio.metrics.Metric -object DIDStateSyncBackgroundJobs { +object DIDStateSyncBackgroundJobs extends BackgroundJobsHelper { - val syncDIDPublicationStateFromDlt: ZIO[WalletAccessContext with ManagedDIDService, GetManagedDIDError, Unit] = - for { + private val TOPIC_NAME = "sync-did-state" + + val didStateSyncTrigger = { + (for { + config <- ZIO.service[AppConfig] + producer <- ZIO.service[Producer[WalletId, WalletId]] + trigger = for { + walletManagementService <- ZIO.service[WalletManagementService] + wallets <- walletManagementService.listWallets().map(_._1) + _ <- ZIO.logInfo(s"Triggering DID state sync for '${wallets.size}' wallets") + _ <- ZIO.foreach(wallets)(w => producer.produce(TOPIC_NAME, w.id, w.id)) + } yield () + _ <- trigger + .catchAll(e => ZIO.logError(s"error while syncing DID publication state: $e")) + .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) + .repeat(Schedule.spaced(config.pollux.didStateSyncTriggerRecurrenceDelay)) + } yield ()).debug.fork + } + + val didStateSyncHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + DIDStateSyncBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.didStateSync) + ) + } yield () + + private def handleMessage(message: Message[WalletId, WalletId]): RIO[ManagedDIDService, Unit] = { + val effect = for { managedDidService <- ZIO.service[ManagedDIDService] _ <- managedDidService.syncManagedDIDState _ <- managedDidService.syncUnconfirmedUpdateOperations } yield () - + effect + .provideSomeLayer(ZLayer.succeed(WalletAccessContext(message.value))) + .catchAll(t => ZIO.logErrorCause("Unable to syncing DID publication state", Cause.fail(t))) + @@ Metric + .gauge("did_publication_state_sync_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala index ffc617df3c..3cdde2853f 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala @@ -2,40 +2,61 @@ package org.hyperledger.identus.agent.server.jobs import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.jobs.BackgroundJobError.ErrorResponseReceivedFromPeerAgent -import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError.WalletNotFoundError -import org.hyperledger.identus.castor.core.model.did.* +import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService +import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage import org.hyperledger.identus.mercury.* -import org.hyperledger.identus.mercury.protocol.issuecredential.* import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError import org.hyperledger.identus.pollux.core.service.CredentialService -import org.hyperledger.identus.shared.models.Failure +import org.hyperledger.identus.resolvers.DIDResolver +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* import zio.metrics.* +import java.util.UUID + object IssueBackgroundJobs extends BackgroundJobsHelper { - val issueCredentialDidCommExchanges = { - for { + private val TOPIC_NAME = "issue" + + val issueFlowsHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + IssueBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.issueFlow) + ) + } yield () + + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ + HttpClient & DidOps & DIDResolver & (CredentialService & DIDNonSecretStorage & (ManagedDIDService & AppConfig)), + Unit + ] = { + (for { + _ <- ZIO.logDebug(s"!!! Handling recordId: ${message.value} via Kafka queue") credentialService <- ZIO.service[CredentialService] - config <- ZIO.service[AppConfig] - records <- credentialService - .getIssueCredentialRecordsByStatesForAllWallets( - ignoreWithZeroRetries = true, - limit = config.pollux.issueBgJobRecordsLimit, - IssueCredentialRecord.ProtocolState.OfferPending, - IssueCredentialRecord.ProtocolState.RequestPending, - IssueCredentialRecord.ProtocolState.RequestGenerated, - IssueCredentialRecord.ProtocolState.RequestReceived, - IssueCredentialRecord.ProtocolState.CredentialPending, - IssueCredentialRecord.ProtocolState.CredentialGenerated - ) - _ <- ZIO - .foreachPar(records)(performIssueCredentialExchange) - .withParallelism(config.pollux.issueBgJobProcessingParallelism) - } yield () + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + record <- credentialService + .findById(DidCommID(message.value.recordId.toString)) + .provideSome(ZLayer.succeed(walletAccessContext)) + .someOrElseZIO(ZIO.dieMessage(s"Record Not Found: ${message.value.recordId}")) + _ <- performIssueCredentialExchange(record) + .tapSomeError { case (walletAccessContext, errorResponse) => + for { + credentialService <- ZIO.service[CredentialService] + _ <- credentialService + .reportProcessingFailure(record.id, Some(errorResponse)) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + } yield () + } + .catchAll { e => ZIO.fail(RuntimeException(s"Attempt failed with: ${e}")) } + } yield ()) @@ Metric + .gauge("issuance_flow_did_com_exchange_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) } private def counterMetric(key: String) = Metric @@ -136,7 +157,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { "issuance_flow_issuer_send_credential_msg_succeed_counter" ) - val aux = for { + val exchange = for { _ <- ZIO.logDebug(s"Running action with records => $record") _ <- record match { // Offer should be sent from Issuer to Holder @@ -227,8 +248,8 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { val holderPendingToGeneratedFlow = for { walletAccessContext <- ZIO .fromOption(offer.to) + .mapError(_ => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) .flatMap(buildWalletAccessContextLayer) - .mapError(e => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) result <- for { credentialService <- ZIO.service[CredentialService] _ <- credentialService @@ -273,8 +294,8 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { val holderPendingToGeneratedFlow = for { walletAccessContext <- ZIO .fromOption(offer.to) + .mapError(_ => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) .flatMap(buildWalletAccessContextLayer) - .mapError(e => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) result <- for { credentialService <- ZIO.service[CredentialService] _ <- credentialService @@ -319,8 +340,8 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { val holderPendingToGeneratedFlow = for { walletAccessContext <- ZIO .fromOption(offer.to) + .mapError(_ => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) .flatMap(buildWalletAccessContextLayer) - .mapError(e => CredentialServiceError.CredentialOfferMissingField(id.value, "recipient")) result <- for { credentialService <- ZIO.service[CredentialService] @@ -629,33 +650,12 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { @@ IssuerSendCredentialAll @@ Metric .gauge("issuance_flow_issuer_send_cred_flow_ms_gauge") .trackDurationWith(_.toMetricsSeconds) - - case record: IssueCredentialRecord => - ZIO.logDebug(s"IssuanceRecord: ${record.id} - ${record.protocolState}") *> ZIO.unit + case r: IssueCredentialRecord => + ZIO.logWarning(s"Invalid candidate record received for processing: $r") *> ZIO.unit } } yield () - aux - .tapError( - { - case walletNotFound: WalletNotFoundError => ZIO.unit - case CredentialServiceError.RecordNotFound(_, _) => ZIO.unit - case CredentialServiceError.UnsupportedDidFormat(_) => ZIO.unit - case failure: Failure => ZIO.unit - case ((walletAccessContext, failure)) => - for { - credentialService <- ZIO.service[CredentialService] - _ <- credentialService - .reportProcessingFailure(record.id, Some(failure)) - .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - } yield () - } - ) - .catchAll(e => ZIO.logErrorCause(s"Issue Credential - Error processing record: ${record.id} ", Cause.fail(e))) - .catchAllDefect(d => - ZIO.logErrorCause(s"Issue Credential - Defect processing record: ${record.id}", Cause.fail(d)) - ) - + exchange } } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala index 9938b6b50b..4bfb247176 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala @@ -30,17 +30,18 @@ import org.hyperledger.identus.pollux.sdjwt.{HolderPrivateKey, IssuerPublicKey, import org.hyperledger.identus.pollux.vc.jwt.{DidResolver as JwtDidResolver, Issuer as JwtIssuer, JWT, JwtPresentation} import org.hyperledger.identus.resolvers.DIDResolver import org.hyperledger.identus.shared.http.* -import org.hyperledger.identus.shared.models.* +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{Failure, *} import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* -import zio.json.* -import zio.json.ast.Json import zio.metrics.* import zio.prelude.Validation import zio.prelude.ZValidation.{Failure as ZFailure, *} -import java.time.{Clock, Instant, ZoneId} +import java.time.{Instant, ZoneId} +import java.util.UUID object PresentBackgroundJobs extends BackgroundJobsHelper { @@ -55,47 +56,50 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { private type MESSAGING_RESOURCES = DidOps & DIDResolver & HttpClient - val presentProofExchanges: ZIO[RESOURCES, Throwable, Unit] = { - for { + private val TOPIC_NAME = "present" + + val presentFlowsHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + PresentBackgroundJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.presentFlow) + ) + } yield () + + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ + RESOURCES, + Unit + ] = { + (for { + _ <- ZIO.logDebug(s"!!! Present Proof Handling recordId: ${message.value} via Kafka queue") presentationService <- ZIO.service[PresentationService] - config <- ZIO.service[AppConfig] - records <- presentationService - .getPresentationRecordsByStatesForAllWallets( - ignoreWithZeroRetries = true, - limit = config.pollux.presentationBgJobRecordsLimit, - PresentationRecord.ProtocolState.RequestPending, - PresentationRecord.ProtocolState.PresentationPending, - PresentationRecord.ProtocolState.PresentationGenerated, - PresentationRecord.ProtocolState.PresentationReceived - ) - .mapError(err => Throwable(s"Error occurred while getting Presentation records: $err")) - _ <- ZIO.logInfo(s"Processing ${records.size} Presentation records") - _ <- ZIO - .foreachPar(records)(performPresentProofExchange) - .withParallelism(config.pollux.presentationBgJobProcessingParallelism) - } yield () + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + record <- presentationService + .findPresentationRecord(DidCommID(message.value.recordId.toString)) + .provideSome(ZLayer.succeed(walletAccessContext)) + .someOrElseZIO(ZIO.dieMessage("Record Not Found")) + _ <- performPresentProofExchange(record) + .tapSomeError { case f: Failure => + for { + presentationService <- ZIO.service[PresentationService] + _ <- presentationService + .reportProcessingFailure(record.id, Some(f)) + } yield () + } + .catchAll { e => ZIO.fail(RuntimeException(s"Attempt failed with: ${e}")) } + } yield ()) @@ Metric + .gauge("present_proof_flow_did_com_exchange_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) } private def counterMetric(key: String) = Metric .counterInt(key) .fromConst(1) - private def performPresentProofExchange(record: PresentationRecord): URIO[RESOURCES, Unit] = - aux(record) - .catchAll { - case ex: Failure => - ZIO - .service[PresentationService] - .flatMap(_.reportProcessingFailure(record.id, Some(ex))) - case ex => ZIO.logErrorCause(s"PresentBackgroundJobs - Error processing record: ${record.id}", Cause.fail(ex)) - } - .catchAllDefect(d => - ZIO.logErrorCause(s"PresentBackgroundJobs - Defect processing record: ${record.id}", Cause.fail(d)) - ) - - private def aux(record: PresentationRecord): ZIO[RESOURCES, ERROR, Unit] = { + private def performPresentProofExchange(record: PresentationRecord): ZIO[RESOURCES, ERROR, Unit] = { import org.hyperledger.identus.pollux.core.model.PresentationRecord.ProtocolState.* - for { + val exchange = for { _ <- ZIO.logDebug(s"Running action with records => $record") _ <- record match { case PresentationRecord( @@ -604,6 +608,8 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { ZIO.logWarning(s"Unhandled PresentationRecord state: ${record.protocolState}") } } yield () + + exchange } object Prover { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala index 71d02db3e2..1fe5d77551 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/StatusListJobs.scala @@ -1,131 +1,181 @@ package org.hyperledger.identus.agent.server.jobs import org.hyperledger.identus.agent.server.config.AppConfig +import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.castor.core.model.did.VerificationRelationship +import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.mercury.* import org.hyperledger.identus.mercury.protocol.revocationnotificaiton.RevocationNotification +import org.hyperledger.identus.pollux.core.model.{CredInStatusList, CredentialStatusListWithCreds} import org.hyperledger.identus.pollux.core.service.{CredentialService, CredentialStatusListService} -import org.hyperledger.identus.pollux.vc.jwt.revocation.{VCStatusList2021, VCStatusList2021Error} -import org.hyperledger.identus.shared.models.* +import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021, VCStatusList2021Error} +import org.hyperledger.identus.resolvers.DIDResolver +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, Producer, WalletIdAndRecordId} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds import zio.* import zio.metrics.Metric +import java.util.UUID + object StatusListJobs extends BackgroundJobsHelper { - val syncRevocationStatuses = - for { - credentialStatusListService <- ZIO.service[CredentialStatusListService] - credentialService <- ZIO.service[CredentialService] - credentialStatusListsWithCreds <- credentialStatusListService.getCredentialsAndItsStatuses - @@ Metric - .gauge("revocation_status_list_sync_get_status_lists_w_creds_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) + private val TOPIC_NAME = "sync-status-list" - updatedVcStatusListsCredsEffects = credentialStatusListsWithCreds.map { statusListWithCreds => - val vcStatusListCredString = statusListWithCreds.statusListCredential - val walletAccessContext = WalletAccessContext(statusListWithCreds.walletId) + val statusListsSyncTrigger = { + (for { + config <- ZIO.service[AppConfig] + producer <- ZIO.service[Producer[UUID, WalletIdAndRecordId]] + trigger = for { + credentialStatusListService <- ZIO.service[CredentialStatusListService] + walletAndStatusListIds <- credentialStatusListService.getCredentialStatusListIds + _ <- ZIO.logInfo(s"Triggering status list revocation sync for '${walletAndStatusListIds.size}' status lists") + _ <- ZIO.foreach(walletAndStatusListIds) { (walletId, statusListId) => + producer.produce(TOPIC_NAME, walletId.toUUID, WalletIdAndRecordId(walletId.toUUID, statusListId)) + } + } yield () + _ <- trigger.repeat(Schedule.spaced(config.pollux.statusListSyncTriggerRecurrenceDelay)) + } yield ()).debug.fork + } - val effect = for { - vcStatusListCredJson <- ZIO - .fromEither(io.circe.parser.parse(vcStatusListCredString)) - .mapError(_.underlying) - issuer <- createJwtVcIssuer( - statusListWithCreds.issuer, - VerificationRelationship.AssertionMethod, - None - ) - vcStatusListCred <- VCStatusList2021 - .decodeFromJson(vcStatusListCredJson, issuer) - .mapError(x => new Throwable(x.msg)) - bitString <- vcStatusListCred.getBitString.mapError(x => new Throwable(x.msg)) - updateBitStringEffects = statusListWithCreds.credentials.map { cred => - if cred.isCanceled then { - val sendMessageEffect = for { - maybeIssueCredentialRecord <- credentialService.findById(cred.issueCredentialRecordId) - issueCredentialRecord <- ZIO - .fromOption(maybeIssueCredentialRecord) - .mapError(_ => - new Throwable(s"Issue credential record not found by id: ${cred.issueCredentialRecordId}") - ) - issueCredentialData <- ZIO - .fromOption(issueCredentialRecord.issueCredentialData) - .mapError(_ => - new Throwable( - s"Issue credential data not found in issue credential record by id: ${cred.issueCredentialRecordId}" - ) - ) - issueCredentialProtocolThreadId <- ZIO - .fromOption(issueCredentialData.thid) - .mapError(_ => new Throwable("thid not found in issue credential data")) - revocationNotification = RevocationNotification.build( - issueCredentialData.from, - issueCredentialData.to, - issueCredentialProtocolThreadId = issueCredentialProtocolThreadId - ) - didCommAgent <- buildDIDCommAgent(issueCredentialData.from) - response <- MessagingService - .send(revocationNotification.makeMessage) - .provideSomeLayer(didCommAgent) @@ Metric - .gauge("revocation_status_list_sync_revocation_notification_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) - } yield response + val statusListSyncHandler = for { + appConfig <- ZIO.service[AppConfig] + _ <- messaging.MessagingService.consumeWithRetryStrategy( + "identus-cloud-agent", + StatusListJobs.handleMessage, + retryStepsFromConfig(TOPIC_NAME, appConfig.agent.messagingService.statusListSync) + ) + } yield () - val updateBitStringEffect = bitString.setRevokedInPlace(cred.statusListIndex, true) + private def handleMessage(message: Message[UUID, WalletIdAndRecordId]): RIO[ + DIDService & ManagedDIDService & CredentialService & DidOps & DIDResolver & HttpClient & + CredentialStatusListService, + Unit + ] = { + (for { + _ <- ZIO.logDebug(s"!!! Handling recordId: ${message.value} via Kafka queue") + credentialStatusListService <- ZIO.service[CredentialStatusListService] + walletAccessContext = WalletAccessContext(WalletId.fromUUID(message.value.walletId)) + statusListWithCreds <- credentialStatusListService + .getCredentialStatusListWithCreds(message.value.recordId) + .provideSome(ZLayer.succeed(walletAccessContext)) + _ <- updateStatusList(statusListWithCreds) + } yield ()) @@ Metric + .gauge("revocation_status_list_sync_job_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } - val updateAndNotify = for { - updated <- updateBitStringEffect.mapError(x => new Throwable(x.message)) - _ <- - if !cred.isProcessed then - sendMessageEffect.flatMap { resp => - if (resp.status >= 200 && resp.status < 300) - ZIO.logInfo("successfully sent revocation notification message") - else ZIO.logError(s"failed to send revocation notification message") - } - else ZIO.unit - } yield updated - updateAndNotify.provideSomeLayer(ZLayer.succeed(walletAccessContext)) @@ Metric - .gauge("revocation_status_list_sync_process_single_credential_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) - } else ZIO.unit - } - _ <- ZIO - .collectAll(updateBitStringEffects) + private def updateStatusList(statusListWithCreds: CredentialStatusListWithCreds) = { + for { + credentialStatusListService <- ZIO.service[CredentialStatusListService] + vcStatusListCredString = statusListWithCreds.statusListCredential + walletAccessContext = WalletAccessContext(statusListWithCreds.walletId) + effect = for { + vcStatusListCredJson <- ZIO + .fromEither(io.circe.parser.parse(vcStatusListCredString)) + .mapError(_.underlying) + issuer <- createJwtVcIssuer(statusListWithCreds.issuer, VerificationRelationship.AssertionMethod, None) + vcStatusListCred <- VCStatusList2021 + .decodeFromJson(vcStatusListCredJson, issuer) + .mapError(x => new Throwable(x.msg)) + bitString <- vcStatusListCred.getBitString.mapError(x => new Throwable(x.msg)) + _ <- ZIO.collectAll( + statusListWithCreds.credentials.map(c => + updateBitStringForCredentialAndNotify(bitString, c, walletAccessContext) + ) + ) + unprocessedEntityIds = statusListWithCreds.credentials.collect { + case x if !x.isProcessed && x.isCanceled => x.id + } + _ <- credentialStatusListService + .markAsProcessedMany(unprocessedEntityIds) + @@ Metric + .gauge("revocation_status_list_sync_mark_as_processed_many_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) - unprocessedEntityIds = statusListWithCreds.credentials.collect { - case x if !x.isProcessed && x.isCanceled => x.id - } - _ <- credentialStatusListService - .markAsProcessedMany(unprocessedEntityIds) - @@ Metric - .gauge("revocation_status_list_sync_mark_as_processed_many_ms_gauge") - .trackDurationWith(_.toMetricsSeconds) + updatedVcStatusListCred <- vcStatusListCred.updateBitString(bitString).mapError { + case VCStatusList2021Error.EncodingError(msg: String) => new Throwable(msg) + case VCStatusList2021Error.DecodingError(msg: String) => new Throwable(msg) + } + vcStatusListCredJsonString <- updatedVcStatusListCred.toJsonWithEmbeddedProof.map(_.spaces2) + _ <- credentialStatusListService.updateStatusListCredential( + statusListWithCreds.id, + vcStatusListCredJsonString + ) + } yield () + _ <- effect + .catchAll(e => + ZIO.logErrorCause(s"Error processing status list record: ${statusListWithCreds.id} ", Cause.fail(e)) + ) + .catchAllDefect(d => + ZIO.logErrorCause(s"Defect processing status list record: ${statusListWithCreds.id}", Cause.fail(d)) + ) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + } yield () + } - updatedVcStatusListCred <- vcStatusListCred.updateBitString(bitString).mapError { - case VCStatusList2021Error.EncodingError(msg: String) => new Throwable(msg) - case VCStatusList2021Error.DecodingError(msg: String) => new Throwable(msg) - } - vcStatusListCredJsonString <- updatedVcStatusListCred.toJsonWithEmbeddedProof - .map(_.spaces2) - _ <- credentialStatusListService - .updateStatusListCredential(statusListWithCreds.id, vcStatusListCredJsonString) - } yield () + private def updateBitStringForCredentialAndNotify( + bitString: BitString, + credInStatusList: CredInStatusList, + walletAccessContext: WalletAccessContext + ) = { + for { + credentialService <- ZIO.service[CredentialService] + _ <- + if credInStatusList.isCanceled then { + val updateBitStringEffect = bitString.setRevokedInPlace(credInStatusList.statusListIndex, true) + val notifyEffect = sendRevocationNotificationMessage(credInStatusList) + val updateAndNotify = for { + updated <- updateBitStringEffect.mapError(x => new Throwable(x.message)) + _ <- + if !credInStatusList.isProcessed then + notifyEffect.flatMap { resp => + if (resp.status >= 200 && resp.status < 300) + ZIO.logInfo("successfully sent revocation notification message") + else ZIO.logError(s"failed to send revocation notification message") + } + else ZIO.unit + } yield updated + updateAndNotify.provideSomeLayer(ZLayer.succeed(walletAccessContext)) @@ Metric + .gauge("revocation_status_list_sync_process_single_credential_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } else ZIO.unit + } yield () + } - effect - .catchAll(e => - ZIO.logErrorCause(s"Error processing status list record: ${statusListWithCreds.id} ", Cause.fail(e)) - ) - .catchAllDefect(d => - ZIO.logErrorCause(s"Defect processing status list record: ${statusListWithCreds.id}", Cause.fail(d)) + private def sendRevocationNotificationMessage( + credInStatusList: CredInStatusList + ) = { + for { + credentialService <- ZIO.service[CredentialService] + maybeIssueCredentialRecord <- credentialService.findById(credInStatusList.issueCredentialRecordId) + issueCredentialRecord <- ZIO + .fromOption(maybeIssueCredentialRecord) + .mapError(_ => + new Throwable(s"Issue credential record not found by id: ${credInStatusList.issueCredentialRecordId}") + ) + issueCredentialData <- ZIO + .fromOption(issueCredentialRecord.issueCredentialData) + .mapError(_ => + new Throwable( + s"Issue credential data not found in issue credential record by id: ${credInStatusList.issueCredentialRecordId}" ) - .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - - } - config <- ZIO.service[AppConfig] - _ <- (ZIO - .collectAll(updatedVcStatusListsCredsEffects) @@ Metric - .gauge("revocation_status_list_sync_process_status_lists_w_creds_ms_gauge") - .trackDurationWith(_.toMetricsSeconds)) - .withParallelism(config.pollux.syncRevocationStatusesBgJobProcessingParallelism) - } yield () + ) + issueCredentialProtocolThreadId <- ZIO + .fromOption(issueCredentialData.thid) + .mapError(_ => new Throwable("thid not found in issue credential data")) + revocationNotification = RevocationNotification.build( + issueCredentialData.from, + issueCredentialData.to, + issueCredentialProtocolThreadId = issueCredentialProtocolThreadId + ) + didCommAgent <- buildDIDCommAgent(issueCredentialData.from) + response <- MessagingService + .send(revocationNotification.makeMessage) + .provideSomeLayer(didCommAgent) @@ Metric + .gauge("revocation_status_list_sync_revocation_notification_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + } yield response + } } diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala index c7d1bd7a8f..4cb227e8ff 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/issue/controller/IssueControllerTestTools.scala @@ -69,9 +69,9 @@ trait IssueControllerTestTools extends PostgresTestContainerSupport { def bootstrapOptions[F[_]](monadError: MonadError[F]): CustomiseInterceptors[F, Any] = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend(controller: IssueController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala index 6facffe5ed..fb4ce6f7ab 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala @@ -12,16 +12,15 @@ import org.hyperledger.identus.oid4vci.storage.InMemoryIssuanceSessionService import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialConfiguration import org.hyperledger.identus.pollux.core.model.CredentialFormat import org.hyperledger.identus.pollux.core.repository.{ - CredentialRepository, CredentialRepositoryInMemory, CredentialStatusListRepositoryInMemory } import org.hyperledger.identus.pollux.core.service.* import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver import org.hyperledger.identus.pollux.vc.jwt.PrismDidResolver +import org.hyperledger.identus.shared.messaging.{MessagingService, MessagingServiceConfig, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.{Clock, Random, URLayer, ZIO, ZLayer} -import zio.json.* import zio.json.ast.Json import zio.mock.MockSpecDefault import zio.test.* @@ -54,6 +53,8 @@ object OIDCCredentialIssuerServiceSpec GenericSecretStorageInMemory.layer, LinkSecretServiceImpl.layer, CredentialServiceImpl.layer, + (MessagingServiceConfig.inMemoryLayer >>> MessagingService.serviceLayer >>> + MessagingService.producerLayer[UUID, WalletIdAndRecordId]).orDie, OIDCCredentialIssuerServiceImpl.layer ) diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala index 19636104a5..55b93f3007 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/credentialdefinition/CredentialDefinitionTestTools.scala @@ -97,9 +97,9 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { def bootstrapOptions[F[_]](monadError: MonadError[F]) = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend( diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala index 5917f13f1c..7f72581960 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/pollux/schema/CredentialSchemaTestTools.scala @@ -96,9 +96,9 @@ trait CredentialSchemaTestTools extends PostgresTestContainerSupport { def bootstrapOptions[F[_]](monadError: MonadError[F]) = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend( diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala index 95ed827fec..80f15ce237 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/system/controller/SystemControllerTestTools.scala @@ -41,9 +41,9 @@ trait SystemControllerTestTools { def bootstrapOptions[F[_]](monadError: MonadError[F]): CustomiseInterceptors[F, Any] = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend(controller: SystemController) = { diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala index d3ca097e3d..e4da96b640 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerTestTools.scala @@ -70,9 +70,9 @@ trait VcVerificationControllerTestTools extends PostgresTestContainerSupport { def bootstrapOptions[F[_]](monadError: MonadError[F]): CustomiseInterceptors[F, Any] = { new CustomiseInterceptors[F, Any](_ => ()) - .exceptionHandler(CustomServerInterceptors.exceptionHandler) - .rejectHandler(CustomServerInterceptors.rejectHandler) - .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + .exceptionHandler(CustomServerInterceptors.tapirExceptionHandler) + .rejectHandler(CustomServerInterceptors.tapirRejectHandler) + .decodeFailureHandler(CustomServerInterceptors.tapirDecodeFailureHandler) } def httpBackend(controller: VcVerificationController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { diff --git a/cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql b/cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql new file mode 100644 index 0000000000..5a59c4a121 --- /dev/null +++ b/cloud-agent/service/wallet-api/src/main/resources/sql/agent/V15__add_did_index_table.sql @@ -0,0 +1,19 @@ +-- Last used DID Index per wallet (solving race condition) +CREATE TABLE public.last_did_index_per_wallet +( + "wallet_id" UUID REFERENCES public.wallet ("wallet_id") NOT NULL PRIMARY KEY, + "last_used_index" INT NOT NULL +); + +ALTER TABLE public.last_did_index_per_wallet + ENABLE ROW LEVEL SECURITY; + +CREATE +POLICY last_did_index_per_wallet_wallet_isolation +ON public.last_did_index_per_wallet +USING (wallet_id = current_setting('app.current_wallet_id')::UUID); + +INSERT INTO public.last_did_index_per_wallet(wallet_id, last_used_index) +SELECT wallet_id, MAX(did_index) +FROM public.prism_did_wallet_state +GROUP BY wallet_id; \ No newline at end of file diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala index 861f3a8b10..37e8543b94 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceImpl.scala @@ -1,8 +1,7 @@ package org.hyperledger.identus.agent.walletapi.service import org.hyperledger.identus.agent.walletapi.model.* -import org.hyperledger.identus.agent.walletapi.model.error.* -import org.hyperledger.identus.agent.walletapi.model.error.given +import org.hyperledger.identus.agent.walletapi.model.error.{*, given} import org.hyperledger.identus.agent.walletapi.service.handler.{DIDCreateHandler, DIDUpdateHandler, PublicationHandler} import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService.DEFAULT_MASTER_KEY_ID import org.hyperledger.identus.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage, WalletSecretStorage} @@ -32,7 +31,6 @@ class ManagedDIDServiceImpl private[walletapi] ( override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, walletSecretStorage: WalletSecretStorage, apollo: Apollo, - createDIDSem: Semaphore ) extends ManagedDIDService { private val AGREEMENT_KEY_ID = KeyId("agreement") @@ -127,7 +125,7 @@ class ManagedDIDServiceImpl private[walletapi] ( def createAndStoreDID( didTemplate: ManagedDIDTemplate ): ZIO[WalletAccessContext, CreateManagedDIDError, LongFormPrismDID] = { - val effect = for { + for { _ <- ZIO .fromEither(ManagedDIDTemplateValidator.validate(didTemplate, defaultDidDocumentServices)) .mapError { x => @@ -144,15 +142,6 @@ class ManagedDIDServiceImpl private[walletapi] ( .mapError(CreateManagedDIDError.InvalidOperation.apply) _ <- material.persist.mapError(CreateManagedDIDError.WalletStorageError.apply) } yield PrismDID.buildLongFormFromOperation(material.operation) - - // This synchronizes createDID effect to only allow 1 execution at a time - // to avoid concurrent didIndex update. Long-term solution should be - // solved at the DB level. - // - // Performance may be improved by not synchronizing the whole operation, - // but only the counter increment part allowing multiple in-flight create operations - // once didIndex is acquired. - createDIDSem.withPermit(effect) } def updateManagedDID( @@ -385,7 +374,6 @@ object ManagedDIDServiceImpl { nonSecretStorage <- ZIO.service[DIDNonSecretStorage] walletSecretStorage <- ZIO.service[WalletSecretStorage] apollo <- ZIO.service[Apollo] - createDIDSem <- Semaphore.make(1) } yield ManagedDIDServiceImpl( defaultDidDocumentServices, didService, @@ -393,8 +381,7 @@ object ManagedDIDServiceImpl { secretStorage, nonSecretStorage, walletSecretStorage, - apollo, - createDIDSem + apollo ) } } diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala index 795e6d6199..02b5142152 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceWithEventNotificationImpl.scala @@ -20,7 +20,6 @@ class ManagedDIDServiceWithEventNotificationImpl( override private[walletapi] val nonSecretStorage: DIDNonSecretStorage, walletSecretStorage: WalletSecretStorage, apollo: Apollo, - createDIDSem: Semaphore, eventNotificationService: EventNotificationService ) extends ManagedDIDServiceImpl( defaultDidDocumentServices, @@ -29,8 +28,7 @@ class ManagedDIDServiceWithEventNotificationImpl( secretStorage, nonSecretStorage, walletSecretStorage, - apollo, - createDIDSem + apollo ) { private val didStatusUpdatedEventName = "DIDStatusUpdated" @@ -81,7 +79,6 @@ object ManagedDIDServiceWithEventNotificationImpl { nonSecretStorage, walletSecretStorage, apollo, - createDIDSem, eventNotificationService ) } diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala index 66fec256bb..d87ef1c91d 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/handler/DIDCreateHandler.scala @@ -31,12 +31,7 @@ private[walletapi] class DIDCreateHandler( walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) seed <- walletSecretStorage.findWalletSeed .someOrElseZIO(ZIO.dieMessage(s"Wallet seed for wallet $walletId does not exist")) - didIndex <- nonSecretStorage - .getMaxDIDIndex() - .mapBoth( - CreateManagedDIDError.WalletStorageError.apply, - maybeIdx => maybeIdx.map(_ + 1).getOrElse(0) - ) + didIndex <- nonSecretStorage.incrementAndGetNextDIDIndex generated <- operationFactory.makeCreateOperation(masterKeyId, seed.toByteArray)(didIndex, didTemplate) (createOperation, keys) = generated state = ManagedDIDState(createOperation, didIndex, PublicationState.Created()) diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala index bfdca44f73..7f86258e80 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala @@ -1,27 +1,21 @@ package org.hyperledger.identus.agent.walletapi.sql +import cats.implicits.toFunctorOps import doobie.* import doobie.implicits.* import doobie.postgres.implicits.* import org.hyperledger.identus.agent.walletapi.model.* import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage -import org.hyperledger.identus.castor.core.model.did.{ - EllipticCurve, - InternalKeyPurpose, - PrismDID, - ScheduledDIDOperationStatus, - VerificationRelationship -} +import org.hyperledger.identus.castor.core.model.did.* import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.shared.db.ContextAwareTask -import org.hyperledger.identus.shared.db.Implicits.* -import org.hyperledger.identus.shared.db.Implicits.given +import org.hyperledger.identus.shared.db.Implicits.{*, given} import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext, WalletId} import zio.* import zio.interop.catz.* import java.time.Instant -import scala.collection.immutable.ArraySeq +import java.util.Objects class JdbcDIDNonSecretStorage(xa: Transactor[ContextAwareTask], xb: Transactor[Task]) extends DIDNonSecretStorage { @@ -109,11 +103,11 @@ class JdbcDIDNonSecretStorage(xa: Transactor[ContextAwareTask], xb: Transactor[T _ <- insertHdKeyIO.updateMany(randKeyValues(now)) } yield () - for { + (for { walletCtx <- ZIO.service[WalletAccessContext] now <- Clock.instant _ <- txnIO(now, walletCtx.walletId).transactWallet(xa) - } yield () + } yield ()).orDie } override def updateManagedDID(did: PrismDID, patch: ManagedDIDStatePatch): RIO[WalletAccessContext, Unit] = { @@ -151,6 +145,41 @@ class JdbcDIDNonSecretStorage(xa: Transactor[ContextAwareTask], xb: Transactor[T cxnIO.transactWallet(xa).map(_.flatten) } + override def incrementAndGetNextDIDIndex: URIO[WalletAccessContext, Int] = { + def acquireAdvisoryLock(walletId: WalletId): ConnectionIO[Unit] = { + // Should be specific to this process + val PROCESS_UNIQUE_ID = 465263 + val hashCode = Objects.hash(walletId.hashCode(), PROCESS_UNIQUE_ID) + sql"SELECT pg_advisory_xact_lock($hashCode)".query[Unit].unique.void + } + + def insertWalletDIDIndexIfNotExists(walletId: WalletId): ConnectionIO[Int] = { + sql""" + | INSERT INTO public.last_did_index_per_wallet (wallet_id, last_used_index) + | VALUES ($walletId, -1) + | ON CONFLICT (wallet_id) DO NOTHING""".stripMargin.update.run + } + + def incrementWalletDIDIndex(walletId: WalletId): ConnectionIO[Int] = { + sql""" + | UPDATE public.last_did_index_per_wallet + | SET last_used_index = last_used_index + 1 + | WHERE wallet_id = $walletId + | RETURNING last_used_index""".stripMargin.query[Int].unique + } + + for { + walletCtx <- ZIO.service[WalletAccessContext] + walletId = walletCtx.walletId + cnxIO = for { + _ <- acquireAdvisoryLock(walletId) + _ <- insertWalletDIDIndexIfNotExists(walletId) + index <- incrementWalletDIDIndex(walletId) + } yield index + index <- cnxIO.transactWallet(xa).orDie + } yield index + } + override def getHdKeyCounter(did: PrismDID): RIO[WalletAccessContext, Option[HdKeyIndexCounter]] = { val status: ScheduledDIDOperationStatus = ScheduledDIDOperationStatus.Confirmed val cxnIO = diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala index 1830dc1600..612338b1ad 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/storage/DIDNonSecretStorage.scala @@ -21,6 +21,8 @@ trait DIDNonSecretStorage { def getMaxDIDIndex(): RIO[WalletAccessContext, Option[Int]] + def incrementAndGetNextDIDIndex: URIO[WalletAccessContext, Int] + def getHdKeyCounter(did: PrismDID): RIO[WalletAccessContext, Option[HdKeyIndexCounter]] /** Return a tuple of key metadata and the operation hash */ diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala index f14df4d00d..6a29b2769d 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/MockDIDNonSecretStorage.scala @@ -49,6 +49,9 @@ case class MockDIDNonSecretStorage(proxy: Proxy) extends DIDNonSecretStorage { override def getMaxDIDIndex(): RIO[WalletAccessContext, Option[Int]] = proxy(MockDIDNonSecretStorage.GetMaxDIDIndex) + override def incrementAndGetNextDIDIndex: URIO[WalletAccessContext, RuntimeFlags] = + proxy(MockDIDNonSecretStorage.IncrementAndGetNextDIDIndex) + override def getHdKeyCounter(did: PrismDID): RIO[WalletAccessContext, Option[HdKeyIndexCounter]] = proxy(MockDIDNonSecretStorage.GetHdKeyCounter, did) @@ -89,6 +92,7 @@ object MockDIDNonSecretStorage extends Mock[DIDNonSecretStorage] { ] object UpdateManagedDID extends Effect[(PrismDID, ManagedDIDStatePatch), Throwable, Unit] object GetMaxDIDIndex extends Effect[Unit, Throwable, Option[Int]] + object IncrementAndGetNextDIDIndex extends Effect[Unit, Nothing, Int] object GetHdKeyCounter extends Effect[PrismDID, Throwable, Option[HdKeyIndexCounter]] object GetKeyMeta extends Effect[(PrismDID, KeyId), Throwable, Option[(ManagedDIDKeyMeta, Array[Byte])]] object InsertHdKeyMeta extends Effect[(PrismDID, KeyId, ManagedDIDKeyMeta, Array[Byte]), Throwable, Unit] diff --git a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala new file mode 100644 index 0000000000..687f5e9aa1 --- /dev/null +++ b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/WalletIdAndRecordId.scala @@ -0,0 +1,19 @@ +//package org.hyperledger.identus.connect.core.model +// +//import org.hyperledger.identus.messaging.Serde +//import zio.json.{DecoderOps, DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} +// +//import java.nio.charset.StandardCharsets +//import java.util.UUID +// +//case class WalletIdAndRecordId(walletId: UUID, recordId: UUID) +// +//object WalletIdAndRecordId { +// given encoder: JsonEncoder[WalletIdAndRecordId] = DeriveJsonEncoder.gen[WalletIdAndRecordId] +// given decoder: JsonDecoder[WalletIdAndRecordId] = DeriveJsonDecoder.gen[WalletIdAndRecordId] +// given ser: Serde[WalletIdAndRecordId] = new Serde[WalletIdAndRecordId] { +// override def serialize(t: WalletIdAndRecordId): Array[Byte] = t.toJson.getBytes(StandardCharsets.UTF_8) +// override def deserialize(ba: Array[Byte]): WalletIdAndRecordId = +// new String(ba, StandardCharsets.UTF_8).fromJson[WalletIdAndRecordId].getOrElse(throw RuntimeException("")) +// } +//} diff --git a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala index a4072aea05..db91377abc 100644 --- a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala +++ b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImpl.scala @@ -1,14 +1,13 @@ package org.hyperledger.identus.connect.core.service -import org.hyperledger.identus.* import org.hyperledger.identus.connect.core.model.{ConnectionRecord, ConnectionRecordBeforeStored} -import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.* import org.hyperledger.identus.connect.core.model.ConnectionRecord.* import org.hyperledger.identus.connect.core.repository.ConnectionRepository import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.mercury.protocol.connection.* import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation +import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils @@ -21,9 +20,12 @@ import java.util.UUID private class ConnectionServiceImpl( connectionRepository: ConnectionRepository, + messageProducer: Producer[UUID, WalletIdAndRecordId], maxRetries: Int = 5, // TODO move to config ) extends ConnectionService { + private val TOPIC_NAME = "connect" + override def createConnectionInvitation( label: Option[String], goalCode: Option[String], @@ -147,6 +149,11 @@ private class ConnectionServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_invitee_pending_to_req_sent" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + // TODO Should we use a singleton producer or create a new one each time?? (underlying Kafka Producer is thread safe) + _ <- messageProducer + .produce(TOPIC_NAME, record.id, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id)) + .orDie maybeRecord <- connectionRepository .findById(record.id) record <- ZIO.getOrFailWith(RecordIdNotFound(recordId))(maybeRecord) @@ -220,6 +227,10 @@ private class ConnectionServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_inviter_pending_to_res_sent" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id)) + .orDie record <- connectionRepository.getById(record.id) } yield record @@ -306,6 +317,6 @@ private class ConnectionServiceImpl( } object ConnectionServiceImpl { - val layer: URLayer[ConnectionRepository, ConnectionService] = - ZLayer.fromFunction(ConnectionServiceImpl(_)) + val layer: URLayer[ConnectionRepository & Producer[UUID, WalletIdAndRecordId], ConnectionService] = + ZLayer.fromFunction(ConnectionServiceImpl(_, _)) } diff --git a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala index b0fa8d43fd..7067b55bf6 100644 --- a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala +++ b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceImplSpec.scala @@ -3,17 +3,17 @@ package org.hyperledger.identus.connect.core.service import io.circe.syntax.* import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.InvalidStateForOperation -import org.hyperledger.identus.connect.core.model.ConnectionRecord import org.hyperledger.identus.connect.core.model.ConnectionRecord.* import org.hyperledger.identus.connect.core.repository.ConnectionRepositoryInMemory import org.hyperledger.identus.mercury.model.{DidId, Message} import org.hyperledger.identus.mercury.protocol.connection.ConnectionResponse +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.WalletIdAndRecordId import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import zio.test.* import zio.test.Assertion.* -import java.time.Instant import java.util.UUID object ConnectionServiceImplSpec extends ZIOSpecDefault { @@ -310,7 +310,13 @@ object ConnectionServiceImplSpec extends ZIOSpecDefault { } } } - ).provide(connectionServiceLayer, ZLayer.succeed(WalletAccessContext(WalletId.random))) + ).provide( + connectionServiceLayer, + messaging.MessagingServiceConfig.inMemoryLayer, + messaging.MessagingService.serviceLayer, + messaging.MessagingService.producerLayer[UUID, WalletIdAndRecordId], + ZLayer.succeed(WalletAccessContext(WalletId.random)), + ) } } diff --git a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala index b9e54811b9..185bd95b95 100644 --- a/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala +++ b/connect/core/src/test/scala/org/hyperledger/identus/connect/core/service/ConnectionServiceNotifierSpec.scala @@ -7,11 +7,12 @@ import org.hyperledger.identus.event.notification.* import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse} import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.WalletIdAndRecordId import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import zio.mock.Expectation import zio.test.* -import zio.ZIO.* import java.time.Instant import java.util.UUID @@ -151,7 +152,10 @@ object ConnectionServiceNotifierSpec extends ZIOSpecDefault { ConnectionRepositoryInMemory.layer ++ inviteeExpectations.toLayer ) >>> ConnectionServiceNotifier.layer, - ZLayer.succeed(WalletAccessContext(WalletId.random)) + ZLayer.succeed(WalletAccessContext(WalletId.random)), + messaging.MessagingServiceConfig.inMemoryLayer, + messaging.MessagingService.serviceLayer, + messaging.MessagingService.producerLayer[UUID, WalletIdAndRecordId] ) ) } diff --git a/event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala b/event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala new file mode 100644 index 0000000000..54aea505f6 --- /dev/null +++ b/event-notification/src/test/scala/org/hyperledger/identus/messaging/MessagingServiceTest.scala @@ -0,0 +1,49 @@ +package org.hyperledger.identus.messaging + +import org.hyperledger.identus.shared.messaging +import org.hyperledger.identus.shared.messaging.{Message, MessagingService, Serde} +import zio.{durationInt, Random, Schedule, Scope, URIO, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer} +import zio.json.{DecoderOps, DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} + +import java.nio.charset.StandardCharsets +import java.util.UUID + +case class Customer(name: String) + +object Customer { + given encoder: JsonEncoder[Customer] = DeriveJsonEncoder.gen[Customer] + given decoder: JsonDecoder[Customer] = DeriveJsonDecoder.gen[Customer] + given serde: Serde[Customer] = new Serde[Customer]: + override def serialize(t: Customer): Array[Byte] = + t.toJson.getBytes(StandardCharsets.UTF_8) + override def deserialize(ba: Array[Byte]): Customer = + new String(ba, StandardCharsets.UTF_8).fromJson[Customer].getOrElse(Customer("Parsing Error")) +} + +object MessagingServiceTest extends ZIOAppDefault { + override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = { + val effect = for { + ms <- ZIO.service[MessagingService] + consumer <- ms.makeConsumer[UUID, Customer]("identus-cloud-agent") + producer <- ms.makeProducer[UUID, Customer]() + f1 <- consumer + .consume("Connect")(handle) + .fork + f2 <- Random.nextUUID + .flatMap(uuid => producer.produce("Connect", uuid, Customer(s"Name $uuid"))) + .repeat(Schedule.spaced(500.millis)) + .fork + _ <- ZIO.never + } yield () + effect.provide( + messaging.MessagingServiceConfig.inMemoryLayer, + messaging.MessagingService.serviceLayer, + ZLayer.succeed("Sample 'R' passed to handler") + ) + } + + def handle[K, V](msg: Message[K, V]): URIO[String, Unit] = for { + tag <- ZIO.service[String] + _ <- ZIO.logInfo(s"Handling new message [$tag]: ${msg.offset} - ${msg.key} - ${msg.value}") + } yield () +} diff --git a/event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala b/event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala new file mode 100644 index 0000000000..c6b068f16b --- /dev/null +++ b/event-notification/src/test/scala/org/hyperledger/identus/messaging/kafka/InMemoryMessagingServiceSpec.scala @@ -0,0 +1,66 @@ +package org.hyperledger.identus.messaging.kafka + +import org.hyperledger.identus.shared.messaging.* +import zio.* +import zio.test.* +import zio.test.Assertion.* + +object InMemoryMessagingServiceSpec extends ZIOSpecDefault { + val testLayer = MessagingServiceConfig.inMemoryLayer >+> MessagingService.serviceLayer >+> + MessagingService.producerLayer[String, String] >+> + MessagingService.consumerLayer[String, String]("test-group") + + def spec = suite("InMemoryMessagingServiceSpec")( + test("should produce and consume messages") { + + val key = "key" + val value = "value" + val topic = "test-topic" + for { + producer <- ZIO.service[Producer[String, String]] + consumer <- ZIO.service[Consumer[String, String]] + promise <- Promise.make[Nothing, Message[String, String]] + _ <- producer.produce(topic, key, value) + _ <- consumer + .consume(topic) { msg => + promise.succeed(msg).unit + } + .fork + receivedMessage <- promise.await + } yield assert(receivedMessage)(equalTo(Message(key, value, 1L, 0))) + }.provideLayer(testLayer), + test("should produce and consume 5 messages") { + val topic = "test-topic" + val messages = List( + ("key1", "value1"), + ("key2", "value2"), + ("key3", "value3"), + ("key4", "value4"), + ("key5", "value5") + ) + + for { + producer <- ZIO.service[Producer[String, String]] + consumer <- ZIO.service[Consumer[String, String]] + promise <- Promise.make[Nothing, List[Message[String, String]]] + ref <- Ref.make(List.empty[Message[String, String]]) + + _ <- ZIO.foreach(messages) { case (key, value) => + producer.produce(topic, key, value) *> ZIO.debug(s"Produced message: $key -> $value") + } + _ <- consumer + .consume(topic) { msg => + ZIO.debug(s"Consumed message: ${msg.key} -> ${msg.value}") *> + ref.update(_ :+ msg) *> ref.get.flatMap { msgs => + if (msgs.size == messages.size) promise.succeed(msgs).unit else ZIO.unit + } + } + .fork + receivedMessages <- promise.await + _ <- ZIO.debug(s"Received messages: ${receivedMessages.map(m => (m.key, m.value))}") + } yield assert(receivedMessages.map(m => (m.key, m.value)).sorted)( + equalTo(messages.sorted) + ) + }.provideLayer(testLayer), + ) +} diff --git a/infrastructure/shared/docker-compose-with-kafka.yml b/infrastructure/shared/docker-compose-with-kafka.yml new file mode 100644 index 0000000000..3e24ad6128 --- /dev/null +++ b/infrastructure/shared/docker-compose-with-kafka.yml @@ -0,0 +1,256 @@ +--- +services: + ########################## + # Database + ########################## + db: + image: postgres:13 + environment: + POSTGRES_MULTIPLE_DATABASES: "pollux,connect,agent,node_db" + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - pg_data_db:/var/lib/postgresql/data + - ./postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ./postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + ports: + - "127.0.0.1:${PG_PORT:-5432}:5432" + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-d", "agent"] + + interval: 10s + timeout: 5s + retries: 5 + + pgadmin: + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} + PGADMIN_CONFIG_SERVER_MODE: "False" + volumes: + - pgadmin:/var/lib/pgadmin + ports: + - "127.0.0.1:${PGADMIN_PORT:-5050}:80" + depends_on: + db: + condition: service_healthy + profiles: + - debug + + ########################## + # Services + ########################## + + prism-node: + image: ghcr.io/input-output-hk/prism-node:${PRISM_NODE_VERSION} + environment: + NODE_PSQL_HOST: db:5432 + NODE_REFRESH_AND_SUBMIT_PERIOD: + NODE_MOVE_SCHEDULED_TO_PENDING_PERIOD: + NODE_WALLET_MAX_TPS: + depends_on: + db: + condition: service_healthy + + vault-server: + image: hashicorp/vault:latest + # ports: + # - "8200:8200" + environment: + VAULT_ADDR: "http://0.0.0.0:8200" + VAULT_DEV_ROOT_TOKEN_ID: ${VAULT_DEV_ROOT_TOKEN_ID} + command: server -dev -dev-root-token-id=${VAULT_DEV_ROOT_TOKEN_ID} + cap_add: + - IPC_LOCK + healthcheck: + test: ["CMD", "vault", "status"] + interval: 10s + timeout: 5s + retries: 5 + + cloud-agent: + image: ghcr.io/hyperledger/identus-cloud-agent:${AGENT_VERSION} + environment: + POLLUX_DB_HOST: db + POLLUX_DB_PORT: 5432 + POLLUX_DB_NAME: pollux + POLLUX_DB_USER: postgres + POLLUX_DB_PASSWORD: postgres + CONNECT_DB_HOST: db + CONNECT_DB_PORT: 5432 + CONNECT_DB_NAME: connect + CONNECT_DB_USER: postgres + CONNECT_DB_PASSWORD: postgres + AGENT_DB_HOST: db + AGENT_DB_PORT: 5432 + AGENT_DB_NAME: agent + AGENT_DB_USER: postgres + AGENT_DB_PASSWORD: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://${DOCKERHOST}:${PORT}/cloud-agent + DIDCOMM_SERVICE_URL: http://${DOCKERHOST}:${PORT}/didcomm + REST_SERVICE_URL: http://${DOCKERHOST}:${PORT}/cloud-agent + PRISM_NODE_HOST: prism-node + PRISM_NODE_PORT: 50053 + VAULT_ADDR: ${VAULT_ADDR:-http://vault-server:8200} + VAULT_TOKEN: ${VAULT_DEV_ROOT_TOKEN_ID:-root} + SECRET_STORAGE_BACKEND: postgres + DEV_MODE: true + DEFAULT_WALLET_ENABLED: + DEFAULT_WALLET_SEED: + DEFAULT_WALLET_WEBHOOK_URL: + DEFAULT_WALLET_WEBHOOK_API_KEY: + DEFAULT_WALLET_AUTH_API_KEY: + DEFAULT_KAFKA_ENABLED: true + GLOBAL_WEBHOOK_URL: + GLOBAL_WEBHOOK_API_KEY: + WEBHOOK_PARALLELISM: + ADMIN_TOKEN: + API_KEY_SALT: + API_KEY_ENABLED: + API_KEY_AUTHENTICATE_AS_DEFAULT_USER: + API_KEY_AUTO_PROVISIONING: + depends_on: + db: + condition: service_healthy + prism-node: + condition: service_started + vault-server: + condition: service_healthy + init-kafka: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://cloud-agent:8085/_system/health"] + interval: 30s + timeout: 10s + retries: 5 + extra_hosts: + - "host.docker.internal:host-gateway" + + swagger-ui: + image: swaggerapi/swagger-ui:v5.1.0 + environment: + - 'URLS=[ + { name: "Cloud Agent", url: "/docs/cloud-agent/api/docs.yaml" } + ]' + + # apisix: + # image: apache/apisix:2.15.0-alpine + # volumes: + # - ./apisix/conf/apisix.yaml:/usr/local/apisix/conf/apisix.yaml:ro + # - ./apisix/conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro + # ports: + # - "${PORT}:9080/tcp" + # depends_on: + # - cloud-agent + # - swagger-ui + + nginx: + image: nginx:latest + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "${PORT}:80/tcp" + depends_on: + - cloud-agent + - swagger-ui + + zookeeper: + image: confluentinc/cp-zookeeper:latest + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + # ports: + # - 22181:2181 + + kafka: + image: confluentinc/cp-kafka:latest + depends_on: + - zookeeper + # ports: + # - 29092:29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: false + healthcheck: + test: + [ + "CMD", + "kafka-topics", + "--list", + "--bootstrap-server", + "localhost:9092", + ] + interval: 5s + timeout: 10s + retries: 5 + + init-kafka: + image: confluentinc/cp-kafka:latest + depends_on: + kafka: + condition: service_healthy + entrypoint: ["/bin/sh", "-c"] + command: | + " + # blocks until kafka is reachable + kafka-topics --bootstrap-server kafka:9092 --list + echo -e 'Creating kafka topics' + + # Connect + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-1 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-2 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-3 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-4 --replication-factor 1 --partitions 20 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-DLQ --replication-factor 1 --partitions 1 + + # Issue + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-DLQ --replication-factor 1 --partitions 1 + + # Present + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-DLQ --replication-factor 1 --partitions 1 + + # DID Publication State Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-did-state --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-did-state-DLQ --replication-factor 1 --partitions 5 + + # Status List Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-status-list --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-status-list-DLQ --replication-factor 1 --partitions 5 + + tail -f /dev/null + " + healthcheck: + test: + [ + "CMD-SHELL", + "kafka-topics --bootstrap-server kafka:9092 --list | grep -q 'sync-status-list'", + ] + interval: 5s + timeout: 10s + retries: 5 + +volumes: + pg_data_db: + pgadmin: +# Temporary commit network setting due to e2e CI bug +# to be enabled later after debugging +#networks: +# default: +# name: ${NETWORK} diff --git a/infrastructure/shared/nginx/nginx.conf b/infrastructure/shared/nginx/nginx.conf new file mode 100644 index 0000000000..937dc35a3f --- /dev/null +++ b/infrastructure/shared/nginx/nginx.conf @@ -0,0 +1,42 @@ +user nginx; + +events { + worker_connections 1000; +} + +http { + # Docker embedded DNS server (overriding TTL) + resolver 127.0.0.11 valid=5s; + + # Upstreams + upstream cloud_agent_8090 { + server cloud-agent:8090; + } + + upstream cloud_agent_8085 { + server cloud-agent:8085; + } + + # Server configuration + server { + listen 80; + + # Route /cloud-agent/* + location ~ ^/cloud-agent/(.*) { + # Proxy rewrite + set $upstream_servers cloud-agent; + rewrite ^/cloud-agent/(.*) /$1 break; + proxy_pass http://$upstream_servers:8085; + proxy_connect_timeout 5s; + } + + # Route /didcomm* + location ~ ^/didcomm(.*) { + # Proxy rewrite + set $upstream_servers cloud-agent; + rewrite ^/didcomm(.*) /$1 break; + proxy_pass http://$upstream_servers:8090; + proxy_connect_timeout 5s; + } + } +} \ No newline at end of file diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala index 47fcced892..ed18017d45 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/DidCommID.scala @@ -7,4 +7,6 @@ opaque type DidCommID = String object DidCommID: def apply(value: String): DidCommID = value def apply(): DidCommID = UUID.randomUUID.toString() - extension (id: DidCommID) def value: String = id + extension (id: DidCommID) + def value: String = id + def uuid: UUID = UUID.fromString(id) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala index 6a86509592..6d8cdbb50d 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepository.scala @@ -1,30 +1,61 @@ package org.hyperledger.identus.pollux.core.repository import org.hyperledger.identus.pollux.core.model.* +import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021} +import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.{ + DecodingError, + EncodingError, + IndexOutOfBounds, + InvalidSize +} import org.hyperledger.identus.pollux.vc.jwt.Issuer -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import java.util.UUID trait CredentialStatusListRepository { - def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]] + def createStatusListVC( + jwtIssuer: Issuer, + statusListRegistryUrl: String, + id: UUID + ): IO[Throwable, String] = { + for { + bitString <- BitString.getInstance().mapError { + case InvalidSize(message) => new Throwable(message) + case EncodingError(message) => new Throwable(message) + case DecodingError(message) => new Throwable(message) + case IndexOutOfBounds(message) => new Throwable(message) + } + emptyStatusListCredential <- VCStatusList2021 + .build( + vcId = s"$statusListRegistryUrl/credential-status/$id", + revocationData = bitString, + jwtIssuer = jwtIssuer + ) + .mapError(x => new Throwable(x.msg)) + + credentialWithEmbeddedProof <- emptyStatusListCredential.toJsonWithEmbeddedProof + } yield credentialWithEmbeddedProof.spaces2 + } + + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] + + def getCredentialStatusListsWithCreds(statusListId: UUID): URIO[WalletAccessContext, CredentialStatusListWithCreds] def findById( id: UUID ): UIO[Option[CredentialStatusList]] - def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] + def incrementAndGetStatusListIndex( + jwtIssuer: Issuer, + statusListRegistryUrl: String + ): URIO[WalletAccessContext, (UUID, Int)] def existsForIssueCredentialRecordId( id: DidCommID ): URIO[WalletAccessContext, Boolean] - def createNewForTheWallet( - jwtIssuer: Issuer, - statusListRegistryServiceName: String - ): URIO[WalletAccessContext, CredentialStatusList] - def allocateSpaceForCredential( issueCredentialRecordId: DidCommID, credentialStatusListId: UUID, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala index 4e059f8e05..a95e80925e 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala @@ -28,6 +28,7 @@ import org.hyperledger.identus.pollux.sdjwt.* import org.hyperledger.identus.pollux.vc.jwt.{Issuer as JwtIssuer, *} import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Secp256k1KeyPair} import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils @@ -42,7 +43,8 @@ import scala.language.implicitConversions object CredentialServiceImpl { val layer: URLayer[ CredentialRepository & CredentialStatusListRepository & DidResolver & UriResolver & GenericSecretStorage & - CredentialDefinitionService & LinkSecretService & DIDService & ManagedDIDService, + CredentialDefinitionService & LinkSecretService & DIDService & ManagedDIDService & + Producer[UUID, WalletIdAndRecordId], CredentialService ] = { ZLayer.fromZIO { @@ -56,7 +58,7 @@ object CredentialServiceImpl { linkSecretService <- ZIO.service[LinkSecretService] didService <- ZIO.service[DIDService] manageDidService <- ZIO.service[ManagedDIDService] - issueCredentialSem <- Semaphore.make(1) + messageProducer <- ZIO.service[Producer[UUID, WalletIdAndRecordId]] } yield CredentialServiceImpl( credentialRepo, credentialStatusListRepo, @@ -68,7 +70,7 @@ object CredentialServiceImpl { didService, manageDidService, 5, - issueCredentialSem + messageProducer ) } } @@ -88,12 +90,14 @@ class CredentialServiceImpl( didService: DIDService, managedDIDService: ManagedDIDService, maxRetries: Int = 5, // TODO move to config - issueCredentialSem: Semaphore + messageProducer: Producer[UUID, WalletIdAndRecordId], ) extends CredentialService { import CredentialServiceImpl.* import IssueCredentialRecord.* + private val TOPIC_NAME = "issue" + override def getIssueCredentialRecords( ignoreWithZeroRetries: Boolean, offset: Option[Int], @@ -187,6 +191,10 @@ class CredentialServiceImpl( count <- credentialRepository .create(record) @@ CustomMetricsAspect .startRecordingTime(s"${record.id}_issuer_offer_pending_to_sent_ms_gauge") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie } yield record } @@ -500,6 +508,10 @@ class CredentialServiceImpl( ) case (format, maybeSubjectId) => ZIO.dieMessage(s"Invalid subjectId input for $format offer acceptance: $maybeSubjectId") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -661,6 +673,10 @@ class CredentialServiceImpl( s"${record.id}_issuance_flow_holder_req_pending_to_generated", "issuance_flow_holder_req_pending_to_generated_ms_gauge" ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -707,6 +723,10 @@ class CredentialServiceImpl( s"${record.id}_issuance_flow_holder_req_pending_to_generated", "issuance_flow_holder_req_pending_to_generated_ms_gauge" ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -751,6 +771,10 @@ class CredentialServiceImpl( ProtocolState.OfferSent ) _ <- credentialRepository.updateWithJWTRequestCredential(record.id, request, ProtocolState.RequestReceived) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -769,6 +793,10 @@ class CredentialServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_issuance_flow_issuer_credential_pending_to_generated" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -913,6 +941,10 @@ class CredentialServiceImpl( s"${record.id}_issuance_flow_issuer_credential_pending_to_generated", "issuance_flow_issuer_credential_pending_to_generated_ms_gauge" ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_issuer_credential_generated_to_sent") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- credentialRepository.getById(record.id) } yield record } @@ -1275,32 +1307,26 @@ class CredentialServiceImpl( record: IssueCredentialRecord, statusListRegistryUrl: String, jwtIssuer: JwtIssuer - ): URIO[WalletAccessContext, CredentialStatus] = { - val effect = for { - lastStatusList <- credentialStatusListRepository.getLatestOfTheWallet - currentStatusList <- lastStatusList - .fold(credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl))( - ZIO.succeed(_) - ) - size = currentStatusList.size - lastUsedIndex = currentStatusList.lastUsedIndex - statusListToBeUsed <- - if lastUsedIndex < size then ZIO.succeed(currentStatusList) - else credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl) + ): URIO[WalletAccessContext, CredentialStatus] = + for { + cslAndIndex <- credentialStatusListRepository.incrementAndGetStatusListIndex( + jwtIssuer, + statusListRegistryUrl + ) + statusListId = cslAndIndex._1 + indexInStatusList = cslAndIndex._2 _ <- credentialStatusListRepository.allocateSpaceForCredential( issueCredentialRecordId = record.id, - credentialStatusListId = statusListToBeUsed.id, - statusListIndex = statusListToBeUsed.lastUsedIndex + 1 + credentialStatusListId = statusListId, + statusListIndex = indexInStatusList ) } yield CredentialStatus( - id = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}#${statusListToBeUsed.lastUsedIndex + 1}", + id = s"$statusListRegistryUrl/credential-status/$statusListId#$indexInStatusList", `type` = "StatusList2021Entry", statusPurpose = StatusPurpose.Revocation, - statusListIndex = lastUsedIndex + 1, - statusListCredential = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}" + statusListIndex = indexInStatusList, + statusListCredential = s"$statusListRegistryUrl/credential-status/$statusListId" ) - issueCredentialSem.withPermit(effect) - } override def generateAnonCredsCredential( recordId: DidCommID diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala index 5a186d2826..418b3faa0c 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListService.scala @@ -6,7 +6,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialStatusListServi StatusListNotFound, StatusListNotFoundForIssueCredentialRecord } -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import java.util.UUID @@ -20,7 +20,9 @@ trait CredentialStatusListService { id: DidCommID ): ZIO[WalletAccessContext, StatusListNotFoundForIssueCredentialRecord | InvalidRoleForOperation, Unit] - def getCredentialsAndItsStatuses: UIO[Seq[CredentialStatusListWithCreds]] + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] + + def getCredentialStatusListWithCreds(statusListId: UUID): URIO[WalletAccessContext, CredentialStatusListWithCreds] def updateStatusListCredential( id: UUID, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala index 92565a8559..ef752f8648 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialStatusListServiceImpl.scala @@ -8,7 +8,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialStatusListServi } import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.Role import org.hyperledger.identus.pollux.core.repository.CredentialStatusListRepository -import org.hyperledger.identus.shared.models.WalletAccessContext +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import java.util.UUID @@ -18,8 +18,11 @@ class CredentialStatusListServiceImpl( credentialStatusListRepository: CredentialStatusListRepository, ) extends CredentialStatusListService { - def getCredentialsAndItsStatuses: UIO[Seq[CredentialStatusListWithCreds]] = - credentialStatusListRepository.getCredentialStatusListsWithCreds + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] = + credentialStatusListRepository.getCredentialStatusListIds + + def getCredentialStatusListWithCreds(statusListId: UUID): URIO[WalletAccessContext, CredentialStatusListWithCreds] = + credentialStatusListRepository.getCredentialStatusListsWithCreds(statusListId) def getById(id: UUID): IO[StatusListNotFound, CredentialStatusList] = for { diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala index 94aa1af79b..20426ec477 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala @@ -105,7 +105,7 @@ trait PresentationService { def findPresentationRecord( recordId: DidCommID - ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] + ): URIO[WalletAccessContext, Option[PresentationRecord]] def findPresentationRecordByThreadId( thid: DidCommID diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala index 8200595ebf..3d18a25bbe 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala @@ -20,6 +20,7 @@ import org.hyperledger.identus.pollux.core.service.serdes.* import org.hyperledger.identus.pollux.sdjwt.{CredentialCompact, HolderPrivateKey, PresentationCompact, SDJWT} import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import org.hyperledger.identus.shared.utils.Base64Utils @@ -37,11 +38,14 @@ private class PresentationServiceImpl( linkSecretService: LinkSecretService, presentationRepository: PresentationRepository, credentialRepository: CredentialRepository, - maxRetries: Int = 5, // TODO move to config + messageProducer: Producer[UUID, WalletIdAndRecordId], + maxRetries: Int = 5, // TODO move to config, ) extends PresentationService { import PresentationRecord.* + private val TOPIC_NAME = "present" + override def markPresentationGenerated( recordId: DidCommID, presentation: Presentation @@ -57,6 +61,10 @@ private class PresentationServiceImpl( ) @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_generated_to_sent_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -298,7 +306,7 @@ private class PresentationServiceImpl( override def findPresentationRecord( recordId: DidCommID - ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] = + ): URIO[WalletAccessContext, Option[PresentationRecord]] = presentationRepository.findPresentationRecord(recordId) override def findPresentationRecordByThreadId( @@ -459,6 +467,10 @@ private class PresentationServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_verifier_req_pending_to_sent_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie } yield record } @@ -531,6 +543,10 @@ private class PresentationServiceImpl( ) _ <- presentationRepository.createPresentationRecord(record) _ <- ZIO.logDebug(s"Received and created the RequestPresentation: $request") + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie } yield record } @@ -813,6 +829,10 @@ private class PresentationServiceImpl( @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -841,6 +861,10 @@ private class PresentationServiceImpl( ) @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -875,6 +899,10 @@ private class PresentationServiceImpl( ) @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(record.id) } yield record } @@ -961,6 +989,10 @@ private class PresentationServiceImpl( .startRecordingTime( s"${record.id}_present_proof_flow_verifier_presentation_received_to_verification_success_or_failure_ms_gauge" ) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(record.id) } yield record } @@ -977,6 +1009,10 @@ private class PresentationServiceImpl( requestPresentation = createDidCommRequestPresentationFromProposal(request) _ <- presentationRepository .updateWithRequestPresentation(recordId, requestPresentation, ProtocolState.PresentationPending) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(recordId) } yield record } @@ -993,6 +1029,10 @@ private class PresentationServiceImpl( record <- getRecordFromThreadId(thid) _ <- presentationRepository .updateWithProposePresentation(record.id, proposePresentation, ProtocolState.ProposalReceived) + walletAccessContext <- ZIO.service[WalletAccessContext] + _ <- messageProducer + .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid)) + .orDie record <- getRecord(record.id) } yield record } @@ -1305,8 +1345,9 @@ private class PresentationServiceImpl( object PresentationServiceImpl { val layer: URLayer[ - UriResolver & LinkSecretService & PresentationRepository & CredentialRepository, + UriResolver & LinkSecretService & PresentationRepository & CredentialRepository & + Producer[UUID, WalletIdAndRecordId], PresentationService ] = - ZLayer.fromFunction(PresentationServiceImpl(_, _, _, _)) + ZLayer.fromFunction(PresentationServiceImpl(_, _, _, _, _)) } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala index 80d358cdbf..350c161100 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala @@ -14,6 +14,7 @@ import org.hyperledger.identus.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCr import org.hyperledger.identus.shared.models.* import zio.* import zio.json.* +import zio.URIO import java.time.Instant import java.util.UUID @@ -275,7 +276,7 @@ class PresentationServiceNotifier( override def findPresentationRecord( recordId: DidCommID - ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] = + ): URIO[WalletAccessContext, Option[PresentationRecord]] = svc.findPresentationRecord(recordId) override def findPresentationRecordByThreadId( diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala index 8ea5ca21d1..3e4f885f2b 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala @@ -1,15 +1,9 @@ package org.hyperledger.identus.pollux.core.repository -import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, PrismDID} +import org.hyperledger.identus.castor.core.model.did.PrismDID import org.hyperledger.identus.pollux.core.model.* -import org.hyperledger.identus.pollux.vc.jwt.{revocation, Issuer, StatusPurpose} -import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021} -import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.{ - DecodingError, - EncodingError, - IndexOutOfBounds, - InvalidSize -} +import org.hyperledger.identus.pollux.vc.jwt.{Issuer, StatusPurpose} +import org.hyperledger.identus.pollux.vc.jwt.revocation.BitString import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -73,65 +67,69 @@ class CredentialStatusListRepositoryInMemory( exists = stores.flatMap(_.values).exists(_.issueCredentialRecordId == id) } yield exists - def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = for { - storageRef <- walletToStatusListStorageRefs - storage <- storageRef.get - latest = storage.toSeq - .sortBy(_._2.createdAt) { (x, y) => if x.isAfter(y) then -1 else 1 /* DESC */ } - .headOption - .map(_._2) - } yield latest - - def createNewForTheWallet( + override def incrementAndGetStatusListIndex( jwtIssuer: Issuer, statusListRegistryUrl: String - ): URIO[WalletAccessContext, CredentialStatusList] = { + ): URIO[WalletAccessContext, (UUID, Int)] = + def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = for { + storageRef <- walletToStatusListStorageRefs + storage <- storageRef.get + latest = storage.toSeq + .sortBy(_._2.createdAt) { (x, y) => if x.isAfter(y) then -1 else 1 /* DESC */ } + .headOption + .map(_._2) + } yield latest - val id = UUID.randomUUID() - val issued = Instant.now() - val issuerDid = jwtIssuer.did - val canonical = PrismDID.fromString(issuerDid.toString).fold(e => throw RuntimeException(e), _.asCanonical) + def createNewForTheWallet( + id: UUID, + jwtIssuer: Issuer, + issued: Instant, + credentialStr: String + ): URIO[WalletAccessContext, CredentialStatusList] = { + val issuerDid = jwtIssuer.did + val canonical = PrismDID.fromString(issuerDid.toString).fold(e => throw RuntimeException(e), _.asCanonical) - val embeddedProofCredential = for { - bitString <- BitString.getInstance().mapError { - case InvalidSize(message) => new Throwable(message) - case EncodingError(message) => new Throwable(message) - case DecodingError(message) => new Throwable(message) - case IndexOutOfBounds(message) => new Throwable(message) - } - resourcePath = - s"credential-status/$id" - emptyJwtCredential <- VCStatusList2021 - .build( - vcId = s"$statusListRegistryUrl/credential-status/$id", - revocationData = bitString, - jwtIssuer = jwtIssuer + for { + storageRef <- walletToStatusListStorageRefs + walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) + newCredentialStatusList = CredentialStatusList( + id = id, + walletId = walletId, + issuer = canonical, + issued = issued, + purpose = StatusPurpose.Revocation, + statusListCredential = credentialStr, + size = BitString.MIN_SL2021_SIZE, + lastUsedIndex = 0, + createdAt = Instant.now(), + updatedAt = None ) - .mapError(x => new Throwable(x.msg)) + _ <- storageRef.update(r => r + (newCredentialStatusList.id -> newCredentialStatusList)) + } yield newCredentialStatusList + } - credentialWithEmbeddedProof <- emptyJwtCredential.toJsonWithEmbeddedProof - } yield credentialWithEmbeddedProof.spaces2 + def updateLastUsedIndex(statusListId: UUID, lastUsedIndex: Int) = + for { + walletToStatusListStorageRef <- walletToStatusListStorageRefs + _ <- walletToStatusListStorageRef.update(r => { + val value = r.get(statusListId) + value.fold(r) { v => + val updated = v.copy(lastUsedIndex = lastUsedIndex, updatedAt = Some(Instant.now)) + r.updated(statusListId, updated) + } + }) + } yield () for { - credential <- embeddedProofCredential.orDie - storageRef <- walletToStatusListStorageRefs - walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId) - newCredentialStatusList = CredentialStatusList( - id = id, - walletId = walletId, - issuer = canonical, - issued = issued, - purpose = StatusPurpose.Revocation, - statusListCredential = credential, - size = BitString.MIN_SL2021_SIZE, - lastUsedIndex = 0, - createdAt = Instant.now(), - updatedAt = None - ) - _ <- storageRef.update(r => r + (newCredentialStatusList.id -> newCredentialStatusList)) - } yield newCredentialStatusList - - } + id <- ZIO.succeed(UUID.randomUUID()) + newStatusListVC <- createStatusListVC(jwtIssuer, statusListRegistryUrl, id).orDie + maybeStatusList <- getLatestOfTheWallet + statusList <- maybeStatusList match + case Some(csl) if csl.lastUsedIndex < csl.size => ZIO.succeed(csl) + case _ => createNewForTheWallet(id, jwtIssuer, Instant.now(), newStatusListVC) + newIndex = statusList.lastUsedIndex + 1 + _ <- updateLastUsedIndex(statusList.id, newIndex) + } yield (statusList.id, newIndex) def allocateSpaceForCredential( issueCredentialRecordId: DidCommID, @@ -152,14 +150,6 @@ class CredentialStatusListRepositoryInMemory( for { credentialInStatusListStorageRef <- statusListToCredInStatusListStorageRefs(credentialStatusListId) _ <- credentialInStatusListStorageRef.update(r => r + (newCredentialInStatusList.id -> newCredentialInStatusList)) - walletToStatusListStorageRef <- walletToStatusListStorageRefs - _ <- walletToStatusListStorageRef.update(r => { - val value = r.get(credentialStatusListId) - value.fold(r) { v => - val updated = v.copy(lastUsedIndex = statusListIndex, updatedAt = Some(Instant.now)) - r.updated(credentialStatusListId, updated) - } - }) } yield () } @@ -188,37 +178,39 @@ class CredentialStatusListRepositoryInMemory( } yield () } - def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]] = { + override def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] = for { statusListsRefs <- allStatusListsStorageRefs statusLists <- statusListsRefs.get - statusListWithCredEffects = statusLists.map { (id, statusList) => - val credsinStatusListEffect = statusListToCredInStatusListStorageRefs(id).flatMap(_.get.map(_.values.toList)) - credsinStatusListEffect.map { credsInStatusList => - CredentialStatusListWithCreds( - id = id, - walletId = statusList.walletId, - issuer = statusList.issuer, - issued = statusList.issued, - purpose = statusList.purpose, - statusListCredential = statusList.statusListCredential, - size = statusList.size, - lastUsedIndex = statusList.lastUsedIndex, - credentials = credsInStatusList.map { cred => - CredInStatusList( - id = cred.id, - issueCredentialRecordId = cred.issueCredentialRecordId, - statusListIndex = cred.statusListIndex, - isCanceled = cred.isCanceled, - isProcessed = cred.isProcessed, - ) - } - ) - } + } yield statusLists.values.toList.map(csl => (csl.walletId, csl.id)) - }.toList - res <- ZIO.collectAll(statusListWithCredEffects) - } yield res + def getCredentialStatusListsWithCreds( + statusListId: UUID + ): URIO[WalletAccessContext, CredentialStatusListWithCreds] = { + for { + statusListsRefs <- allStatusListsStorageRefs + statusLists <- statusListsRefs.get + statusList = statusLists(statusListId) + credsInStatusList <- statusListToCredInStatusListStorageRefs(statusList.id).flatMap(_.get.map(_.values.toList)) + } yield CredentialStatusListWithCreds( + id = statusList.id, + walletId = statusList.walletId, + issuer = statusList.issuer, + issued = statusList.issued, + purpose = statusList.purpose, + statusListCredential = statusList.statusListCredential, + size = statusList.size, + lastUsedIndex = statusList.lastUsedIndex, + credentials = credsInStatusList.map { cred => + CredInStatusList( + id = cred.id, + issueCredentialRecordId = cred.issueCredentialRecordId, + statusListIndex = cred.statusListIndex, + isCanceled = cred.isCanceled, + isProcessed = cred.isProcessed, + ) + } + ) } def updateStatusListCredential( diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala index a03eb88803..8ae2fd6602 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala @@ -3,7 +3,6 @@ package org.hyperledger.identus.pollux.core.service import io.circe.Json import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService -import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage import org.hyperledger.identus.castor.core.model.did.PrismDID import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.mercury.model.{AttachmentDescriptor, DidId} @@ -18,6 +17,7 @@ import org.hyperledger.identus.pollux.core.repository.{ import org.hyperledger.identus.pollux.prex.{ClaimFormat, Ldp, PresentationDefinition} import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{MessagingService, MessagingServiceConfig, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -41,6 +41,8 @@ trait CredentialServiceSpecHelper { credentialDefinitionServiceLayer, GenericSecretStorageInMemory.layer, LinkSecretServiceImpl.layer, + (MessagingServiceConfig.inMemoryLayer >>> MessagingService.serviceLayer >>> + MessagingService.producerLayer[UUID, WalletIdAndRecordId]).orDie, CredentialServiceImpl.layer ) diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala index 4f58c2f570..dad75bfcee 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala @@ -16,7 +16,7 @@ import org.hyperledger.identus.pollux.core.service.serdes.{AnoncredCredentialPro import org.hyperledger.identus.pollux.sdjwt.{HolderPrivateKey, PresentationCompact} import org.hyperledger.identus.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCredentialPayload} import org.hyperledger.identus.shared.models.* -import zio.{mock, Duration, IO, UIO, URLayer, ZIO, ZLayer} +import zio.{mock, Duration, IO, UIO, URIO, URLayer, ZIO, ZLayer} import zio.json.* import zio.mock.{Mock, Proxy} @@ -329,7 +329,8 @@ object MockPresentationService extends Mock[PresentationService] { state: PresentationRecord.ProtocolState* ): IO[PresentationError, Seq[PresentationRecord]] = ??? - override def findPresentationRecord(recordId: DidCommID): IO[PresentationError, Option[PresentationRecord]] = ??? + override def findPresentationRecord(recordId: DidCommID): URIO[WalletAccessContext, Option[PresentationRecord]] = + ??? override def findPresentationRecordByThreadId( thid: DidCommID diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala index e438d4687b..1e59c4704a 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala @@ -1,6 +1,5 @@ package org.hyperledger.identus.pollux.core.service -import com.nimbusds.jose.jwk.* import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.castor.core.model.did.DID import org.hyperledger.identus.mercury.{AgentPeerService, PeerDID} @@ -14,6 +13,7 @@ import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResol import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.crypto.KmpSecp256k1KeyOps import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.messaging.{MessagingService, MessagingServiceConfig, WalletIdAndRecordId} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -42,7 +42,9 @@ trait PresentationServiceSpecHelper { uriResolverLayer, linkSecretLayer, PresentationRepositoryInMemory.layer, - CredentialRepositoryInMemory.layer + CredentialRepositoryInMemory.layer, + (MessagingServiceConfig.inMemoryLayer >>> MessagingService.serviceLayer >>> + MessagingService.producerLayer[UUID, WalletIdAndRecordId]).orDie, ) ++ defaultWalletLayer def createIssuer(did: String): Issuer = { diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala index f0a9dc2dbe..f4b27410cf 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala @@ -1,6 +1,8 @@ package org.hyperledger.identus.pollux.sql.repository +import cats.implicits.toFunctorOps import doobie.* +import doobie.free.connection.ConnectionOp import doobie.implicits.* import doobie.postgres.* import doobie.postgres.implicits.* @@ -8,8 +10,7 @@ import org.hyperledger.identus.castor.core.model.did.* import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.repository.CredentialStatusListRepository import org.hyperledger.identus.pollux.vc.jwt.{Issuer, StatusPurpose} -import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, BitStringError, VCStatusList2021} -import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.* +import org.hyperledger.identus.pollux.vc.jwt.revocation.BitString import org.hyperledger.identus.shared.db.ContextAwareTask import org.hyperledger.identus.shared.db.Implicits.* import org.hyperledger.identus.shared.db.Implicits.given @@ -18,7 +19,7 @@ import zio.* import zio.interop.catz.* import java.time.Instant -import java.util.UUID +import java.util.{Objects, UUID} class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: Transactor[Task]) extends CredentialStatusListRepository { @@ -47,9 +48,19 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T .orDie } - def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = { + override def incrementAndGetStatusListIndex( + jwtIssuer: Issuer, + statusListRegistryUrl: String + ): URIO[WalletAccessContext, (UUID, Int)] = { - val cxnIO = + def acquireAdvisoryLock(walletId: WalletId): ConnectionIO[Unit] = { + // Should be specific to this process + val PROCESS_UNIQUE_ID = 235457 + val hashCode = Objects.hash(walletId.hashCode(), PROCESS_UNIQUE_ID) + sql"SELECT pg_advisory_xact_lock($hashCode)".query[Unit].unique.void + } + + def getLatestOfTheWallet: ConnectionIO[Option[CredentialStatusList]] = sql""" | SELECT | id, @@ -62,74 +73,70 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T | last_used_index, | created_at, | updated_at - | FROM public.credential_status_lists order by created_at DESC limit 1 + | FROM public.credential_status_lists + | ORDER BY created_at DESC limit 1 |""".stripMargin .query[CredentialStatusList] .option - cxnIO - .transactWallet(xa) - .orDie - - } + def createNewForTheWallet( + id: UUID, + issuerDid: String, + issued: Instant, + credentialStr: String + ): ConnectionIO[CredentialStatusList] = + sql""" + |INSERT INTO public.credential_status_lists ( + | id, + | issuer, + | issued, + | purpose, + | status_list_credential, + | size, + | last_used_index, + | wallet_id + | ) + |VALUES ( + | $id, + | $issuerDid, + | $issued, + | ${StatusPurpose.Revocation}::public.enum_credential_status_list_purpose, + | $credentialStr::JSON, + | ${BitString.MIN_SL2021_SIZE}, + | 0, + | current_setting('app.current_wallet_id')::UUID + | ) + |RETURNING id, wallet_id, issuer, issued, purpose, status_list_credential, size, last_used_index, created_at, updated_at + """.stripMargin + .query[CredentialStatusList] + .unique - def createNewForTheWallet( - jwtIssuer: Issuer, - statusListRegistryUrl: String - ): URIO[WalletAccessContext, CredentialStatusList] = { - - val id = UUID.randomUUID() - val issued = Instant.now() - val issuerDid = jwtIssuer.did.toString - - val credentialWithEmbeddedProof = for { - bitString <- BitString.getInstance().mapError { - case InvalidSize(message) => new Throwable(message) - case EncodingError(message) => new Throwable(message) - case DecodingError(message) => new Throwable(message) - case IndexOutOfBounds(message) => new Throwable(message) - } - resourcePath = - s"credential-status/$id" - emptyStatusListCredential <- VCStatusList2021 - .build( - vcId = s"$statusListRegistryUrl/credential-status/$id", - revocationData = bitString, - jwtIssuer = jwtIssuer - ) - .mapError(x => new Throwable(x.msg)) - - credentialWithEmbeddedProof <- emptyStatusListCredential.toJsonWithEmbeddedProof - } yield credentialWithEmbeddedProof.spaces2 + def updateLastUsedIndex(statusListId: UUID, lastUsedIndex: Int): ConnectionIO[Int] = + sql""" + | UPDATE public.credential_status_lists + | SET + | last_used_index = $lastUsedIndex, + | updated_at = ${Instant.now()} + | WHERE + | id = $statusListId + |""".stripMargin.update.run (for { - credentialStr <- credentialWithEmbeddedProof - query = sql""" - |INSERT INTO public.credential_status_lists ( - | id, - | issuer, - | issued, - | purpose, - | status_list_credential, - | size, - | last_used_index, - | wallet_id - | ) - |VALUES ( - | $id, - | $issuerDid, - | $issued, - | ${StatusPurpose.Revocation}::public.enum_credential_status_list_purpose, - | $credentialStr::JSON, - | ${BitString.MIN_SL2021_SIZE}, - | 0, - | current_setting('app.current_wallet_id')::UUID - | ) - |RETURNING id, wallet_id, issuer, issued, purpose, status_list_credential, size, last_used_index, created_at, updated_at - """.stripMargin.query[CredentialStatusList].unique - newStatusList <- query.transactWallet(xa) - } yield newStatusList).orDie - + id <- ZIO.succeed(UUID.randomUUID()) + newStatusListVC <- createStatusListVC(jwtIssuer, statusListRegistryUrl, id) + walletCtx <- ZIO.service[WalletAccessContext] + walletId = walletCtx.walletId + cnxIO = for { + _ <- acquireAdvisoryLock(walletId) + maybeStatusList <- getLatestOfTheWallet + statusList <- maybeStatusList match + case Some(csl) if csl.lastUsedIndex < csl.size => cats.free.Free.pure[ConnectionOp, CredentialStatusList](csl) + case _ => createNewForTheWallet(id, jwtIssuer.did.toString, Instant.now(), newStatusListVC) + newIndex = statusList.lastUsedIndex + 1 + _ <- updateLastUsedIndex(statusList.id, newIndex) + } yield (statusList.id, newIndex) + result <- cnxIO.transactWallet(xa) + } yield result).orDie } def allocateSpaceForCredential( @@ -214,9 +221,24 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T } yield () } - def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]] = { + def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]] = { + val cxnIO = + sql""" + | SELECT + | wallet_id, + | id + | FROM public.credential_status_lists + |""".stripMargin + .query[(WalletId, UUID)] + .to[Seq] + cxnIO + .transact(xb) + .orDie + } - // Might need to add wallet Id in the select query, because I'm selecting all of them + def getCredentialStatusListsWithCreds( + statusListId: UUID + ): URIO[WalletAccessContext, CredentialStatusListWithCreds] = { val cxnIO = sql""" | SELECT @@ -235,42 +257,35 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T | cisl.is_processed | FROM public.credential_status_lists csl | LEFT JOIN public.credentials_in_status_list cisl ON csl.id = cisl.credential_status_list_id + | WHERE + | csl.id = $statusListId |""".stripMargin .query[CredentialStatusListWithCred] .to[List] - - val credentialStatusListsWithCredZio = cxnIO - .transact(xb) - .orDie - - for { - credentialStatusListsWithCred <- credentialStatusListsWithCredZio - } yield { - credentialStatusListsWithCred - .groupBy(_.credentialStatusListId) - .map { case (id, items) => - CredentialStatusListWithCreds( - id, - items.head.walletId, - items.head.issuer, - items.head.issued, - items.head.purpose, - items.head.statusListCredential, - items.head.size, - items.head.lastUsedIndex, - items.map { item => - CredInStatusList( - item.credentialInStatusListId, - item.issueCredentialRecordId, - item.statusListIndex, - item.isCanceled, - item.isProcessed, - ) - } + .transactWallet(xa) + .orDie + + cxnIO.map(items => + CredentialStatusListWithCreds( + statusListId, + items.head.walletId, + items.head.issuer, + items.head.issued, + items.head.purpose, + items.head.statusListCredential, + items.head.size, + items.head.lastUsedIndex, + items.map { item => + CredInStatusList( + item.credentialInStatusListId, + item.issueCredentialRecordId, + item.statusListIndex, + item.isCanceled, + item.isProcessed, ) } - .toList - } + ) + ) } def updateStatusListCredential( diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala index 1e477640c1..71b4cad3dc 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala @@ -459,7 +459,6 @@ class JdbcPresentationRepository( | id = $recordId | AND protocol_state = $from """.stripMargin.update - cxnIO.run .transactWallet(xa) .orDie diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala new file mode 100644 index 0000000000..8c3a60e56e --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingService.scala @@ -0,0 +1,106 @@ +package org.hyperledger.identus.shared.messaging + +import org.hyperledger.identus.shared.messaging.kafka.{InMemoryMessagingService, ZKafkaMessagingServiceImpl} +import zio.{durationInt, Cause, Duration, EnvironmentTag, RIO, RLayer, Task, URIO, URLayer, ZIO, ZLayer} + +import java.time.Instant +trait MessagingService { + def makeConsumer[K, V](groupId: String)(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Consumer[K, V]] + def makeProducer[K, V]()(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Producer[K, V]] +} + +object MessagingService { + + case class RetryStep(topicName: String, consumerCount: Int, consumerBackoff: Duration, nextTopicName: Option[String]) + + object RetryStep { + def apply(topicName: String, consumerCount: Int, consumerBackoff: Duration, nextTopicName: String): RetryStep = + RetryStep(topicName, consumerCount, consumerBackoff, Some(nextTopicName)) + } + + def consumeWithRetryStrategy[K: EnvironmentTag, V: EnvironmentTag, HR]( + groupId: String, + handler: Message[K, V] => RIO[HR, Unit], + steps: Seq[RetryStep] + )(implicit kSerde: Serde[K], vSerde: Serde[V]): RIO[HR & Producer[K, V] & MessagingService, Unit] = { + for { + messagingService <- ZIO.service[MessagingService] + messageProducer <- ZIO.service[Producer[K, V]] + _ <- ZIO.foreachPar(steps) { step => + ZIO.foreachPar(1 to step.consumerCount)(_ => + for { + consumer <- messagingService.makeConsumer[K, V](groupId) + _ <- consumer + .consume[HR](step.topicName) { m => + for { + // Wait configured backoff before processing message + millisSpentInQueue <- ZIO.succeed(Instant.now().toEpochMilli - m.timestamp) + sleepDelay = step.consumerBackoff.toMillis - millisSpentInQueue + _ <- ZIO.when(sleepDelay > 0)(ZIO.sleep(Duration.fromMillis(sleepDelay))) + _ <- handler(m) + .catchAll { t => + for { + _ <- ZIO.logErrorCause(s"Error processing message: ${m.key} ", Cause.fail(t)) + _ <- step.nextTopicName match + case Some(name) => + messageProducer + .produce(name, m.key, m.value) + .catchAll(t => + ZIO.logErrorCause("Unable to send message to the next topic", Cause.fail(t)) + ) + case None => ZIO.unit + } yield () + } + .catchAllDefect(t => ZIO.logErrorCause(s"Defect processing message: ${m.key} ", Cause.fail(t))) + } yield () + } + .debug + .fork + } yield () + ) + } + } yield () + } + + def consume[K: EnvironmentTag, V: EnvironmentTag, HR]( + groupId: String, + topicName: String, + consumerCount: Int, + handler: Message[K, V] => RIO[HR, Unit] + )(implicit kSerde: Serde[K], vSerde: Serde[V]): RIO[HR & Producer[K, V] & MessagingService, Unit] = + consumeWithRetryStrategy(groupId, handler, Seq(RetryStep(topicName, consumerCount, 0.seconds, None))) + + val serviceLayer: URLayer[MessagingServiceConfig, MessagingService] = + ZLayer + .service[MessagingServiceConfig] + .flatMap(config => + if (config.get.kafkaEnabled) ZKafkaMessagingServiceImpl.layer + else InMemoryMessagingService.layer + ) + + def producerLayer[K: EnvironmentTag, V: EnvironmentTag](implicit + kSerde: Serde[K], + vSerde: Serde[V] + ): RLayer[MessagingService, Producer[K, V]] = ZLayer.fromZIO(for { + messagingService <- ZIO.service[MessagingService] + producer <- messagingService.makeProducer[K, V]() + } yield producer) + + def consumerLayer[K: EnvironmentTag, V: EnvironmentTag](groupId: String)(implicit + kSerde: Serde[K], + vSerde: Serde[V] + ): RLayer[MessagingService, Consumer[K, V]] = ZLayer.fromZIO(for { + messagingService <- ZIO.service[MessagingService] + consumer <- messagingService.makeConsumer[K, V](groupId) + } yield consumer) + +} + +case class Message[K, V](key: K, value: V, offset: Long, timestamp: Long) + +trait Consumer[K, V] { + def consume[HR](topic: String, topics: String*)(handler: Message[K, V] => URIO[HR, Unit]): RIO[HR, Unit] +} +trait Producer[K, V] { + def produce(topic: String, key: K, value: V): Task[Unit] +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala new file mode 100644 index 0000000000..dd63c1a424 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/MessagingServiceConfig.scala @@ -0,0 +1,58 @@ +package org.hyperledger.identus.shared.messaging + +import zio.{ULayer, ZLayer} + +import java.time.Duration + +case class MessagingServiceConfig( + connectFlow: ConsumerJobConfig, + issueFlow: ConsumerJobConfig, + presentFlow: ConsumerJobConfig, + didStateSync: ConsumerJobConfig, + statusListSync: ConsumerJobConfig, + inMemoryQueueCapacity: Int, + kafkaEnabled: Boolean, + kafka: Option[KafkaConfig] +) + +final case class ConsumerJobConfig( + consumerCount: Int, + retryStrategy: Option[ConsumerRetryStrategy] +) + +final case class ConsumerRetryStrategy( + maxRetries: Int, + initialDelay: Duration, + maxDelay: Duration +) + +final case class KafkaConfig( + bootstrapServers: String, + consumers: KafkaConsumersConfig +) + +final case class KafkaConsumersConfig( + autoCreateTopics: Boolean, + maxPollRecords: Int, + maxPollInterval: Duration, + pollTimeout: Duration, + rebalanceSafeCommits: Boolean +) + +object MessagingServiceConfig { + + val inMemoryLayer: ULayer[MessagingServiceConfig] = + ZLayer.succeed( + MessagingServiceConfig( + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + ConsumerJobConfig(1, None), + 100, + false, + None + ) + ) + +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala new file mode 100644 index 0000000000..94eadf3849 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/Serde.scala @@ -0,0 +1,55 @@ +package org.hyperledger.identus.shared.messaging + +import org.hyperledger.identus.shared.models.WalletId + +import java.nio.charset.StandardCharsets +import java.nio.ByteBuffer +import java.util.UUID + +case class ByteArrayWrapper(ba: Array[Byte]) + +trait Serde[T] { + def serialize(t: T): Array[Byte] + def deserialize(ba: Array[Byte]): T +} + +object Serde { + given byteArraySerde: Serde[ByteArrayWrapper] = new Serde[ByteArrayWrapper] { + override def serialize(t: ByteArrayWrapper): Array[Byte] = t.ba + override def deserialize(ba: Array[Byte]): ByteArrayWrapper = ByteArrayWrapper(ba) + } + + given stringSerde: Serde[String] = new Serde[String] { + override def serialize(t: String): Array[Byte] = t.getBytes() + override def deserialize(ba: Array[Byte]): String = new String(ba, StandardCharsets.UTF_8) + } + + given intSerde: Serde[Int] = new Serde[Int] { + override def serialize(t: Int): Array[Byte] = { + val buffer = java.nio.ByteBuffer.allocate(4) + buffer.putInt(t) + buffer.array() + } + override def deserialize(ba: Array[Byte]): Int = ByteBuffer.wrap(ba).getInt() + } + + given uuidSerde: Serde[UUID] = new Serde[UUID] { + override def serialize(t: UUID): Array[Byte] = { + val buffer = java.nio.ByteBuffer.allocate(16) + buffer.putLong(t.getMostSignificantBits) + buffer.putLong(t.getLeastSignificantBits) + buffer.array() + } + override def deserialize(ba: Array[Byte]): UUID = { + val byteBuffer = ByteBuffer.wrap(ba) + val high = byteBuffer.getLong + val low = byteBuffer.getLong + new UUID(high, low) + } + } + + given walletIdSerde(using uuidSerde: Serde[UUID]): Serde[WalletId] = new Serde[WalletId] { + override def serialize(w: WalletId): Array[Byte] = uuidSerde.serialize(w.toUUID) + override def deserialize(ba: Array[Byte]): WalletId = WalletId.fromUUID(uuidSerde.deserialize(ba)) + } +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala new file mode 100644 index 0000000000..ff1c9e8d76 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/WalletIdAndRecordId.scala @@ -0,0 +1,20 @@ +package org.hyperledger.identus.shared.messaging + +import zio.json.{DecoderOps, DeriveJsonDecoder, DeriveJsonEncoder, EncoderOps, JsonDecoder, JsonEncoder} + +import java.nio.charset.StandardCharsets +import java.util.UUID + +case class WalletIdAndRecordId(walletId: UUID, recordId: UUID) + +object WalletIdAndRecordId { + given encoder: JsonEncoder[WalletIdAndRecordId] = DeriveJsonEncoder.gen[WalletIdAndRecordId] + given decoder: JsonDecoder[WalletIdAndRecordId] = DeriveJsonDecoder.gen[WalletIdAndRecordId] + given ser: Serde[WalletIdAndRecordId] = new Serde[WalletIdAndRecordId] { + override def serialize(t: WalletIdAndRecordId): Array[Byte] = t.toJson.getBytes(StandardCharsets.UTF_8) + override def deserialize(ba: Array[Byte]): WalletIdAndRecordId = + new String(ba, StandardCharsets.UTF_8) + .fromJson[WalletIdAndRecordId] + .getOrElse(throw RuntimeException("Deserialization Error WalletIdAndRecordId")) + } +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala new file mode 100644 index 0000000000..54d8c935c2 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/InMemoryMessagingService.scala @@ -0,0 +1,146 @@ +package org.hyperledger.identus.shared.messaging.kafka + +import org.hyperledger.identus.shared.messaging.* +import org.hyperledger.identus.shared.messaging.kafka.InMemoryMessagingService.* +import zio.* +import zio.concurrent.ConcurrentMap +import zio.stream.* + +import java.util.concurrent.TimeUnit + +case class ConsumerGroupKey(groupId: GroupId, topic: Topic) + +class InMemoryMessagingService( + topicQueues: ConcurrentMap[Topic, (Queue[Message[_, _]], Ref[Offset])], + queueCapacity: Int, + processedMessagesMap: ConcurrentMap[ + ConsumerGroupKey, + ConcurrentMap[Offset, TimeStamp] + ] +) extends MessagingService { + + override def makeConsumer[K, V](groupId: String)(using kSerde: Serde[K], vSerde: Serde[V]): Task[Consumer[K, V]] = { + ZIO.succeed(new InMemoryConsumer[K, V](groupId, topicQueues, processedMessagesMap)) + } + + override def makeProducer[K, V]()(using kSerde: Serde[K], vSerde: Serde[V]): Task[Producer[K, V]] = + ZIO.succeed(new InMemoryProducer[K, V](topicQueues, queueCapacity)) +} + +class InMemoryConsumer[K, V]( + groupId: GroupId, + topicQueues: ConcurrentMap[Topic, (Queue[Message[_, _]], Ref[Offset])], + processedMessagesMap: ConcurrentMap[ConsumerGroupKey, ConcurrentMap[Offset, TimeStamp]] +) extends Consumer[K, V] { + override def consume[HR](topic: String, topics: String*)(handler: Message[K, V] => URIO[HR, Unit]): RIO[HR, Unit] = { + val allTopics = topic +: topics + def getQueueStream(topic: String): ZStream[Any, Nothing, (String, Message[K, V])] = + ZStream.repeatZIO { + topicQueues.get(topic).flatMap { + case Some((queue, _)) => + ZIO.debug(s"Connected to queue for topic $topic in group $groupId") *> + ZIO.succeed(ZStream.fromQueue(queue).collect { case msg: Message[K, V] @unchecked => (topic, msg) }) + case None => + ZIO.sleep(1.second) *> ZIO.succeed(ZStream.empty) + } + }.flatten + + val streams = allTopics.map(getQueueStream) + ZStream + .mergeAllUnbounded()(streams: _*) + .tap { case (topic, msg) => ZIO.log(s"Processing message in group $groupId, topic:$topic : $msg") } + .filterZIO { case (topic, msg) => + for { + currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) + key = ConsumerGroupKey(groupId, topic) + topicProcessedMessages <- processedMessagesMap.get(key).flatMap { + case Some(map) => ZIO.succeed(map) + case None => + for { + newMap <- ConcurrentMap.empty[Offset, TimeStamp] + _ <- processedMessagesMap.put(key, newMap) + } yield newMap + } + isNew <- topicProcessedMessages + .putIfAbsent(Offset(msg.offset), TimeStamp(currentTime)) + .map(_.isEmpty) + } yield isNew + } + .mapZIO { case (_, msg) => handler(msg) } + .tap(_ => ZIO.log(s"Message processed in group $groupId, topic:$topic")) + .runDrain + } +} + +class InMemoryProducer[K, V]( + topicQueues: ConcurrentMap[Topic, (Queue[Message[_, _]], Ref[Offset])], + queueCapacity: Int +) extends Producer[K, V] { + override def produce(topic: String, key: K, value: V): Task[Unit] = for { + queueAndOffsetRef <- topicQueues.get(topic).flatMap { + case Some(qAndOffSetRef) => ZIO.succeed(qAndOffSetRef) + case None => + for { + newQueue <- Queue.sliding[Message[_, _]](queueCapacity) + newOffSetRef <- Ref.make(Offset(0L)) + _ <- topicQueues.put(topic, (newQueue, newOffSetRef)) + } yield (newQueue, newOffSetRef) + } + (queue, offsetRef) = queueAndOffsetRef + currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) + messageId <- offsetRef.updateAndGet(x => Offset(x.value + 1)) // unique atomic id incremented per topic + _ <- queue.offer(Message(key, value, messageId.value, currentTime)) + } yield () +} + +object InMemoryMessagingService { + type Topic = String + type GroupId = String + + opaque type Offset = Long + object Offset: + def apply(value: Long): Offset = value + extension (id: Offset) def value: Long = id + + opaque type TimeStamp = Long + object TimeStamp: + def apply(value: Long): TimeStamp = value + extension (ts: TimeStamp) def value: Long = ts + + val layer: URLayer[MessagingServiceConfig, MessagingService] = + ZLayer.fromZIO { + for { + config <- ZIO.service[MessagingServiceConfig] + queueMap <- ConcurrentMap.empty[Topic, (Queue[Message[_, _]], Ref[Offset])] + processedMessagesMap <- ConcurrentMap.empty[ConsumerGroupKey, ConcurrentMap[Offset, TimeStamp]] + _ <- cleanupTaskForProcessedMessages(processedMessagesMap) + } yield new InMemoryMessagingService(queueMap, config.inMemoryQueueCapacity, processedMessagesMap) + } + + private def cleanupTaskForProcessedMessages( + processedMessagesMap: ConcurrentMap[ConsumerGroupKey, ConcurrentMap[Offset, TimeStamp]], + maxAge: Duration = 60.minutes // Maximum age for entries + ): UIO[Unit] = { + def cleanupOldEntries(map: ConcurrentMap[Offset, TimeStamp]): UIO[Unit] = for { + currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) + entries <- map.toList + _ <- ZIO.foreachDiscard(entries) { case (key, timestamp) => + if (currentTime - timestamp > maxAge.toMillis) + map.remove(key) *> ZIO.log(s"Removed old entry with key: $key and timestamp: $timestamp") + else + ZIO.unit + } + } yield () + + (for { + entries <- processedMessagesMap.toList + _ <- ZIO.foreachDiscard(entries) { case (key, map) => + ZIO.log(s"Cleaning up entries for group: ${key.groupId} and topic: ${key.topic}") *> + cleanupOldEntries(map) + } + } yield ()) + .repeat(Schedule.spaced(10.minutes)) + .fork + .unit + } +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala new file mode 100644 index 0000000000..9180fc4d62 --- /dev/null +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/messaging/kafka/ZKafkaMessagingServiceImpl.scala @@ -0,0 +1,136 @@ +package org.hyperledger.identus.shared.messaging.kafka + +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.common.header.Headers +import org.hyperledger.identus.shared.messaging.* +import zio.{Duration, RIO, Task, URIO, URLayer, ZIO, ZLayer} +import zio.kafka.consumer.{ + Consumer as ZKConsumer, + ConsumerSettings as ZKConsumerSettings, + Subscription as ZKSubscription +} +import zio.kafka.producer.{Producer as ZKProducer, ProducerSettings as ZKProducerSettings} +import zio.kafka.serde.{Deserializer as ZKDeserializer, Serializer as ZKSerializer} + +class ZKafkaMessagingServiceImpl( + bootstrapServers: List[String], + autoCreateTopics: Boolean, + maxPollRecords: Int, + maxPollInterval: Duration, + pollTimeout: Duration, + rebalanceSafeCommits: Boolean +) extends MessagingService { + override def makeConsumer[K, V](groupId: String)(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Consumer[K, V]] = + ZIO.succeed( + new ZKafkaConsumerImpl[K, V]( + bootstrapServers, + groupId, + kSerde, + vSerde, + autoCreateTopics, + maxPollRecords, + maxPollInterval, + pollTimeout, + rebalanceSafeCommits + ) + ) + + override def makeProducer[K, V]()(implicit kSerde: Serde[K], vSerde: Serde[V]): Task[Producer[K, V]] = + ZIO.succeed(new ZKafkaProducerImpl[K, V](bootstrapServers, kSerde, vSerde)) +} + +object ZKafkaMessagingServiceImpl { + val layer: URLayer[MessagingServiceConfig, MessagingService] = + ZLayer.fromZIO { + for { + config <- ZIO.service[MessagingServiceConfig] + kafkaConfig <- config.kafka match + case Some(cfg) => ZIO.succeed(cfg) + case None => ZIO.dieMessage("Kafka config is undefined") + } yield new ZKafkaMessagingServiceImpl( + kafkaConfig.bootstrapServers.split(',').toList, + kafkaConfig.consumers.autoCreateTopics, + kafkaConfig.consumers.maxPollRecords, + kafkaConfig.consumers.maxPollInterval, + kafkaConfig.consumers.pollTimeout, + kafkaConfig.consumers.rebalanceSafeCommits + ) + } +} + +class ZKafkaConsumerImpl[K, V]( + bootstrapServers: List[String], + groupId: String, + kSerde: Serde[K], + vSerde: Serde[V], + autoCreateTopics: Boolean, + maxPollRecords: Int, + maxPollInterval: Duration, + pollTimeout: Duration, + rebalanceSafeCommits: Boolean +) extends Consumer[K, V] { + private val zkConsumer = ZLayer.scoped( + ZKConsumer.make( + ZKConsumerSettings(bootstrapServers) + .withProperty(ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG, autoCreateTopics.toString) + .withGroupId(groupId) + // 'max.poll.records' default is 500. This is a Kafka property. + .withMaxPollRecords(maxPollRecords) + // 'max.poll.interval.ms' default is 5 minutes. This is a Kafka property. + .withMaxPollInterval(maxPollInterval) // Should be max.poll.records x 'max processing time per record' + // 'pollTimeout' default is 50 millis. This is a ZIO Kafka property. + .withPollTimeout(pollTimeout) + // .withOffsetRetrieval(OffsetRetrieval.Auto(AutoOffsetStrategy.Earliest)) + .withRebalanceSafeCommits(rebalanceSafeCommits) + // .withMaxRebalanceDuration(30.seconds) + ) + ) + + private val zkKeyDeserializer = new ZKDeserializer[Any, K] { + override def deserialize(topic: String, headers: Headers, data: Array[Byte]): RIO[Any, K] = + ZIO.succeed(kSerde.deserialize(data)) + } + + private val zkValueDeserializer = new ZKDeserializer[Any, V] { + override def deserialize(topic: String, headers: Headers, data: Array[Byte]): RIO[Any, V] = + ZIO.succeed(vSerde.deserialize(data)) + } + + override def consume[HR](topic: String, topics: String*)(handler: Message[K, V] => URIO[HR, Unit]): RIO[HR, Unit] = + ZKConsumer + .plainStream(ZKSubscription.topics(topic, topics*), zkKeyDeserializer, zkValueDeserializer) + .provideSomeLayer(zkConsumer) + .mapZIO(record => + handler(Message(record.key, record.value, record.offset.offset, record.timestamp)).as(record.offset) + ) + .aggregateAsync(ZKConsumer.offsetBatches) + .mapZIO(_.commit) + .runDrain +} + +class ZKafkaProducerImpl[K, V](bootstrapServers: List[String], kSerde: Serde[K], vSerde: Serde[V]) + extends Producer[K, V] { + private val zkProducer = ZLayer.scoped( + ZKProducer.make( + ZKProducerSettings(bootstrapServers) + ) + ) + + private val zkKeySerializer = new ZKSerializer[Any, K] { + override def serialize(topic: String, headers: Headers, value: K): RIO[Any, Array[Byte]] = + ZIO.succeed(kSerde.serialize(value)) + } + + private val zkValueSerializer = new ZKSerializer[Any, V] { + override def serialize(topic: String, headers: Headers, value: V): RIO[Any, Array[Byte]] = + ZIO.succeed(vSerde.serialize(value)) + } + + override def produce(topic: String, key: K, value: V): Task[Unit] = + ZKProducer + .produce(topic, key, value, zkKeySerializer, zkValueSerializer) + .tap(metadata => ZIO.logInfo(s"Message produced: ${metadata.offset()}")) + .map(_ => ()) + .provideSome(zkProducer) + +} diff --git a/tests/integration-tests/src/test/resources/containers/agent.yml b/tests/integration-tests/src/test/resources/containers/agent.yml index 5d3048eea3..74bf287ada 100644 --- a/tests/integration-tests/src/test/resources/containers/agent.yml +++ b/tests/integration-tests/src/test/resources/containers/agent.yml @@ -42,6 +42,8 @@ services: REST_SERVICE_URL: POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: API_KEY_ENABLED: + STATUS_LIST_SYNC_TRIGGER_RECURRENCE_DELAY: 5 seconds + DID_STATE_SYNC_TRIGGER_RECURRENCE_DELAY: 5 seconds # Secret storage configuration SECRET_STORAGE_BACKEND: VAULT_ADDR: "http://host.docker.internal:${VAULT_HTTP_PORT}" @@ -52,9 +54,13 @@ services: KEYCLOAK_CLIENT_ID: KEYCLOAK_CLIENT_SECRET: KEYCLOAK_UMA_AUTO_UPGRADE_RPT: true # no configurable at the moment + # Kafka Messaging Service + DEFAULT_KAFKA_ENABLED: true depends_on: postgres: condition: service_healthy + init-kafka: + condition: service_healthy ports: - "${AGENT_DIDCOMM_PORT}:${AGENT_DIDCOMM_PORT}" - "${AGENT_HTTP_PORT}:${AGENT_HTTP_PORT}" @@ -72,3 +78,91 @@ services: # Extra hosts for Linux networking extra_hosts: - "host.docker.internal:host-gateway" + zookeeper: + image: confluentinc/cp-zookeeper:latest + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + # ports: + # - 22181:2181 + + kafka: + image: confluentinc/cp-kafka:latest + depends_on: + - zookeeper + # ports: + # - 29092:29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: false + healthcheck: + test: + [ + "CMD", + "kafka-topics", + "--list", + "--bootstrap-server", + "localhost:9092", + ] + interval: 5s + timeout: 10s + retries: 5 + + init-kafka: + image: confluentinc/cp-kafka:latest + depends_on: + kafka: + condition: service_healthy + entrypoint: ["/bin/sh", "-c"] + command: | + " + # blocks until kafka is reachable + kafka-topics --bootstrap-server kafka:9092 --list + echo -e 'Creating kafka topics' + + # Connect + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic connect-DLQ --replication-factor 1 --partitions 1 + + # Issue + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic issue-DLQ --replication-factor 1 --partitions 1 + + # Present + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-1 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-2 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-3 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-retry-4 --replication-factor 1 --partitions 5 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic present-DLQ --replication-factor 1 --partitions 1 + + # DID Publication State Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-did-state --replication-factor 1 --partitions 5 + + # Status List Sync + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic sync-status-list --replication-factor 1 --partitions 5 + + tail -f /dev/null + " + healthcheck: + test: + [ + "CMD-SHELL", + "kafka-topics --bootstrap-server kafka:9092 --list | grep -q 'sync-status-list'", + ] + interval: 5s + timeout: 10s + retries: 5