Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
215ca05
feat: add Java language support to DynamicResourceDoc
hongwei1 Aug 31, 2026
69eaa44
fix: thread programming_lang into dynamic-resource-doc validate endpoint
hongwei1 Aug 31, 2026
2b7a288
Merge branch 'develop-obp' into feature/dynamicresourcedoc-java-support
hongwei1 Aug 31, 2026
75dff04
test: extract duplicated dynamic-resource-docs request literal to a h…
hongwei1 Aug 31, 2026
cd1b548
test: extract shared 401/403/200 role-gate assertion for dynamic reso…
hongwei1 Aug 31, 2026
2a754c7
test: regenerate frozen_type_meta_data.txt to match the updated blob
hongwei1 Aug 31, 2026
3a50efd
fix: make Java dynamic-code dependency validation actually enforce
hongwei1 Aug 31, 2026
6ce2669
test: cover Java dynamic-code dependency validation end-to-end
hongwei1 Aug 31, 2026
9b458b6
fix: don't let a cached compile result skip fresh dependency validation
hongwei1 Sep 1, 2026
a369957
fix: satisfy CI's test-isolation lint for the setPropsValues helper
hongwei1 Sep 1, 2026
b7bef71
fix: decouple dependency-whitelist validation from show_used_connecto…
hongwei1 Sep 1, 2026
eee46ac
fix: register compiled Java bytecode with ClassPool only once per source
hongwei1 Sep 1, 2026
04b1417
fix: fall back to Scala for dynamic resource docs with a NULL lang co…
hongwei1 Sep 1, 2026
759cc82
fix: strict dependency validation unconditionally rejected every Java…
hongwei1 Sep 1, 2026
3276ffd
Merge remote-tracking branch 'origin/develop' into feature/dynamicres…
hongwei1 Sep 1, 2026
84ea41a
Merge remote-tracking branch 'origin/develop' into feature/dynamicres…
hongwei1 Sep 1, 2026
adea5e5
fix: recurse through multi-level same-class calls in Java dependency …
hongwei1 Sep 1, 2026
b745291
fix: reject unsupported programming_lang in the v6 validate endpoint
hongwei1 Sep 1, 2026
0fbd2f5
fix: log a whitelist rejection distinctly from a genuine compile failure
hongwei1 Sep 1, 2026
7b6057f
perf: bind allowedCompilationMethods once per validateDependency call
hongwei1 Sep 1, 2026
6bb5223
fix: fail fast when a compiled Java class's bytecode is missing
hongwei1 Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion obp-api/src/main/scala/code/api/constant/constant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,12 @@
final val CREATE_LOCALISED_RESOURCE_DOC_JSON_TTL: Int = APIUtil.getPropsValue(s"createLocalisedResourceDocJson.cache.ttl.seconds", "3600").toInt
final val GET_DYNAMIC_RESOURCE_DOCS_TTL: Int = APIUtil.getPropsValue(s"dynamicResourceDocsObp.cache.ttl.seconds", "3600").toInt
final val GET_STATIC_RESOURCE_DOCS_TTL: Int = APIUtil.getPropsValue(s"staticResourceDocsObp.cache.ttl.seconds", "3600").toInt
final val SHOW_USED_CONNECTOR_METHODS: Boolean = APIUtil.getPropsAsBoolValue(s"show_used_connector_methods", false)
// def, not final val: DynamicUtil.Validation.validateDependency (dynamic-code dependency
// checking) needs this to react to a props change without a restart -- e.g. test-time
// setPropsValues overrides. A final val here would freeze at whatever value was true the
// moment this object was first touched (typically during server boot, well before any test
// scenario runs), and no later prop override could ever reach it.
def SHOW_USED_CONNECTOR_METHODS: Boolean = APIUtil.getPropsAsBoolValue(s"show_used_connector_methods", false)

Check warning on line 328 in obp-api/src/main/scala/code/api/constant/constant.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "SHOW_USED_CONNECTOR_METHODS" to match the regular expression ^([a-z][a-zA-Z0-9]*+(_[^a-zA-Z0-9]++)?+|[^a-zA-Z0-9]++)$

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AaBdBNysBLFj2WjM7SIf&open=AaBdBNysBLFj2WjM7SIf&pullRequest=2905

// Rate Limiting Cache Prefixes (with global namespace and versioning)
// Both call_counter and rl_active are versioned for consistent cache invalidation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ trait EndpointGroup {
* @param successResponseBody successResponseBody from the post json body,it is JValue here.
* @param methodBody it is url-encoded string for the api level code.
*/
case class CompiledObjects(exampleRequestBody: Option[JValue], successResponseBody: Option[JValue], methodBody: String) {
case class CompiledObjects(exampleRequestBody: Option[JValue], successResponseBody: Option[JValue], methodBody: String, programmingLang: String = "Scala") {
val decodedMethodBody = URLDecoder.decode(methodBody, "UTF-8")
val requestBody: Product = exampleRequestBody match {
//this case means, we accept the empty string "" from json post body, we need to map it to None.
Expand All @@ -87,7 +87,24 @@ case class CompiledObjects(exampleRequestBody: Option[JValue], successResponseBo
}
val successResponse: Product = toCaseObject(successResponseBody)

private val partialFunction: Http4sEndpointIO = {
private val partialFunction: Http4sEndpointIO = programmingLang match {
case "java" | "Java" =>
DynamicUtil.createJavaHttp4sEndpoint(decodedMethodBody) match {
case Full(func) => func
case Failure(msg: String, exception: Box[Throwable], _) =>
throw exception.getOrElse(new RuntimeException(msg))
case _ => throw new RuntimeException("compiled code return nothing")
}
case _ /* "Scala" | "scala" | "" | null, default */ =>
scalaPartialFunction
}

// Unchanged Scala-template compile path, factored out so the `partialFunction` match above stays
// readable. Only evaluated for Scala-language docs (the default) — Java-language docs never
// touch this, so example/response-body JValues that don't fit the Scala case-class generator
// (irrelevant for Java, since it doesn't use RequestRootJsonClass/ResponseRootJsonClass) are a
// non-issue there.
private def scalaPartialFunction: Http4sEndpointIO = {

//If the requestBody is PrimaryDataBody, return None. otherwise, return the exampleRequestBody:Option[JValue]
// In side OBP resourceDoc, requestBody and successResponse must be Product type,
Expand Down Expand Up @@ -157,8 +174,21 @@ case class CompiledObjects(exampleRequestBody: Option[JValue], successResponseBo
* this will check all the dynamic scala code dependencies at compile time.
*
*Search for the usage, you can see how to use it in OBP code.
*
* Scala-only: for the Scala language, `this.partialFunction` IS the compiled user code, so
* validating its bytecode directly is correct. For Java, `this.partialFunction` is instead
* OBP's own Http4sEndpointIO wrapper (built by DynamicUtil.createJavaHttp4sEndpoint) around the
* real compiled Java class -- its bytecode legitimately calls internal OBP helpers
* (DynamicUtil.javaValueToJValue/logger, CustomJsonFormats.formats, JsonAliases.compactRender)
* that were never meant to be dependency-whitelisted, since they are framework glue, not
* user-supplied code. createJavaHttp4sEndpoint already validates the real compiled Java class
* internally (see its own doc comment) before ever returning that wrapper, so re-validating the
* wrapper here is both redundant and wrong -- it would reject every Java doc unconditionally.
*/
def validateDependency() = Validation.validateDependency(this.partialFunction)
def validateDependency() = programmingLang match {
case "java" | "Java" => ()
case _ => Validation.validateDependency(this.partialFunction)
}

/**
* This is used to check the security permission at the run time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ object DynamicResourceDocsEndpointGroup extends EndpointGroup with code.util.Hel
try {
Some(toResourceDoc(dynamicDoc))
} catch {
// Validation.validateDependency / createJavaHttp4sEndpoint's own rejection path both throw
// this specifically for a dependency-whitelist miss -- distinct from a genuine compile
// failure, and reachable here (not just at create/update time) because CompiledObjects'
// validation runs fresh on every construction and dynamic_code_compile_validate_dependencies
// can be tightened after a doc was already registered. Logging it as a "deprecated Lift
// contract" problem sends whoever reads this log to re-author a body that is not the
// problem, instead of at the whitelist they (or someone else) just edited.
case e: code.api.JsonResponseException =>
val reason = e.jsonResponse match {
case APIUtil.JsonResponseExtractor(msg, _) => msg
case _ => Option(e.getMessage).getOrElse("")
}
logger.error(s"[DynamicResourceDocsEndpointGroup] skipping dynamic resource doc '${dynamicDoc.requestVerb} ${dynamicDoc.requestUrl}' " +
s"(id=${dynamicDoc.dynamicResourceDocId.getOrElse("")}, programming_lang=${dynamicDoc.programmingLang}): rejected by dependency " +
s"validation (dynamic_code_compile_validate_dependencies). $reason")
None
case e: Throwable =>
logger.error(s"[DynamicResourceDocsEndpointGroup] skipping dynamic resource doc '${dynamicDoc.requestVerb} ${dynamicDoc.requestUrl}' " +
s"(id=${dynamicDoc.dynamicResourceDocId.getOrElse("")}): its methodBody could not be compiled under the native http4s contract. " +
Expand Down Expand Up @@ -49,7 +65,7 @@ object DynamicResourceDocsEndpointGroup extends EndpointGroup with code.util.Hel
*
*/
private val toResourceDoc: JsonDynamicResourceDoc => ResourceDoc = { dynamicDoc =>
val compiledObjects = CompiledObjects(dynamicDoc.exampleRequestBody, dynamicDoc.successResponseBody, dynamicDoc.methodBody)
val compiledObjects = CompiledObjects(dynamicDoc.exampleRequestBody, dynamicDoc.successResponseBody, dynamicDoc.methodBody, dynamicDoc.programmingLang)
ResourceDoc(
// partialFunction is a no-op stub — the runtime dispatch uses the native handler in
// dynamicHttp4sFunction (the compiled artifact is OBPEndpointIO, not the Lift OBPEndpoint).
Expand Down
6 changes: 4 additions & 2 deletions obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4396,8 +4396,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
*
* than the return value may be (getUserAndSessionContextFuture, ***,***),(map,***,***), (getOrElse,***,***) ......
*/
def getDependentMethods(className: String, methodName:String, signature: String): List[(String, String, String)] = {
if (SHOW_USED_CONNECTOR_METHODS) {
// force bypasses the SHOW_USED_CONNECTOR_METHODS gate below -- see
// DynamicUtil.getDynamicCodeDependentMethods' doc comment for why security validation needs this.
def getDependentMethods(className: String, methodName:String, signature: String, force: Boolean = false): List[(String, String, String)] = {
if (SHOW_USED_CONNECTOR_METHODS || force) {
val methods = ListBuffer[(String, String, String)]()
//NOTE: MEMORY_USER this ctClass will be cached in ClassPool, it may load too many classes into heap.
//eg: className == code.api.UKOpenBanking.v3_1_0.APIMethods_AccountAccessApi$$anonfun$createAccountAccessConsents$lzycompute$1
Expand Down
Loading
Loading