Skip to content

Refactor/remove obp special endpoints#2816

Open
hongwei1 wants to merge 10 commits into
OpenBankProject:developfrom
hongwei1:refactor/removeObpSpecialEndpoints
Open

Refactor/remove obp special endpoints#2816
hongwei1 wants to merge 10 commits into
OpenBankProject:developfrom
hongwei1:refactor/removeObpSpecialEndpoints

Conversation

@hongwei1
Copy link
Copy Markdown
Contributor

No description provided.

hongwei1 added 6 commits May 27, 2026 23:59
…ve http4s

Replace LiftRules.statelessDispatch registration of OBPAPIDynamicEndpoint
with a dedicated in-process Lift adapter (Http4sDynamicEndpoint) wired into
Http4sApp.baseServices, positioned ahead of the Lift bridge.

Covers both runtime pieces:
- Piece B (proxy): ImplementationsDynamicEndpoint.dynamicEndpoint, matched
  by DynamicReq.unapply and proxied to a backend connector / obp_mock.
- Piece C (runtime-compiled): DynamicEndpoints.dynamicEndpoint, serving
  practise / dynamic-resource-doc endpoints compiled from user Scala via
  DynamicUtil.compileScalaCode[OBPEndpoint].

Piece C compiled artifacts are hardwired to Lift types
(PartialFunction[Req, CallContext => Box[JsonResponse]]) and cannot be
natively rewritten. The adapter runs the exact wrapped form Lift held in
statelessDispatch — routes.map(apiPrefix andThen buildOAuthHandler) —
inside S.init, preserving failIfBadAuthorizationHeader / failIfBadJSON
semantics and endpoint metrics unchanged.

Changes:
- Http4sDynamicEndpoint.scala (new): in-process Lift adapter. Buffers
  body, builds Lift Req via buildLiftReq, enters S.init, collectFirst
  over wrappedRoutes, converts Box[LiftResponse] to http4s Response.
  Catches JsonResponseException (eager failIfBadAuthorizationHeader) and
  ContinuationException (async Lift). No withBusinessDBTransaction wrap
  (dynamic-endpoint wrote on autocommit connections via the bridge too).
- Http4sLiftWebBridge.scala: promote buildLiftReq, liftResponseToHttp4s,
  resolveContinuation from private to public so Http4sDynamicEndpoint
  (different package) can reuse them.
- OBPRestHelper.scala: extract buildOAuthHandler from oauthServe. Returns
  the identical PartialFunction[Req, () => Box[LiftResponse]] without
  registering into statelessDispatch, so the adapter can construct the
  exact wrapped form in-process.
- Http4sApp.scala: add dynamicEndpointRoutes val (gate on
  ApiVersion.dynamic-endpoint) and wire into baseServices orElse chain
  after dynamicEntityRoutes, before Http4sLiftWebBridge.
- APIUtil.scala: comment out LiftRules.statelessDispatch.append for
  ApiVersion.dynamic-endpoint; keep empty case label so it does not
  fall through to the ScannedApiVersion branch.
- OBPAPIDynamicEndpoint.scala: comment out statelessDispatch self-
  registration and OPTIONS serve (CORS now handled globally by
  Http4sApp.corsHandler). Keep object / version / allResourceDocs /
  routes intact (adapter source list + resource-docs aggregation).

Verified: 45 / 45 dynamic regression tests pass on JDK 11
(DynamicEndpointsTest 30, DynamicUtilTest 9, DynamicResourceDocTest 3,
DynamicMessageDocTest 2, DynamicIntegrationTest 1).
Stage 1 of removing the Lift adapter from the dynamic-endpoint dispatch:
the proxy path (DynamicReq-matched requests proxied to a backend connector
or obp_mock) is now served by a native http4s handler instead of building a
Lift Req via buildLiftReq and running inside S.init.

- DynamicEndpointHelper: extract the framework-neutral core of DynamicReq.unapply
  into DynamicReq.resolveProxyTarget(method, partPath, query, body). The Lift
  unapply now delegates to it after its content-type/prefix gate; the native
  dispatcher calls the same method, so both build the identical proxy 9-tuple
  from the same DB lookup (dynamicEndpointInfos / findDynamicEndpoint).
- APIMethodsDynamicEndpoint: extract proxyHandle(...) -> Future[(JValue, Int)],
  the framework-neutral proxy logic (before/after authenticate interceptors,
  authentication, entitlement check, dynamic-entity mapping branch or
  mock/connector proxy). The before interceptor is reduced to (message, code)
  via JsonResponseExtractor + booleanToFuture (mirroring the existing after
  interceptor and Http4sDynamicEntity) instead of returning a Lift JsonResponse
  directly. The Lift dynamicEndpoint handler now delegates to proxyHandle.
- Http4sDynamicEndpoint: add native proxy(req) — builds CallContext via
  Http4sCallContextBuilder, matches via resolveProxyTarget, runs proxyHandle and
  renders the connector/mock status code through the new
  EndpointHelpers.executeFutureWithStatus. Tried ahead of the Lift adapter; a
  non-match falls through to the adapter, which still serves Piece C
  (runtime-compiled endpoints). Proxy writes stay on auto-commit (no
  withBusinessDBTransaction), matching the prior bridge/adapter behaviour.
- Http4sSupport: add EndpointHelpers.executeFutureWithStatus for rendering a
  (result, statusCode) pair with a dynamic HTTP status + metric + error handling.

The mock-response thread-local (MockResponseHolder) is read synchronously by the
connector at Future-construction time, so wrapping the connector call in
MockResponseHolder.init inside proxyHandle preserves behaviour on the cats-effect
thread pool.

Verified on JDK 11: 154 / 154 pass across DynamicEndpointsTest (proxy E2E),
DynamicEndpointHelperTest, ForceError/JsonSchema/AuthenticationType validation
(interceptor regression), DynamicResourceDocTest, DynamicMessageDocTest,
DynamicIntegrationTest, DynamicUtilTest.
…e http4s

Stage 2 (final) of removing the Lift adapter from the dynamic-endpoint dispatch.
The runtime-compiled / practise endpoints are now served natively; Http4sDynamicEndpoint
no longer uses buildLiftReq / liftResponseToHttp4s / S.init / statelessSession at all.

The dynamic-code authoring contract is redefined from Lift to native http4s:
  process(callContext, request: net.liftweb.http.Req, pathParams): Box[JsonResponse]
becomes
  process(callContext, request: org.http4s.Request[IO], pathParams): IO[Response[IO]]
Bodies read the request payload from callContext.httpBody, return errors via the new
errorResponse(msg, code) helper (replacing Full(errorJsonResponse(...))), and may still
yield an OBPReturnType which an injected implicit converts to IO[Response[IO]] (status from
CallContext.httpCode). BREAKING: existing DB-stored methodBody rows written against the Lift
contract no longer compile; DynamicResourceDocsEndpointGroup now isolates a failing row
(log + skip) instead of crashing the group/boot, with a message to re-author against the
native contract.

Changes:
- APIUtil: add type OBPEndpointIO = PartialFunction[Request[IO], CallContext => IO[Response[IO]]]
  (distinct from the Lift OBPEndpoint, which is shared by every static endpoint and unchanged);
  add ResourceDoc.dynamicHttp4sFunction: Option[OBPEndpointIO] = None to carry the compiled
  native handler; add ResourceDoc.matchesPartPath (public form of the wrappedWithAuthCheck URL
  match) and ResourceDoc.authCheckIO (native mirror of wrappedWithAuthCheck's
  auth/obp-id/bank/roles/account/view/counterparty chain, reusing the same predicates + *Fun).
- DynamicCompileEndpoint: process returns IO[Response[IO]]; endpoint is OBPEndpointIO; add the
  OBPReturnType[T] => IO[Response[IO]] implicit and errorResponse helper; run via runInSandboxIO.
- DynamicUtil.Sandbox: add runInSandboxIO — builds the body's IO under the privileged context
  (synchronous construction restricted, matching the Lift path) and evaluates it outside, and
  recovers a NonLocalReturnControl so a `return errorResponse(...)` in template code yields its
  response instead of a 500.
- DynamicEndpoints: native code-generation template (OBPEndpointIO, http4s imports, pathParams
  from request.uri segments); EndpointGroup drops the Lift endpoints/wrapEndpoint; findEndpoint
  now takes Request[IO] and returns the matched ResourceDoc; the Lift dynamicEndpoint is removed.
- DynamicResourceDocsEndpointGroup / PractiseEndpointGroup: carry the compiled handler in
  dynamicHttp4sFunction with partialFunction = dynamicEndpointStub; per-row try/skip for legacy rows.
- PractiseEndpoint / ExampleValue.dynamicResourceDocMethodBodyExample: rewritten to the native
  contract (the body operators copy from).
- Http4sDynamicEndpoint: native pieceC dispatch (findEndpoint -> authCheckIO -> compiled handler
  in sandbox -> IO[Response]); the entire Lift adapter is deleted. Entry is proxy.orElse(pieceC).
- OBPAPIDynamicEndpoint.routes: drop the removed Lift Piece C entry.
- DynamicResourceDocTest: add native-execution E2E scenarios (practise endpoint anonymous;
  create-and-call a runtime-compiled doc — happy path, and no-body 400 exercising the
  NonLocalReturn recovery). These prove the doc RUNS, not just compiles.
- test props (build_pull_request.yml + test.default.props.template): grant the standard
  dynamic_code_sandbox_permissions (reflection / getenv) so dynamic bodies can execute under the
  sandbox in tests, matching default.props / production.default.props.

Verified on JDK 11: 156 / 156 pass across DynamicResourceDocTest (incl. the 2 new E2E),
DynamicEndpointsTest, DynamicEndpointHelperTest, DynamicMessageDocTest, DynamicIntegrationTest,
DynamicUtilTest, and ForceError / JsonSchema / AuthenticationType interceptor regression.
… gate

The native dynamic-endpoint proxy carried an isJsonRequest gate (introduced with
the Piece B migration) that only matched requests whose Content-Type or Accept
literally contained "json". The Lift DynamicReq extractor it replaced gated on
testResponse_?, which treats a wildcard Accept (and an absent Accept) as
JSON-acceptable — so it matched the OBP test client's GET proxy calls (Accept */*,
Content-Type text/plain). The literal check rejected those GETs, so a created
dynamic endpoint called via GET fell through to the Lift bridge and returned 404
(caught by RateLimitingTest's Dynamic Endpoint scenario in the full suite).

Remove the gate entirely: the native dispatch has no XML alternative, and
resolveProxyTarget already returns None for any path that is not a registered
dynamic-endpoint (falling through to Piece C / the chain), so the gate is
unnecessary. POST proxy calls (JSON body) and GET proxy calls now both match.

Verified on JDK 11: RateLimitingTest, DynamicEndpointsTest, DynamicResourceDocTest
all pass (44/44).
Adds a DynamicResourceDocTest scenario that creates a runtime-compiled
dynamic-resource-doc gated by a (system-level) dynamic role and asserts the
native auth chain enforces it: 401 without authentication, 403 when
authenticated but missing the role, 200 once the role is granted. This was the
only branch of ResourceDoc.authCheckIO (the native mirror of wrappedWithAuthCheck
introduced in the Piece C migration) not previously exercised — the existing
native-execution scenarios only covered the no-role/anonymous path.

Verified on JDK 11: DynamicResourceDocTest 6/6 pass.

Note: the proxy entity-mapping branch (isDynamicEntityResponse, in proxyHandle)
is intentionally not given a new HTTP E2E here — it is verbatim-relocated Lift
code (no logic change in the migration), already has an isDynamicEntityResponse
unit test (DynamicEndpointHelperTest) plus mock-branch HTTP coverage
(DynamicEndpointsTest / RateLimitingTest), and the existing example fixtures
(swagger host=obp_mock, mapping referencing unrelated entities) are not aligned
for a clean end-to-end call; a bespoke fixture would be brittle for little gain.
Adds a regression safety net ahead of refactoring the DynamicMessageDoc runtime
mechanism. Two new scenarios in DynamicMessageDocTest:

- 401: the management endpoints (POST/GET/GET-all/PUT/DELETE on
  /management/dynamic-message-docs) reject unauthenticated requests with 401.
  (Previously only the metadata CRUD and role-403 paths were covered.)
- Runtime invoke chain: store a DynamicMessageDoc (Scala methodBody) via
  DynamicMessageDocProvider.create, then call DynamicConnector.invoke and assert
  the stored body is compiled and run, returning the expected object. This covers
  the full DB-stored-methodBody -> invoke -> getFunction -> getByProcess ->
  createFunction (DynamicUtil.compileScalaCode) -> execute path; InternalConnectorTest
  only exercised createFunction+executeFunction in isolation, bypassing the DB and
  invoke/getFunction.

Connector methods do not run inside the security sandbox, so no sandbox-permission
setup is needed; the Scala methodBody is compiled at runtime, which requires JDK 11.

Test-only; no main code changed. The DynamicMessageDoc management endpoints are
already native http4s and the runtime path uses no Lift web layer, so this is a
coverage safety net rather than a migration.

Verified on JDK 11: DynamicMessageDocTest 4/4, InternalConnectorTest, DynamicUtilTest pass.
@hongwei1 hongwei1 closed this May 28, 2026
hongwei1 added 2 commits May 28, 2026 11:22
Cleanup after the dynamic-endpoint/entity native migration. No behaviour change;
removes dead Lift web types that no longer participate in dispatch.

Part A — shared (DynamicUtil sandbox):
- Drop the two NonLocalReturnControl[JsonResponse] catch clauses in
  Sandbox.createSandbox.runInSandbox (now a plain AccessController.doPrivileged) and
  the `import net.liftweb.http.JsonResponse`. The only caller is runInSandboxIO,
  whose forceBodyIO already recovers a NonLocalReturnControl before it reaches
  runInSandbox; connector methods (DynamicMessageDoc) never run inside the sandbox.

Part B — dynamic-endpoint dead Lift refs:
- ResourceDocsAPIMethods: add `case dynamic-endpoint => resourceDocs` to
  activeResourceDocs (mirrors dynamic-entity), so the dynamic-endpoint resource docs
  are returned unfiltered instead of being filtered by Lift route class. This must
  precede removing the routes entry, otherwise the proxy docs would be filtered out.
- APIMethodsDynamicEndpoint: remove the dead Lift `dynamicEndpoint: OBPEndpoint`
  (matched by DynamicReq.unapply) — dispatch is fully native via proxyHandle. Drop the
  now-unused DynamicReq and net.liftweb.http.{JsonResponse, Req} imports.
- DynamicEndpointHelper: remove the dead `DynamicReq.unapply(r: Req)` extractor (its
  only consumer was the removed dynamicEndpoint); keep resolveProxyTarget. DynamicReq
  no longer extends JsonTest with JsonBody. Drop the net.liftweb.http.Req import.
- OBPAPIDynamicEndpoint: routes reduced to List(dynamicEndpointStub); drop the
  net.liftweb.http.{LiftResponse, PlainTextResponse} import (commented-out CORS only).
- Tests: DynamicendPointsTest / ForceErrorValidationTest referenced the removed
  dynamicEndpoint via nameOf for a Tag; kept the tag name as the literal "dynamicEndpoint"
  (same convention already used there for the migrated genericEndpoint).

The RestHelper mixins on DynamicEndpointHelper / APIMethodsDynamicEndpoint are kept
(DynamicEndpointHelper overrides RestHelper's `formats`); removing them is higher-risk
and out of scope for this cleanup.

Verified on JDK 11: 105/105 across DynamicEndpointsTest, DynamicResourceDocTest,
DynamicEndpointHelperTest, DynamicMessageDocTest, DynamicUtilTest, InternalConnectorTest,
ForceErrorValidationTest. Full run_all_tests.sh in progress.
The develop/container CI (build_container.yml) generates test.default.props from
scratch via echo lines and was missing dynamic_code_sandbox_permissions — only
build_pull_request.yml had it. Without those permissions the dynamic-code security
sandbox denies getenv (connector metric prop reads), reflection and
NetPermission("specifyStreamHandler"), so DynamicResourceDocTest's three
native-execution scenarios (practise endpoint, create+call a runtime-compiled doc,
role-gated doc) fail with AccessControlException in shard 1 (v4 only).

Add the same permission list used by build_pull_request.yml / default.props /
production.default.props so dynamic resource-doc bodies can execute under the sandbox
in this workflow too. CI-only change.
@hongwei1 hongwei1 reopened this May 28, 2026
@hongwei1 hongwei1 closed this May 28, 2026
@hongwei1 hongwei1 reopened this May 28, 2026
hongwei1 added 2 commits May 28, 2026 14:20
…ponse format

Add Http4sBGv13PIIS (1 endpoint: POST /funds-confirmations) and Http4sBGv13 aggregator
wired into Http4sApp ahead of the Lift bridge.

Fix ErrorResponseConverter to emit ErrorMessagesBG (tppMessages) format for all
Berlin Group paths, mirroring APIUtil.failedJsonResponse's URL-prefix check. Without
this, BG v1.3 tests that assert tppMessages structure receive the standard OBP
{code,message} format and fail with "head of empty list".
…ttp4s

Also fixes ResourceDocMatcher.apiPrefixPattern to handle Berlin Group paths
(/berlin-group/v1.3/...) in addition to OBP-standard paths (/obp/vX.X.X/...).
Without this fix the middleware could not strip the BG prefix, segment counts
mismatched, findResourceDoc returned None, and auth was bypassed.
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant