Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .github/workflows/build_container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ jobs:
echo consents.allowed=true >> obp-api/src/main/resources/props/test.default.props
echo hikari.maximumPoolSize=20 >> obp-api/src/main/resources/props/test.default.props
echo write_metrics=false >> obp-api/src/main/resources/props/test.default.props
# Permissions granted to runtime-compiled dynamic-endpoint code inside the security sandbox
# (mirrors default.props / production.default.props). Required so dynamic resource-doc bodies
# can do JSON extraction (reflection) and read OBP props (getenv); without it the sandbox
# denies these and DynamicResourceDocTest's native-execution scenarios fail.
echo 'dynamic_code_sandbox_permissions=[new java.net.NetPermission("specifyStreamHandler"), new java.lang.reflect.ReflectPermission("suppressAccessChecks"), new java.lang.RuntimePermission("getenv.*"), new java.util.PropertyPermission("cglib.useCache", "read"), new java.util.PropertyPermission("net.sf.cglib.test.stressHashCodes", "read"), new java.util.PropertyPermission("cglib.debugLocation", "read"), new java.lang.RuntimePermission("accessDeclaredMembers"), new java.lang.RuntimePermission("getClassLoader")]' >> obp-api/src/main/resources/props/test.default.props

- name: Run tests — shard ${{ matrix.shard }} (${{ matrix.name }})
run: |
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/build_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ jobs:
# there's no mail server in CI. That surfaces as 500 in any test that
# hits an endpoint triggering the notification (v5 consent flows, etc.).
echo mail.test.mode=true >> obp-api/src/main/resources/props/test.default.props
# Permissions granted to runtime-compiled dynamic-endpoint code inside the security sandbox
# (mirrors default.props / production.default.props). Required so dynamic resource-doc bodies
# can do JSON extraction (reflection) and read OBP props (getenv); without it the sandbox
# denies these and DynamicResourceDocTest's native-execution scenarios fail.
echo 'dynamic_code_sandbox_permissions=[new java.net.NetPermission("specifyStreamHandler"), new java.lang.reflect.ReflectPermission("suppressAccessChecks"), new java.lang.RuntimePermission("getenv.*"), new java.util.PropertyPermission("cglib.useCache", "read"), new java.util.PropertyPermission("net.sf.cglib.test.stressHashCodes", "read"), new java.util.PropertyPermission("cglib.debugLocation", "read"), new java.lang.RuntimePermission("accessDeclaredMembers"), new java.lang.RuntimePermission("getClassLoader")]' >> obp-api/src/main/resources/props/test.default.props

- name: Run tests — shard ${{ matrix.shard }} (${{ matrix.name }})
run: |
Expand Down
15 changes: 15 additions & 0 deletions obp-api/src/main/resources/props/test.default.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,18 @@ allow_public_views =true
# requests + N background queries = 2*N connections needed. Default of 10 is exhausted by
# the 10-thread concurrency tests. Set to 20 to provide headroom.
hikari.maximumPoolSize=20

# Permissions granted to runtime-compiled dynamic-endpoint code inside the security sandbox.
# Mirrors default.props / production.default.props. Required so dynamic resource-doc bodies can do
# JSON extraction (reflection) and read OBP props (getenv); without it the sandbox denies these and
# dynamic-endpoint EXECUTION cannot run (only metadata CRUD / compilation). See DynamicResourceDocTest.
dynamic_code_sandbox_permissions=[\
new java.net.NetPermission("specifyStreamHandler"),\
new java.lang.reflect.ReflectPermission("suppressAccessChecks"),\
new java.lang.RuntimePermission("getenv.*"),\
new java.util.PropertyPermission("cglib.useCache", "read"),\
new java.util.PropertyPermission("net.sf.cglib.test.stressHashCodes", "read"),\
new java.util.PropertyPermission("cglib.debugLocation", "read"),\
new java.lang.RuntimePermission("accessDeclaredMembers"),\
new java.lang.RuntimePermission("getClassLoader")\
]
63 changes: 36 additions & 27 deletions obp-api/src/main/scala/code/api/OBPRestHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -574,37 +574,46 @@ trait OBPRestHelper extends RestHelper with MdcLoggable {
*/

def oauthServe(handler: PartialFunction[Req, CallContext => Box[JsonResponse]], rd: Option[ResourceDoc] = None): Unit = {
val obpHandler : PartialFunction[Req, () => Box[LiftResponse]] = {
new PartialFunction[Req, () => Box[LiftResponse]] {
def apply(r : Req): () => Box[LiftResponse] = {
//check (in that order):
//if request is correct json
//if request matches PartialFunction cases for each defined url
//if request has correct oauth headers
val startTime = Helpers.now
val response = failIfBadAuthorizationHeader(rd) {
failIfBadJSON(r, handler)
}
val endTime = Helpers.now
WriteMetricUtil.writeEndpointMetric(startTime, endTime.getTime - startTime.getTime, rd)
response
serve(buildOAuthHandler(handler, rd))
}

/**
* Build the oauth-wrapped Lift handler that `oauthServe` would otherwise register directly into
* Lift's statelessDispatch. Extracted as a public method so the in-process Lift adapter in
* code.api.dynamic.endpoint.Http4sDynamicEndpoint can construct the exact same wrapped form
* (failIfBadAuthorizationHeader { failIfBadJSON } + endpoint metric) for the dynamic-endpoint
* routes and apply it directly — without registering into statelessDispatch. Behaviour for the
* normal oauthServe path is unchanged (oauthServe now just `serve(buildOAuthHandler(...))`).
*/
def buildOAuthHandler(handler: PartialFunction[Req, CallContext => Box[JsonResponse]], rd: Option[ResourceDoc] = None): PartialFunction[Req, () => Box[LiftResponse]] = {
new PartialFunction[Req, () => Box[LiftResponse]] {
def apply(r : Req): () => Box[LiftResponse] = {
//check (in that order):
//if request is correct json
//if request matches PartialFunction cases for each defined url
//if request has correct oauth headers
val startTime = Helpers.now
val response = failIfBadAuthorizationHeader(rd) {
failIfBadJSON(r, handler)
}
def isDefinedAt(r : Req) = {
//if the content-type is json and json parsing failed, simply accept call but then fail in apply() before
//the url cases don't match because json failed
r.json_? match {
case true =>
//Try to evaluate the json
r.json match {
case Failure(msg, _, _) => true
case _ => handler.isDefinedAt(r)
}
case false => handler.isDefinedAt(r)
}
val endTime = Helpers.now
WriteMetricUtil.writeEndpointMetric(startTime, endTime.getTime - startTime.getTime, rd)
response
}
def isDefinedAt(r : Req) = {
//if the content-type is json and json parsing failed, simply accept call but then fail in apply() before
//the url cases don't match because json failed
r.json_? match {
case true =>
//Try to evaluate the json
r.json match {
case Failure(msg, _, _) => true
case _ => handler.isDefinedAt(r)
}
case false => handler.isDefinedAt(r)
}
}
}
serve(obpHandler)
}

override protected def serve(handler: PartialFunction[Req, () => Box[LiftResponse]]) : Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
case ApiVersion.v1_4_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v1_3_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.`dynamic-entity` => resourceDocs // runtime CRUD now on Http4sDynamicEntity; routes are Nil, skip Lift-route filter
case ApiVersion.`dynamic-endpoint` => resourceDocs // dispatch now on Http4sDynamicEndpoint (proxy + native Piece C); routes carry only the stub, skip Lift-route filter
case ApiVersion.ukOpenBankingV20 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.ukOpenBankingV31 => resourceDocs // fully on http4s — no Lift route filter
case _ => resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import code.DynamicData.{DynamicData, DynamicDataProvider}
import code.api.dynamic.endpoint.helper.{DynamicEndpointHelper, MockResponseHolder}
import code.api.dynamic.endpoint.helper.DynamicEndpointHelper.DynamicReq
import code.api.dynamic.endpoint.helper.MockResponseHolder
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo, EntityName}
import code.api.util.APIUtil._
Expand All @@ -19,7 +18,6 @@
import com.openbankproject.commons.util.{ApiVersion, JsonUtils}
import net.liftweb.common._
import net.liftweb.http.rest.RestHelper
import net.liftweb.http.{JsonResponse, Req}
import net.liftweb.json.JsonAST.JValue
import net.liftweb.json.JsonDSL._
import net.liftweb.json._
Expand Down Expand Up @@ -59,21 +57,46 @@
box.openOrThrowException("impossible error")
}

lazy val dynamicEndpoint: OBPEndpoint = {
case DynamicReq(url, json, method, params, pathParams, role, operationId, mockResponse, bankId) => { cc =>
// process before authentication interceptor, get intercept result
/**
* Framework-neutral proxy logic for a matched dynamic-endpoint, shared by the Lift
* `dynamicEndpoint` handler (below) and the native http4s dispatcher
* (code.api.dynamic.endpoint.Http4sDynamicEndpoint). Runs the before/after authenticate
* interceptors, authentication, the entitlement check, and either the dynamic-entity mapping
* branch or the proxy/mock connector call. Returns the response body JValue paired with the
* HTTP status code carried by the connector/mock result (the Lift handler re-wraps it into a
* CallContext.httpCode; the http4s handler renders the status directly).
*
* The before-authenticate interceptor (which the Lift handler used to short-circuit by
* returning its JsonResponse directly) is reduced here to (message, code) via
* JsonResponseExtractor and re-raised through booleanToFuture, mirroring the after-interceptor
* handling below and Http4sDynamicEntity — same code/message, no Lift JsonResponse rendering.
*/
def proxyHandle(

Check failure on line 74 in obp-api/src/main/scala/code/api/dynamic/endpoint/APIMethodsDynamicEndpoint.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

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

Check warning on line 74 in obp-api/src/main/scala/code/api/dynamic/endpoint/APIMethodsDynamicEndpoint.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function has 10 parameters, which is greater than the 7 authorized.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZ5vbNeNSlnq5GojC7FA&open=AZ5vbNeNSlnq5GojC7FA&pullRequest=2818
url: String,
json: JValue,
method: org.apache.pekko.http.scaladsl.model.HttpMethod,
params: Map[String, List[String]],
pathParams: Map[String, String],
role: ApiRole,
operationId: String,
mockResponse: Option[(Int, JValue)],
bankId: Option[String],
cc: CallContext
): Future[(JValue, Int)] = {
val resourceDoc = DynamicEndpointHelper.doc.find(_.operationId == operationId)
val callContext = cc.copy(operationId = Some(operationId), resourceDocument = resourceDoc)
val beforeInterceptResult: Box[JsonResponse] = beforeAuthenticateInterceptResult(Option(callContext), operationId)
if (beforeInterceptResult.isDefined) beforeInterceptResult
else for {
// process before authentication interceptor; a non-empty result short-circuits (rendered with its own code).
// Computed before the for-comprehension (a for-comprehension cannot begin with an `=` assignment).
val beforeJsonResponse: Box[ErrorMessage] = beforeAuthenticateInterceptResult(Option(callContext), operationId).collect({
case JsonResponseExtractor(message, code) => ErrorMessage(code, message)
})
for {
_ <- Helper.booleanToFuture(failMsg = beforeJsonResponse.map(_.message).orNull, failCode = beforeJsonResponse.map(_.code).openOr(400), cc = Option(callContext)) {
beforeJsonResponse.isEmpty
}
(Full(u), callContext) <- authenticatedAccess(callContext) // Inject operationId into Call Context. It's used by Rate Limiting.
_ <- NewStyle.function.hasEntitlement(bankId.getOrElse(""), u.userId, role, callContext)

// validate request json payload
httpRequestMethod = cc.verb
path = StringUtils.substringAfter(cc.url, DynamicEndpointHelper.urlPrefix)

// process after authentication interceptor, get intercept result
jsonResponse: Box[ErrorMessage] = afterAuthenticateInterceptResult(callContext, operationId).collect({
case JsonResponseExtractor(message, code) => ErrorMessage(code, message)
Expand Down Expand Up @@ -190,7 +213,7 @@
box match {
case Full(v) =>
val code = (v \ "code").asInstanceOf[JInt].num.toInt
(v \ "value", callContext.map(_.copy(httpCode = Some(code))))
(v \ "value", code)

case e: Failure =>
val changedMsgFailure = e.copy(msg = s"$InternalServerError ${e.msg}")
Expand All @@ -199,8 +222,11 @@
}

}
}
}
// The Lift `dynamicEndpoint: OBPEndpoint` (matched by DynamicReq.unapply, returning Box[JsonResponse])
// has been removed: dynamic-endpoint dispatch is fully native (Http4sDynamicEndpoint.proxy calls
// proxyHandle directly), and the resource-doc aggregation no longer filters by Lift route class
// (ResourceDocsAPIMethods now returns the dynamic-endpoint resourceDocs unfiltered, like dynamic-entity).
}
}

Expand Down
Loading
Loading