feature/json_schema_validation: implement props config schema

one props example:
json.schema.validation.rules=POST|PUT:/obp/v4.0.0|v3.1.0/banks/BANK_ID/fx
/obp/v4.0.0|v3.1.0/banks/BANK_ID/fx={"$schema":"http://json-schema.org/draft-07/schema","description":"The fix","examples":[{"bank_id":"gh.29.uk","from_currency_code":"EUR","to_currency_code":"USD","conversion_value":1.136305,"inverse_conversion_value":0.8800454103431737,"effective_date":"2017-09-19T00:00:00Z"}],"required":["bank_id","from_currency_code","to_currency_code","conversion_value","inverse_conversion_value","effective_date"],"title":"The Fix schema","type":"object","properties":{"bank_id":{"$id":"#/properties/bank_id","type":"string","title":"The bank_id schema","description":"An explanation about the purpose of this instance.","default":"","examples":["gh.29.uk"]},"from_currency_code":{"$id":"#/properties/from_currency_code","default":"","description":"An explanation about the purpose of this instance.","examples":["EUR"],"title":"The from_currency_code schema","enum":["EUR","YUAN"],"type":"string"},"to_currency_code":{"$id":"#/properties/to_currency_code","default":"","description":"An explanation about the purpose of this instance.","examples":["USD"],"title":"The to_currency_code schema","enum":["USD","YUAN"],"type":"string"},"conversion_value":{"$id":"#/properties/conversion_value","type":"number","title":"The conversion_value schema","description":"An explanation about the purpose of this instance.","default":0.0,"examples":[1.136305]},"inverse_conversion_value":{"$id":"#/properties/inverse_conversion_value","type":"number","title":"The inverse_conversion_value schema","description":"An explanation about the purpose of this instance.","default":0.0,"examples":[0.8800454103431737]},"effective_date":{"$id":"#/properties/effective_date","type":"string","title":"The effective_date schema","description":"An explanation about the purpose of this instance.","default":"","examples":["2017-09-19T00:00:00Z"]}},"additionalProperties":true}
This commit is contained in:
shuang 2020-11-27 08:52:36 +08:00
parent 2b4a14f856
commit eb5a5bc824
3 changed files with 103 additions and 5 deletions

View File

@ -462,6 +462,13 @@
<version>1.7.0</version>
</dependency>
<!--json schema validation start-->
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>1.0.45</version>
</dependency>
<!--json schema validation end-->
</dependencies>
<build>

View File

@ -41,17 +41,21 @@ import code.api.v4_0_0.APIMethods400
import code.model.dataAccess.AuthUser
import code.util.Helper.MdcLoggable
import com.alibaba.ttl.TransmittableThreadLocal
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
import com.networknt.schema.{JsonSchema, JsonSchemaFactory, SpecVersionDetector, ValidationMessage}
import com.openbankproject.commons.model.ErrorMessage
import com.openbankproject.commons.util.{ApiVersion, ReflectUtils, ScannedApiVersion}
import com.openbankproject.commons.util.{ApiVersion, Functions, ReflectUtils, ScannedApiVersion}
import net.liftweb.common._
import net.liftweb.http.rest.RestHelper
import net.liftweb.http.{JsonResponse, LiftResponse, Req, S}
import net.liftweb.http.{BadRequestResponse, JsonResponse, LiftResponse, Req, S}
import net.liftweb.json.Extraction
import net.liftweb.json.JsonAST.JValue
import net.liftweb.util.Helpers
import org.apache.commons.lang3.StringUtils
import scala.collection.immutable.List
import scala.collection.mutable.ArrayBuffer
import scala.jdk.CollectionConverters.iterableAsScalaIterableConverter
import scala.math.Ordering
trait APIFailure{
@ -424,12 +428,98 @@ trait OBPRestHelper extends RestHelper with MdcLoggable {
serve(obpHandler)
}
case class JsonSchemaValidator(methods: Array[String], url: String, schema: String) {
import JsonSchemaValidator._
private val methodSet: Set[String] = methods.map(_.trim.toUpperCase)
.filter(requestMethods.contains(_))
.toSet
assert(methodSet.nonEmpty, s"json schema validation rules contains illegal methods: ${methods.mkString("|")}:$url")
private val jsonSchema: JsonSchema = {
val schemaJson: JsonNode = mapper.readTree(schema)
val factory = JsonSchemaFactory.getInstance(SpecVersionDetector.detect(schemaJson))
factory.getSchema(schemaJson)
}
private val urlPartMatchers: Array[String => Boolean] = {
StringUtils.split(url, '/').map {
case v if v.contains('|') => // or style: v3.1.0|v4.0.0
val parts = StringUtils.split(v, "|")
parts.contains(_:String)
case v if v.forall(it => it < 'a' || it > 'z' ) => // placeholder style: BANK_ID
Functions.truePredicate[String]
case v =>
v == _
}
}
def isRequestMatches(method: String, requestUrl: List[String]): Boolean = {
methodSet.contains(method) &&
requestUrl.size == urlPartMatchers.size && {
urlPartMatchers.zip(requestUrl).forall(it => {
val (fun, v) = it
fun(v)
})
}
}
def validatePayload(jsonContent: Array[Byte]): java.util.Set[ValidationMessage] = {
val zson = mapper.readTree(jsonContent)
jsonSchema.validate(zson)
}
}
object JsonSchemaValidator {
private val mapper = new ObjectMapper
// regex, match this format string: "GET|POST|PUTV:/obp/v4.0.0|v3.1.0/banks/BANK_ID/fx"
// group 1 is http method names, group 2 is url
private val regex = """([|a-zA-Z]+?):([^,]+)""".r
private val requestMethods = Set(
"POST","PUT","DELETE","GET","HEAD","PATCH","TRACE","OPTIONS","CONNECT"
)
// create JsonSchemaValidator with structured string, style like:
// PUT|POST:/obp/v4.0.0|v3.1.0/banks/BANK_ID/fx
def apply(toParseRule: String): JsonSchemaValidator = toParseRule match {
case regex(methods , url) =>
val schemaStr = APIUtil.getPropsValue(url).openOrThrowException(s"key '$url' is not found in props file, it is mandatory for 'json.schema.validation.rules'")
JsonSchemaValidator(StringUtils.split(methods, "|"), url, schemaStr)
case v => throw new RuntimeException(s"props 'json.schema.validation.rules' value contains illegal part: $v")
}
}
private val jsonSchemaRuleMatchers: Array[JsonSchemaValidator] = {
APIUtil.getPropsValue("json.schema.validation.rules") match {
case Full(v) =>
StringUtils.split(v,",").map(JsonSchemaValidator(_))
case _ => Array.empty[JsonSchemaValidator]
}
}
override protected def serve(handler: PartialFunction[Req, () => Box[LiftResponse]]) : Unit = {
val obpHandler : PartialFunction[Req, () => Box[LiftResponse]] = {
new PartialFunction[Req, () => Box[LiftResponse]] {
def apply(r : Req) = {
//Wraps the partial function with some logging
handler(r)
def apply(r : Req): () => Box[LiftResponse] = {
val validationError: Box[String] = for {
body <- r.body
validator <- jsonSchemaRuleMatchers.find(_.isRequestMatches(r.requestType.method, r.path.partPath))
errors: Iterable[ValidationMessage] = validator.validatePayload(body).asScala
if errors.nonEmpty
} yield errors.mkString("; ")
validationError match {
case Full(errorInfo) =>
val errorResponse = s"""{"code":400,"message":"${ErrorMessages.InvalidRequestPayload} $errorInfo"}"""
() => Full(BadRequestResponse(errorResponse))
case _ =>
//Wraps the partial function with some logging
handler(r)
}
}
def isDefinedAt(r : Req) = handler.isDefinedAt(r)
}

View File

@ -56,6 +56,7 @@ object ErrorMessages {
val InvalidMyDynamicEndpointUser = "OBP-09011: DynamicEndpoint can only be updated/deleted by the user who created it. Please try `Update/DELETE Dynamic Endpoint` endpoint"
val InvalidBankIdDynamicEntity = "OBP-09012: This is a bank level dynamic entity. Please specify a valid value for BANK_ID."
val InvalidRequestPayload = "OBP-09013: Request body is invalid json structure."
// General messages (OBP-10XXX)