diff --git a/obp-api/pom.xml b/obp-api/pom.xml
index 5a9d684e5..e17c987db 100644
--- a/obp-api/pom.xml
+++ b/obp-api/pom.xml
@@ -462,6 +462,13 @@
1.7.0
+
+
+ com.networknt
+ json-schema-validator
+ 1.0.45
+
+
diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala
index 5e0b7a803..c328189dc 100644
--- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala
+++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala
@@ -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)
}
diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
index 5835197bd..c6e682ea9 100644
--- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
+++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
@@ -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)