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)