Skip to content

Commit

Permalink
feat: integrate json-path in presentation definition (#1311)
Browse files Browse the repository at this point in the history
Signed-off-by: Pat Losoponkul <pat.losoponkul@iohk.io>
  • Loading branch information
patlo-iog authored Aug 30, 2024
1 parent 3a7984b commit 9ef6b09
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 20 deletions.
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ lazy val D = new {
val jakartaJson: ModuleID = "org.glassfish" % "jakarta.json" % "2.0.1"
val ironVC: ModuleID = "com.apicatalog" % "iron-verifiable-credentials" % "0.14.0"
val scodecBits: ModuleID = "org.scodec" %% "scodec-bits" % "1.1.38"
val jaywayJsonPath: ModuleID = "com.jayway.jsonpath" % "json-path" % "2.9.0"

// https://mvnrepository.com/artifact/org.didcommx/didcomm/0.3.2
val didcommx: ModuleID = "org.didcommx" % "didcomm" % "0.3.2"
Expand Down Expand Up @@ -200,7 +201,8 @@ lazy val D_SharedJson = new {
D.jakartaJson,
D.ironVC,
D.scodecBits,
D.networkntJsonSchemaValidator
D.networkntJsonSchemaValidator,
D.jaywayJsonPath
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import org.hyperledger.identus.castor.core.service.DIDService
import org.hyperledger.identus.mercury.{AgentPeerService, DidAgent}
import org.hyperledger.identus.mercury.model.DidId
import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation
import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError
import org.hyperledger.identus.pollux.core.model.error.PresentationError
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.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ import org.hyperledger.identus.pollux.core.model.CredentialFormat.{AnonCreds, JW
import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.Role
import org.hyperledger.identus.pollux.core.service.CredentialService
import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext}
import zio.{URLayer, ZIO, ZLayer}
import zio.Duration
import zio.{Duration, URLayer, ZIO, ZLayer}

import scala.language.implicitConversions
class IssueControllerImpl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import io.circe.Json
import munit.*
import org.hyperledger.identus.pollux.prex.*

import scala.language.implicitConversions

class PresentationAttachmentSpec extends ZSuite {

test("Verifier Request Presentation Attachment") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@ import com.networknt.schema.{JsonSchema, SpecVersion}
import io.circe.*
import io.circe.generic.semiauto.*
import io.circe.Json as CirceJson
import org.hyperledger.identus.shared.json.{JsonInterop, JsonSchemaError, JsonSchemaUtils}
import org.hyperledger.identus.shared.json.{JsonInterop, JsonPath, JsonPathError, JsonSchemaError, JsonSchemaUtils}
import zio.*
import zio.json.ast.Json as ZioJson

// TODO: define proper type
type JsonPath = String
opaque type JsonPathValue = String

object JsonPathValue {
given Encoder[JsonPathValue] = Encoder.encodeString
given Decoder[JsonPathValue] = Decoder.decodeString
given Conversion[String, JsonPathValue] = identity

extension (jpv: JsonPathValue) {
def toJsonPath: IO[JsonPathError, JsonPath] = JsonPath.compile(jpv)
def value: String = jpv
}
}

opaque type FieldFilter = ZioJson

Expand All @@ -29,10 +39,11 @@ object FieldFilter {

case class Field(
id: Option[String] = None,
path: Seq[JsonPath] = Seq.empty,
path: Seq[JsonPathValue] = Seq.empty,
name: Option[String] = None,
purpose: Option[String] = None,
filter: Option[FieldFilter] = None
filter: Option[FieldFilter] = None,
optional: Option[Boolean] = None
)

object Field {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package org.hyperledger.identus.pollux.prex

import org.hyperledger.identus.pollux.prex.PresentationDefinitionError.{
InvalidFilterJsonPath,
InvalidFilterJsonSchema,
JsonSchemaOptionNotSupported
}
import org.hyperledger.identus.shared.json.{JsonSchemaError, JsonSchemaValidator, JsonSchemaValidatorImpl}
import org.hyperledger.identus.shared.json.{
JsonPathError,
JsonSchemaError,
JsonSchemaValidator,
JsonSchemaValidatorImpl
}
import org.hyperledger.identus.shared.models.{Failure, StatusCode}
import zio.*

Expand All @@ -15,6 +21,12 @@ sealed trait PresentationDefinitionError extends Failure {
}

object PresentationDefinitionError {
final case class InvalidFilterJsonPath(path: String, error: JsonPathError) extends PresentationDefinitionError {
override def statusCode: StatusCode = StatusCode.BadRequest
override def userFacingMessage: String =
s"PresentationDefinition input_descriptors path '$path' is not a valid JsonPath"
}

final case class InvalidFilterJsonSchema(json: String, error: JsonSchemaError) extends PresentationDefinitionError {
override def statusCode: StatusCode = StatusCode.BadRequest
override def userFacingMessage: String =
Expand Down Expand Up @@ -47,17 +59,28 @@ object PresentationDefinitionValidatorImpl {
class PresentationDefinitionValidatorImpl(filterSchemaValidator: JsonSchemaValidator)
extends PresentationDefinitionValidator {
override def validate(pd: PresentationDefinition): IO[PresentationDefinitionError, Unit] = {
val filters = pd.input_descriptors
val fields = pd.input_descriptors
.flatMap(_.constraints.fields)
.flatten
.flatMap(_.filter)

val paths = fields.flatMap(_.path)
val filters = fields.flatMap(_.filter)

for {
_ <- validateJsonPaths(paths)
_ <- validateFilters(filters)
_ <- validateAllowedFilterSchemaKeys(filters)
} yield ()
}

private def validateJsonPaths(paths: Seq[JsonPathValue]): IO[PresentationDefinitionError, Unit] = {
ZIO
.foreach(paths) { path =>
path.toJsonPath.mapError(InvalidFilterJsonPath(path.value, _))
}
.unit
}

// while we use full-blown json-schema library, we limit the schema optiton
// to make sure verfier don't go crazy on schema causing problem with holder interoperability
// see SDK supported keys https://github.com/hyperledger/identus-edge-agent-sdk-ts/blob/da27890ad4ff3d32576bda8bc99a1185e7239a4c/src/domain/models/VerifiableCredential.ts#L120
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.circe.*
import io.circe.generic.auto.*
import io.circe.parser.*
import org.hyperledger.identus.pollux.prex.PresentationDefinitionError.{
InvalidFilterJsonPath,
InvalidFilterJsonSchema,
JsonSchemaOptionNotSupported
}
Expand Down Expand Up @@ -68,9 +69,6 @@ object PresentationDefinitionValidatorSpec extends ZIOSpecDefault {
pd <- ZIO
.fromEither(decode[ExampleTransportEnvelope](pdJson))
.map(_.presentation_definition)
filters <- ZIO
.succeed(pd.input_descriptors.flatMap(_.constraints.fields.getOrElse(Nil)))
.map(_.flatMap(_.filter))
exit <- validator.validate(pd).exit
} yield assert(exit)(failsWithA[InvalidFilterJsonSchema])
},
Expand Down Expand Up @@ -103,11 +101,39 @@ object PresentationDefinitionValidatorSpec extends ZIOSpecDefault {
pd <- ZIO
.fromEither(decode[ExampleTransportEnvelope](pdJson))
.map(_.presentation_definition)
filters <- ZIO
.succeed(pd.input_descriptors.flatMap(_.constraints.fields.getOrElse(Nil)))
.map(_.flatMap(_.filter))
exit <- validator.validate(pd).exit
} yield assert(exit)(failsWithA[JsonSchemaOptionNotSupported])
},
test("reject when path is not a valid json path") {
val pdJson =
"""{
| "presentation_definition": {
| "id": "32f54163-7166-48f1-93d8-ff217bdb0653",
| "input_descriptors": [
| {
| "id": "wa_driver_license",
| "name": "Washington State Business License",
| "purpose": "We can only allow licensed Washington State business representatives into the WA Business Conference",
| "constraints": {
| "fields": [
| {
| "path": ["$$"]
| }
| ]
| }
| }
| ]
| }
|}
""".stripMargin

for {
validator <- ZIO.service[PresentationDefinitionValidator]
pd <- ZIO
.fromEither(decode[ExampleTransportEnvelope](pdJson))
.map(_.presentation_definition)
exit <- validator.validate(pd).exit
} yield assert(exit)(failsWithA[InvalidFilterJsonPath])
}
)
.provide(PresentationDefinitionValidatorImpl.layer)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.hyperledger.identus.shared.json

import com.jayway.jsonpath.{InvalidPathException, JsonPath as JaywayJsonPath, PathNotFoundException}
import com.jayway.jsonpath.spi.json.JacksonJsonProvider
import org.hyperledger.identus.shared.models.{Failure, StatusCode}
import zio.*
import zio.json.*
import zio.json.ast.Json

sealed trait JsonPathError extends Failure {
override def namespace: String = "JsonPathError"
}

object JsonPathError {
final case class InvalidPathInput(msg: String) extends JsonPathError {
override def statusCode: StatusCode = StatusCode.BadRequest
override def userFacingMessage: String = s"The JsonPath input is not valid: $msg"
}

final case class PathNotFound(path: String) extends JsonPathError {
override def statusCode: StatusCode = StatusCode.BadRequest
override def userFacingMessage: String = s"The json path '$path' cannot be found in a json"
}
}

opaque type JsonPath = JaywayJsonPath

object JsonPath {
def compile(path: String): IO[JsonPathError, JsonPath] = {
ZIO
.attempt(JaywayJsonPath.compile(path))
.refineOrDie {
case e: IllegalArgumentException => JsonPathError.InvalidPathInput(e.getMessage())
case e: InvalidPathException => JsonPathError.InvalidPathInput(e.getMessage())
}
}

extension (jsonPath: JsonPath) {
def read(json: Json): IO[JsonPathError, Json] = {
val jsonProvider = JacksonJsonProvider()
val document = JaywayJsonPath.parse(json.toString())
for {
queriedObj <- ZIO
.attempt(document.read[java.lang.Object](jsonPath))
.refineOrDie { case e: PathNotFoundException =>
JsonPathError.PathNotFound(jsonPath.getPath())
}
queriedJsonStr = jsonProvider.toJson(queriedObj)
queriedJson <- ZIO.fromEither(queriedJsonStr.fromJson[Json]).orDieWith(Exception(_))
} yield queriedJson
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package org.hyperledger.identus.shared.json

import org.hyperledger.identus.shared.json.JsonPathError.{InvalidPathInput, PathNotFound}
import zio.*
import zio.json.*
import zio.json.ast.Json
import zio.test.*
import zio.test.Assertion.*

object JsonPathSpec extends ZIOSpecDefault {

override def spec = suite("JsonPathSpec")(
test("sucessfully compile a valid json path") {
val paths = Seq(
"$.store.book[*].author",
"$..author",
"$.store.*",
"$.store..price",
"$..book[2]",
"$..book[-1:]",
"$..book[0,1]",
"$..book[:2]",
"$..book[?(@.isbn)]",
"$..book[?(@.price<10)]",
"$..book[?(@.price==8.95)]",
"$..book[?(@.price<30 && @.category==\"fiction\")]",
"$..*",
"$['foo']['bar']"
)
ZIO
.foreach(paths)(JsonPath.compile)
.as(assertCompletes)
},
test("do not accept invalid json path") {
val paths = Seq(
"",
" ",
" $ ",
"$$",
"hello world",
)
ZIO
.foreach(paths)(p => JsonPath.compile(p).flip)
.map { errors =>
assert(errors)(forall(isSubtype[InvalidPathInput](anything)))
}
},
test("query valid path inside json structure") {
val jsonStr =
"""{
| "vc": {
| "name": "alice",
| "age": 42,
| "degree": null,
| "pets": ["dog", "cat"],
| "isEmployed": false,
| "languages": {"english": "native", "chinese": "fluent"}
| }
|}
""".stripMargin
for {
json <- ZIO.fromEither(jsonStr.fromJson[Json])
namePath <- JsonPath.compile("$.vc.name")
agePath <- JsonPath.compile("$.vc.age")
degreePath <- JsonPath.compile("$.vc.degree")
petPath <- JsonPath.compile("$.vc.pets")
firstPetPath <- JsonPath.compile("$.vc.pets[0]")
isEmployedPath <- JsonPath.compile("$.vc.isEmployed")
languagesPath <- JsonPath.compile("$.vc.languages")
name <- namePath.read(json)
age <- agePath.read(json)
degree <- degreePath.read(json)
pet <- petPath.read(json)
firstPet <- firstPetPath.read(json)
isEmployed <- isEmployedPath.read(json)
languages <- languagesPath.read(json)
} yield assert(name.asString)(isSome(equalTo("alice")))
&& assert(age.asNumber)(isSome(equalTo(Json.Num(42))))
&& assert(degree.asNull)(isSome(anything))
&& assert(pet.asArray)(isSome(hasSize((equalTo(2)))))
&& assert(firstPet.asString)(isSome(equalTo("dog")))
&& assert(isEmployed.asBoolean)(isSome(isFalse))
&& assert(languages.asObject)(isSome(anything))
},
test("query invalid path inside json structure") {
val jsonStr =
"""{
| "vc": {
| "name": "alice",
| "pets": ["dog", "cat"]
| }
|}
""".stripMargin
for {
json <- ZIO.fromEither(jsonStr.fromJson[Json])
nonExistingPath <- JsonPath.compile("$.vc2.name")
invalidTypeArrayPath <- JsonPath.compile("$.vc.name[0]")
outOfBoundArrayPath <- JsonPath.compile("$.vc.name[5]")
outOfBoundSlicePath <- JsonPath.compile("$.vc.name[1:4]")
exit1 <- nonExistingPath.read(json).exit
exit2 <- invalidTypeArrayPath.read(json).exit
exit3 <- outOfBoundArrayPath.read(json).exit
exit4 <- outOfBoundSlicePath.read(json).exit
} yield assert(exit1)(failsWithA[PathNotFound])
&& assert(exit2)(failsWithA[PathNotFound])
&& assert(exit3)(failsWithA[PathNotFound])
&& assert(exit4)(failsWithA[PathNotFound])
}
)

}

0 comments on commit 9ef6b09

Please sign in to comment.