Skip to content

Migration and vulnerabilities#2780

Merged
simonredfern merged 34 commits into
OpenBankProject:developfrom
constantine2nd:develop
May 15, 2026
Merged

Migration and vulnerabilities#2780
simonredfern merged 34 commits into
OpenBankProject:developfrom
constantine2nd:develop

Conversation

@constantine2nd
Copy link
Copy Markdown
Contributor

No description provided.

100 of 102 functional endpoints (42 GET, 10 DELETE, 19 POST, 25 PUT,
1 GET-shaped revoke, 3 SCA aliases) on native http4s + path-rewriting
bridge to Http4s300; all 181 v3.1.0 tests pass.

Two endpoints intentionally remain on the Lift bridge and are tracked
in LIFT_HTTP4S_MIGRATION.md "Per-version Lift leftovers":
- getMessageDocsSwagger — absorbed by future Http4sResourceDocs workstream
- getObpConnectorLoopback — deprecated stub that throws NotImplemented

Side-fixes uncovered while making v3.1.0 tests pass:
- ResourceDocMiddleware joins missing roles with " or " (was ", ")
  to match NewStyle.function.hasAtLeastOneEntitlement convention.
- Http4s200.getPrivateAccountsAtOneBank now returns raw List[BasicAccountJSON]
  (JArray), matching Lift; the sibling /accounts/private endpoint still
  wraps in BasicAccountsJSON.

Docs:
- CLAUDE.md gains 11 new gotchas (empty path segments, RuntimeException → 500,
  role check before body parse, DELETE returns 200 vs 204, etc.) and the
  comma-separated -DwildcardSuites recipe for running tests locally.
- LIFT_HTTP4S_MIGRATION.md adds a "Per-version Lift leftovers" tracking
  table and folds getMessageDocsSwagger into the resource-docs workstream
  steps.
CI shard4 (RefreshUserTest) caught a regression my local run missed:
Lift's refreshUser returns 201, my migration used withUser which returns
200. Switched to executeFutureCreated. Both RefreshUserTest scenarios
now pass.

The local test discovery slipped because the suite class
`RefreshUserTest` lives in a file named `RefreshObpDateTest.scala`, and
the basename-based -DwildcardSuites generator I documented in CLAUDE.md
produced `code.api.v3_1_0.RefreshObpDateTest` (no such class).

Replace the basename generator with one that greps each file for its
declared `class X extends ServerSetup` name. Now produces the correct
FQNs even when class and filename diverge. Documented the failure mode
explicitly so the next migration doesn't repeat the trap.
Scaffold Http4s400.scala with staticResourceDocs/resourceDocs split
mirroring APIMethods400 (leaves room for dynamic-doc entries),
v400→v310 path-rewriting bridge, wired into Http4sApp.

Endpoints migrated so far (47):
- Dynamic-entity family (11/11): get/create/update/delete System+BankLevel
  + get/update/delete My. New helper tryOrApiFail converts
  IllegalArgumentException from DynamicEntityCommons.apply validation
  into a JSON-encoded APIFailureNewStyle so the response is 400 with
  the verbatim message (NewStyle.function.tryons would produce a Lift
  Failure chain that doesn't match what tests assert with startWith).
- Dynamic-endpoint family (12/12): create/getList/get/delete System
  +BankLevel/My + updateHost variants. Shared helpers ported from
  APIMethods400 (createDynamicEndpointImpl, updateDynamicEndpointHostImpl,
  getDynamicEndpoint(s)Impl).
- Mainstream batch 1 (7): getMapperDatabaseInfo, getLogoutLink, getBanks,
  getBank, ibanChecker, callsLimit, createBank.
- Override audit batch 1 (5 more): root, getAtms, getAtm, getProducts,
  getProduct, createAtm, createProduct, createProductAttribute,
  updateProductAttribute.

Tests: DynamicEntityTest (21), DynamicEndpointsTest (30),
DynamicEndpointHelperTest (20), DynamicResourceDocTest (3),
DynamicMessageDocTest (2), DynamicIntegrationTest (1), BankTests (3),
BankAttributeTests (11), MapperDatabaseInfoTest (3),
RateLimitingTest (9), AtmsTest (9), ProductTest (4) — all pass.

Discovered & documented: the "bridge-cascade hijack" gotcha. When a
new version overrides an older version's same URL+verb (e.g. v4's
POST /banks adds entitlement granting that v2.2.0's POST /banks
doesn't), the v4 override must be in Http4s400 own-routes before the
bridge cascade runs — otherwise the bridge rewrites the path
(v4.0.0 → v3.1.0 → v3.0.0 → v2.2.0) and the request lands on the
older version's handler. createBank was the first such case.

The collectResourceDocs URL+verb dedup at Lift's level normally
keeps the highest-version handler for each route, which is why the
test passes without any v4 work (full fallthrough to Lift). Once an
HttpXYZ for an in-flight migration is wired in, that "Lift dedup"
protection disappears for routes the bridge intercepts.

CLAUDE.md updated:
- Added the bridge-cascade hijack gotcha with symptom + how to find
  the override set in advance (intersect v4 ResourceDoc URLs+verbs
  with older Http4s files; the Lift excludeEndpoints list is *not*
  the right one — it names removed endpoints, not overrides).

22 v4-over-older overrides remain (user management, customer
endpoints, account/balance/counterparty, consumer/scope, consents,
complex POSTs). Will continue in follow-up commits.
…/account family

Adds v4-native handlers for endpoints that were silently routed to older
versions via the v400→v310→...→v200 bridge cascade. Without these, the
v4-specific behaviour (different JSON shapes, different status codes,
different role checks) was being lost.

Migrated in this commit:
- root (GET /root)
- getAtms (GET /banks/BANK_ID/atms), getAtm (GET .../ATM_ID), createAtm (POST)
- getProducts (GET /banks/BANK_ID/products), getProduct (GET .../PRODUCT_CODE)
- createProduct (PUT), createProductAttribute (POST), updateProductAttribute (PUT)
- getEntitlements (GET /users/USER_ID/entitlements)
- getUserByUserId, getUserByUsername, getUsersByEmail, getUsers
- getCustomersByAttributes (GET /banks/BANK_ID/customers)
- createCustomer (POST /banks/BANK_ID/customers)
- getBankAccountsBalancesForCurrentUser (GET /banks/BANK_ID/balances)
- getCoreAccountById (GET /my/banks/.../account)
- getPrivateAccountByIdFull (GET .../VIEW_ID/account)
- getPrivateAccountsAtOneBank (GET /banks/BANK_ID/accounts)
- createUserCustomerLinks (POST /banks/BANK_ID/user_customer_links)

Progress: 25 of 35 v4-over-older overrides migrated; 10 remain
(counterparties GET/POST, /banks/.../my/consents, getProductAttribute,
scopes GET/POST, /management/consumers POST, createAccountV400, and
answerTransactionRequestChallenge).

Tests passing: AtmsTest (9), ProductTest (4), UserTest (14),
EntitlementTests (9), AccountTest (13), AccountBalanceTest (2),
CustomerTest (12), CustomerAttributesTest (17), UserCustomerLinkTest (9),
plus all earlier suites (BankTests, BankAttributeTests,
MapperDatabaseInfoTest, RateLimitingTest, DynamicEntityTest,
DynamicEndpointsTest, etc).

Notes on the migration:
- Replaced Lift's SS thread-global DSL (which is populated by Lift's
  dispatch wrapper and unavailable in http4s) with direct cc.user /
  cc.bank / cc.bankAccount / cc.view access — middleware already
  populates these fields before the handler runs.
- For getPrivateAccountsAtOneBank, gave explicit types to the tuple
  destructure (List[View], List[code.views.system.AccountAccess])
  because the path-dependent return type of Views.views.vend was
  inferring Any otherwise.
- For createUserCustomerLinks, removed the assert + inline tryons
  pattern in favour of the standard withUserAndBankAndBodyCreated
  helper; the inline isValidID(bankId.value) check stays.
…e role check

Migrated to Http4s400 own-routes:
- getProductAttribute (GET /banks/BANK_ID/products/PRODUCT_CODE/attributes/PRODUCT_ATTRIBUTE_ID)
- getScopes (GET /consumers/CONSUMER_ID/scopes)
- addScope (POST /consumers/CONSUMER_ID/scopes — 201)
- getConsents (GET /banks/BANK_ID/my/consents — returns ConsentsJsonV400
  with api_standard/api_version fields, distinct from v3.1.0's
  ConsentsJsonV310)

ResourceDocMiddleware fix:
ScopesTest scenarios "require_scopes_for_all_roles=true but without scope"
and "require_scopes_for_listed_roles=CanGetAnyUser but without scope"
were failing with 200 when 403 was expected. Root cause:
authorizeRoles was using APIUtil.hasEntitlement(checkBankId, userId, role)
which only checks user entitlements and ignores the
require_scopes_for_all_roles / require_scopes_for_listed_roles
properties. Switched to
APIUtil.handleAccessControlRegardingEntitlementsAndScopes(bankId,
userId, consumerId, roles), the same scope-aware check the Lift
dispatcher uses. The pre-existing bug only surfaced once getUserByUserId
moved from Lift to http4s.

Tests passing: ScopesTest (10/10), ConsentTests (5/5), ProductTest (4),
AccountTest (13), BankTests (33), UserTest (14), CustomerTest (12),
EntitlementTests (9), UserCustomerLinkTest (9), AccountBalanceTest (2),
AtmsTest (9), CustomerAttributesTest (17), RateLimitingTest, plus
Http4s700RoutesTest (111), Http4s500SystemViewsTest (13), and
v3.1.0 ConsentTest/AccountAccessTest — 124 v7/v5/v3.1 tests in total.

Override audit: 29/35 done, 6 remain (createCounterpartyForAnyAccount,
getExplicitCounterpartiesForAccount, getExplicitCounterpartyById,
getFirehoseAccountsAtOneBank, updateAccountLabel, createConsumer,
createTransactionRequestCard, answerTransactionRequestChallenge).
… updateAccountLabel

Migrated to Http4s400 own-routes:
- updateAccountLabel (POST /banks/BANK_ID/accounts/ACCOUNT_ID → 200) — v4
  takes UpdateAccountJsonV400 {label}, distinct from v1.2.1's
  UpdateAccountJSON {id, label, bank_id}; previously hijacked by Http4s121.
- getExplicitCounterpartiesForAccount (GET .../counterparties) — v4
  returns counterpartiesJson400 (with currency field) and uses
  view.allowed_actions for permission check with 403; previously hijacked
  by Http4s220 which returned the v2.2.0 shape (no currency) with 400.
- getExplicitCounterpartyById (GET .../counterparties/COUNTERPARTY_ID) —
  v4 must return 400 (not 404) when counterparty is unknown, matching the
  delete-then-get test that expects 400 after the counterparty is gone.
  Uses EXPLICIT_COUNTERPARTY_ID URL template var (non-standard ALL_CAPS)
  to bypass middleware's 404 validation, then calls
  NewStyle.function.getCounterpartyByCounterpartyId which fails with 400.
- createExplicitCounterparty (POST .../counterparties → 201) — v4 sets
  currency from postJson.currency (v2.2.0 sets ""), validates ISO currency
  code, and returns 403 (not 400) when the view lacks can_add_counterparty.

Tests: CounterpartyTest (7/7), API1_2_1Test (323), AccountTest (13),
BankTests (33), ProductTest (4), AtmsTest (9), UserTest (14),
EntitlementTests (9), ScopesTest (10), ConsentTests (5),
UserCustomerLinkTest (9) — 399 total, all pass.

Override audit: 33/37 done, 4 remain (getFirehoseAccountsAtOneBank,
createConsumer, createTransactionRequestCard,
answerTransactionRequestChallenge).
Symptom: POSTs that fell through to the bridge cascade (v400 → v310 →
v300 → v220 → v210 …) were producing 500 errors with empty bodies in
the downstream handler. TransactionRequestsTest had 21 such failures;
v5_1_0.CounterpartyLimitTest and VRPConsentRequestTest had matching
ones because their POSTs also bridge down to v2.1.0
createTransactionRequest.

Root cause: http4s request bodies are single-shot fs2 streams.
Http4sCallContextBuilder.fromRequest is called once per version's
ResourceDocMiddleware (because the bridge wraps each version's
wrappedRoutes), and the first call drained the stream. Every later
fromRequest in the cascade saw an empty stream and stored
cc.httpBody = None. By the time v2.1.0 createTransactionRequest's
handler read `req.bodyText.compile.string`, the body was gone and
JSON parsing of "" failed inside createTransactionRequestImpl —
which then bubbled out as an uncaught exception → 500.

Fix:
1. Http4sApp.baseServices now pre-reads the body once at the top of
   the chain, stashes the bytes in a new `cachedBodyKey` attribute,
   and rebuilds the request body stream via fs2.Stream.emits so that
   any downstream component that still reads `req.body` (e.g. the
   Lift fallback bridge) gets the same payload.
2. Http4sCallContextBuilder.fromRequest looks up `cachedBodyKey`
   first and short-circuits when present, instead of trying to
   re-drain the stream.
3. ResourceDocMiddleware re-attaches the cached body before calling
   the inner routes so the request keeps the cache through
   middleware → handler transitions.
4. Updated direct callers of `req.bodyText.compile.string`
   (Http4s210, Http4s220, Http4s300) to read from `cc.httpBody`
   instead, since the stream-reading code path is no longer the
   canonical source after the cache lands.

Also: Http4s210 createTransactionRequest's "unknown
transactionRequestType" branch used to throw a plain
RuntimeException, which ErrorResponseConverter mapped to 500. It
now throws an Exception whose message is the JSON-encoded
APIFailureNewStyle with failCode = 400, matching the convention
used elsewhere in the file.

Still outstanding: a few v4 tests (TransactionRequestsTest 21,
v5_1_0 CounterpartyLimitTest 3, VRPConsentRequestTest 3,
FirehoseTest 3, etc.) need the corresponding v4 endpoints
migrated to Http4s400 own-routes so they get v4-specific behavior
(e.g. ACCOUNT/REFUND/SIMPLE/AGENT_CASH_WITHDRAWAL transaction
request types, v4 firehose response shape). Those endpoints are
on the next-batch list — this commit fixes the underlying bridge
cascade so they aren't masked by 500s.

Local regression check: AccountTest (13) + BankTests (3 own) +
CounterpartyTest (7) + ScopesTest (10) + ConsentTests (5) — 38
total, all pass.
`isTemplateVariable` treated every all-caps segment (uppercase letters +
underscore + digits) as a template variable. That collapsed real
literals like SANDBOX_TAN, COUNTERPARTY, SEPA, FREE_FORM, ACCOUNT, CARD
into wildcards, so the v2.1.0 ResourceDoc

  /banks/BANK_ID/accounts/ACCOUNT_ID/GRANT_VIEW_ID/transaction-request-types/SANDBOX_TAN/transaction-requests

matched *any* transaction-request-type URL, including v4-only ACCOUNT
and CARD. The middleware then auth-checked, routed to v2.1.0's handler,
which doesn't know those types — the request was either returned with
the wrong status or 500'd inside v2.1.0's logic, instead of falling
through the bridge cascade to the v4 Lift endpoint that handles it.

Fix: keep the original "looks all-caps → wildcard" rule but exclude an
explicit set of known literals (the 10 trans-req types listed in v4
plus the 4 SCA-method values EMAIL/SMS/IMPLICIT/NOT_EMAIL_NEITHER_SMS).
This preserves the non-standard placeholder convention (NEW_ACCOUNT_ID,
GRANT_VIEW_ID, FIREHOSE_BANK_ID, EXPLICIT_COUNTERPARTY_ID, SYS_VIEW_ID,
SCA_METHOD, TRANSACTION_REQUEST_TYPE, …) without an allow-list.

Also: v2.1.0's createTransactionRequest now guards its route pattern
against unsupported types so the bridge-cascade fall-through is robust
even if a new literal slips past the matcher. The catch-all ResourceDoc
for TRANSACTION_REQUEST_TYPE in v2.1.0 has been removed; the four
type-specific docs (SANDBOX_TAN, COUNTERPARTY, SEPA, FREE_FORM) are
sufficient and the unknown-type branch in createTransactionRequestImpl
now encodes its failure as an APIFailureNewStyle JSON so unreachable
errors map to 400 rather than 500.

Local results:
- TransactionRequestsTest: 21 → 16 failures (5 more pass — the SEPA/
  COUNTERPARTY/FREE_FORM/SANDBOX_TAN scenarios that were previously
  hijacked; the 16 remaining are all CARD or v4-only types that need
  v4's own-route migration).
- v3_1_0.ConsentTest: still 0 failures (SCA_METHOD still works because
  it ends with _METHOD — actually it stays a wildcard because it's not
  in the literal allow-list, and EMAIL/SMS/IMPLICIT are listed
  precisely because they would otherwise fail the matcher).
- v4 BankTests/AccountTest/ScopesTest/ConsentTests + v5 + v7
  Http4s700RoutesTest: 146 tests, all pass.

Outstanding for the remaining 16 TransactionRequestsTest failures plus
v5.1 CounterpartyLimitTest/VRPConsentRequestTest/TransactionRequestTest
and v4 MakerCheckerTransactionRequestTest/FirehoseTest: these all want
v4-specific behaviour the bridge cascade can't deliver because v4 has
no own-route yet. Migrating createTransactionRequest +
answerTransactionRequestChallenge + getFirehoseAccountsAtOneBank to
Http4s400 is the next step.
…nge too

Pair with the earlier route-guard fix on v2.1.0 createTransactionRequest.
answerTransactionRequestChallenge had the same problem: its single
catch-all ResourceDoc used a TRANSACTION_REQUEST_TYPE template variable
that still matched v4-only types (ACCOUNT, ACCOUNT_OTP, REFUND, SIMPLE,
AGENT_CASH_WITHDRAWAL, CARD). The middleware then auth-checked,
discovered the route guard rejected the type, and returned 404 from
`getOrElseF(NotFound)` instead of letting the request fall through.

Fix:
1. Add the same route guard to answerTransactionRequestChallenge so it
   only matches the four v2.1.0-supported types.
2. Replace the single TRANSACTION_REQUEST_TYPE catch-all ResourceDoc
   with four type-specific docs (one per supported type). Unknown
   types now miss the ResourceDoc lookup entirely, the middleware
   returns None, and the bridge cascade walks the request down to the
   Lift fallback where APIMethods400.answerTransactionRequestChallenge
   handles the v4-specific challenge logic (maker-checker,
   ChallengeJsonV400 shape, attribute attachment).

Local results across the relevant trans-req-related suites:
  TransactionRequestsTest, MakerCheckerTransactionRequestTest,
  CounterpartyLimitTest, VRPConsentRequestTest, TransactionRequestTest
  → 43 passing / 15 failing (was 27 failing pre-fix, 21 before the
  matcher fix). The remaining 15 are about challenges not appearing in
  the v4 response body for the test's high-amount scenarios — Lift's
  v4 createTransactionRequest is reached and returns 201, but
  `body.challenges` comes back empty. That's a separate issue
  (presumably a connector/threshold config) not caused by my changes.

Regressions: none.
  AccountTest + BankTests + ScopesTest + ConsentTests +
  v3_1_0 ConsentTest + AccountAttributeTest + Http4s700RoutesTest:
  148 tests, all pass.
GET /banks/BANK_ID/firehose/accounts/views/VIEW_ID was a bridge-hijack
override: v4 returns ModeratedFirehoseAccountsJsonV400 (with `accounts`,
`product_code`, …) but the bridge cascade routed firehose calls to
Http4s300.getFirehoseAccountsAtOneBank which returns
ModeratedCoreAccountsJsonV300. FirehoseTest extracts the v4 shape and
failed with `MappingException: No usable value for accounts / product_code`.

Migration mirrors the v3.0.0 implementation almost line-for-line — same
auth + role check, same bank/view lookup, same firehose-account fetching
and per-account moderation — only the final response uses
`JSONFactory400.createFirehoseCoreBankAccountJSON` which returns the
ModeratedFirehoseAccountsJsonV400 shape.

ResourceDoc keeps `FIREHOSE_BANK_ID` / `FIREHOSE_VIEW_ID` in the URL
template so middleware skips bank/view validation; the in-handler
booleanToFuture checks fire in the order tests expect:
  1. AccountFirehoseNotAllowedOnThisInstance → 400
  2. UserHasMissingRoles (canUseAccountFirehose or *AtAnyBank) → 403
  3. BankNotFound → 404

Local results: FirehoseTest 8/8 pass (3 prior failures resolved),
AccountTest 13/13, BankTests 3/3 — 24 tests, no regressions.

Override audit: 34/37 migrated; 3 remain (createConsumer,
createTransactionRequestCard, and the underlying createTransactionRequest
which needs a bigger refactor to move the SS thread-globals out of
LocalMappedConnectorInternal — for now we rely on the matcher fix +
v2.1.0 route guards to let v4 trans-req types fall through to the Lift
fallback, which works for ACCOUNT/AGENT_CASH_WITHDRAWAL but leaves the
"challenges array empty" test assertions still failing).
Last remaining hijack on the trans-request happy path. Before:
v2.1.0 ResourceDocs for SANDBOX_TAN/COUNTERPARTY/SEPA/FREE_FORM were
literal matches (after the matcher fix), so bridge-cascaded v4 URLs
landed in v2.1.0's handler and returned the v2.1.0 response shape
(missing the v4-only `challenges: List[ChallengeJsonV400]` field). v4-
only types like ACCOUNT/AGENT_CASH_WITHDRAWAL/CARD already fell through
to Lift, but the test still failed for the four overlapping types.

The new route in Http4s400 owns ALL trans-req-type POSTs at v4 (no
guard — also catches unknown types so the test's
"invalidTransactionRequestType" scenario gets 400 from the connector
rather than a 404 fall-through to Lift).

Implementation: delegate to
`LocalMappedConnectorInternal.createTransactionRequest` — the same
helper the Lift `lazy val createTransactionRequestAccount` (and 7
sibling lazy vals) call. The helper reads `SS.user` (Lift thread-
global) on its first line; we initialize it via `APIUtil.SS.init` so
the synchronous read inside the for-comprehension captures the http4s
cc.user before any flatMap. SS.init also needs a `View` instance, but
the connector itself only consumes ViewId, so a `systemView(viewIdStr)`
lookup is enough.

ResourceDoc uses `GRANT_VIEW_ID` (non-standard) so middleware skips
view-access validation. The connector's
`checkAuthorisationToCreateTransactionRequest` returns 400
InsufficientAuthorisationToCreateTransactionRequest if the user has
neither the role nor view permission, matching the Lift v4 behaviour
the test expects.

Local TransactionRequestsTest results:
  Before any of today's work : 21 / 34 failures
  After matcher + body-cache : 16 / 34 failures
  After v210 route guards    :  7 / 34 failures
  After this commit          :  7 / 34 failures (27 pass)

The remaining 7 are all `400 did not equal 202` on the
`answerTransactionRequestChallenge` step of the same flow — v4
challenges for SANDBOX_TAN/COUNTERPARTY/SEPA/FREE_FORM still bridge to
v2.1.0's answer-challenge handler which doesn't recognize the v4
ChallengeAnswerJson400 shape and 400s. Fix needs a similar v4 own-route
for the challenge endpoint; the body is ~280 lines so it's a separate
commit.

Override audit: 36/37 migrated; 1 remains (createConsumer).
The v2.1.0 answer-challenge ResourceDocs (one per of the 4 v2.1.0-
supported types — SANDBOX_TAN/COUNTERPARTY/SEPA/FREE_FORM, added in
77acfe3) were still hijacking the URL via the bridge cascade and
returning v2.1.0's 400 (`InvalidJsonFormat … ChallengeAnswerJSON`)
instead of letting the v4 Lift endpoint handle the v4-shaped
ChallengeAnswerJson400. The full Lift handler is ~280 lines, so
rather than duplicate it we claim the URL at the http4s layer (taking
priority over the bridge cascade) and delegate to
`Http4sLiftWebBridge.dispatch(req)` — which runs Lift's own dispatcher
directly. Lift then picks up v4's `answerTransactionRequestChallenge`
because it's registered first in `OBPAPI4_0_0.routes` via
`endpointsOf4_0_0`.

Local results: code.api.v4_0_0.TransactionRequestsTest — 34/34 pass
(was 7 failing for the four shared types' answer-challenge step).

Caveat: v5.1.0 VRPConsentRequestTest now fails 4 tests with 500 on
trans-request creation. Those tests use `v4_0_0_Request` to POST a
trans-request and now hit the v4 own-route added in b673da1, which
calls `LocalMappedConnectorInternal.createTransactionRequest` for
real — and that triggers `sendCustomerNotification` which tries SMTP
and ConnectException's because no mail server is configured in the
test env. Before b673da1 the URL was bridge-hijacked into v2.1.0's
handler, which doesn't call the notification path, so the SMTP issue
was hidden. The underlying SMTP-failure path needs a graceful fallback
(swallow or log instead of raise) — separate from this commit, since
it's about the notification connector, not routing.

Override audit: 37/37 migrated; 1 remains (createConsumer) — and the
remaining audit entry is the only v4 override left in the bridge-
hijack list.
…ResourceDocMiddleware

Three interceptors used to live in `APIUtil.afterAuthenticateInterceptors`,
fired by Lift's wrapEndpoint between auth and the endpoint body. Once an
endpoint moves to http4s those interceptors stopped running, which made
the corresponding test suites fail in odd ways (500 on Force-Error,
201/500 on the body validators). The same applied to v2.2.0+ endpoints
that http4s now owns — like createFx — because every v4.0.0 test using
those endpoints (via the bridge cascade) sees the migrated handler, not
Lift's interceptor chain.

Port: three new validation steps in `ResourceDocMiddleware.validateOnly`,
each direct ports of the corresponding Lift logic:

1. `processForceError` — opt-in via `enable.force_error` prop. Reads
   `Force-Error` / `Response-Code` headers, validates name format,
   checks the error code is in the ResourceDoc's `errorResponseBodies`,
   then synthesizes the requested error.

2. `validateAuthType` — looks up `AuthenticationTypeValidationProvider`
   by operation id; rejects with 400 if the current request's authType
   isn't in the registered allow-list. Skips anonymous requests
   (they've already passed/failed auth elsewhere).

3. `validateJsonSchema` — looks up the registered JSON schema for this
   endpoint via `JsonSchemaValidationProvider`, validates `cc.httpBody`
   against it, returns 400 with `InvalidRequestPayload + errors` joined
   by `; ` — same prefix the Lift interceptor used so existing
   assertions on `$.from_currency_code: does not have a value …` still
   match.

Order in the validation chain: after authenticate + authorizeRoles,
before bank/account/view validation. That matches Lift's flow (auth
first, then interceptors, then endpoint-specific path validation).

Local results:
- ForceErrorValidationTest: 35 / 35 (was 10 failing)
- AuthenticationTypeValidationTest: 27 / 27 (was 1 failing)
- JsonSchemaValidationTest: 27 / 27 (was 1 failing)
Regression check across 184 tests (Account/Bank/Scopes/Consents/
v3_1 ConsentTest/v7 Http4s700RoutesTest/Firehose/TransactionRequests):
0 failures.

Group 2 done. Remaining: group 3 (v5.1 limits, VRP consent) and the
v5.1 SMTP-side-effect surfacing from group 1 — both require new own-routes
or connector tolerance, not interceptor work.
…e mail.test.mode in CI

Two fixes that together close out the v5.1 VRP / counterparty-limit /
trans-request CI failures.

(1) Http4s400.createTransactionRequest looked up the URL's view via
    `Views.views.vend.systemView(viewId)` only — a hard fail for VRP
    consent flows whose URLs carry a custom `_vrp-…` view id rather
    than a system view name. The lookup threw
    `An Empty Box was opened. The justification was OBP-30005: View
    not found`, surfacing as 500. Fix: fall back to `customView` (the
    account-scoped lookup) when the system view miss. SS.init only
    needs *some* View instance; the connector itself works off the
    ViewId parameter, so the fallback view is purely for thread-global
    plumbing.

(2) The connector's email branch (LocalMappedConnector.
    sendCustomerNotification → CommonsEmailWrapper.sendTextEmail)
    opens a real SMTP socket unless `mail.test.mode=true`. CI has no
    mail server, so it threw ConnectException → 500 on any v5 consent
    flow once group 1 connected those flows to the real connector. The
    CI workflow already generates `test.default.props` line by line in
    a setup step; add `mail.test.mode=true` there.

Local results:
- v5_1_0.CounterpartyLimitTest: 6 / 6 (was 3 failing pre-group-1)
- v5_1_0.VRPConsentRequestTest: 6 / 6 (was 4 failing post-group-1)
- v5_1_0.TransactionRequestTest: 8 / 8 (was 1 failing pre-group-1)
- Full regression across 297 tests (groups 1+2+3 + base v4 + v3.1
  ConsentTest + v7 Http4s700RoutesTest): 0 failures.

Group 3 done.
…REE_FORM doc

The ResourceDoc matcher now treats a fixed set of ALL_CAPS segments
(EMAIL, SMS, IMPLICIT, SANDBOX_TAN, FREE_FORM, ...) as literals rather
than wildcards, because real Lift endpoints register them as literal
SCA-method or transaction-request-type segments.

Two collateral regressions:

1) `GET /users/email/EMAIL/terminator` used EMAIL as a placeholder
   variable. Once EMAIL became literal, the matcher only matched URLs
   whose third segment was the literal string "EMAIL" — the
   "with proper entitlement" scenarios send a real address and missed
   the doc entirely, so middleware skipped auth/role validation and
   the handler 500'd on the empty CallContext. Rename the placeholder
   to USER_EMAIL in both the http4s and Lift declarations so the
   matcher treats it as a wildcard.

2) The v2.1.0 FREE_FORM `createTransactionRequest` ResourceDoc carried
   `Some(List(canCreateAnyTransactionRequest))`. In the Lift handler
   that role is only used to *bypass* view-permission checks inside
   `checkAuthorisationToCreateTransactionRequest`; it is not a
   required entitlement. Once the matcher correctly resolved the
   FREE_FORM doc, the middleware began enforcing the role and tests
   running as the owner-view user got 403 instead of 201. Set the
   role to None and let the inline view check govern access.

All 42 scenarios in v3.0/v4.0 UserTest and v2.1 TransactionRequestsTest
now pass.
Two recurring gotchas surfaced while migrating v3.0/v4.0 getUsersByEmail
and v2.1.0 createTransactionRequest FREE_FORM:

1. The Http4sSupport matcher's `literalAllCapsSegments` set treats names
   like EMAIL, SMS, IMPLICIT, SANDBOX_TAN, FREE_FORM as concrete URL
   segments (because real SCA-method / transaction-request-type endpoints
   register them as literals). Re-using one of those names as a
   placeholder variable in a different endpoint's URL template silently
   breaks the matcher.

2. Some Lift entitlements are bypass conditions inside authorisation
   helpers, not required roles. Copying them into the http4s ResourceDoc
   role list converts them into requirements and locks out view-permission
   callers — http4s middleware enforces doc roles, Lift did not.

Both belong with the existing "Tricky Parts" entries so future
migrations don't re-derive the failure mode.
Completes the v5.0.0 Lift→http4s migration. Http4s500 now serves all 39
v5.0.0 own endpoints (9 prior + 33 new) and cascades inherited v1–v4
endpoints through the v500→v400 path-rewriting bridge instead of falling
through to Http4sLiftWebBridge.

Endpoints migrated (33):

- 12 overrides (had to land before the bridge, else cascade hijack):
  createBank, updateBank, createAccount, createUserAuthContext,
  getUserAuthContexts, createUserAuthContextUpdateRequest,
  answerUserAuthContextUpdateChallenge, createCustomer,
  getCustomersAtOneBank, createProduct, addCardForBank,
  getViewsForBankAccount, getAdapterInfo

- 21 new in v5.0.0:
  - 6 consent-request endpoints (3 SCA-method aliases —
    EMAIL/SMS/IMPLICIT — share the createConsentByConsentRequestId
    handler via a guarded route pattern)
  - 5 customer endpoints (getCustomerOverview, getCustomerOverviewFlat,
    getMyCustomersAtAnyBank, getMyCustomersAtBank,
    getCustomersMinimalAtOneBank)
  - 6 customer-account-link endpoints (CRUD)
  - headAtms, getMetricsAtBank, getSystemViewsIds

createAccount: ResourceDoc keeps Some(List(canCreateAccount)) and
relies on ResourceDocMiddleware enforcement; the inline
"userIdAccountOwner == loggedInUserId" check is preserved as a
no-op safety net mirroring Lift exactly. (Lift's
OBPRestHelper.registerRoutes wraps every endpoint in
wrappedWithAuthCheck, which DOES enforce doc roles — contrary to
the CLAUDE.md "Conditional role check (403)" note. The note should
be revised: bypass roles vs required roles is the real
distinction.)

createConsentByConsentRequestId: the VRP branch's nested for-comp
hits Scala 2.12 type-inference limits when val bindings inside a
deep monadic context interact with an if/else whose branches
return structurally similar but separately inferred types.
Refactor: extract postJson, postCounterpartyLimitV510, and the
inner for-comp into explicit val bindings (vrpFlow:
Future[(BankId, AccountId, ViewId, CounterpartyId)]) and annotate
the else branch's tuple type. Behaviour unchanged.

Bridge cascade now: v500Routes own-routes → v500ToV400Bridge →
(v4 own → v4ToV310Bridge → … → v1.2.1).

Tests: v5.0.0 (85 pass / 13 ignored), v5.1.0 (239 pass), v6.0.0 +
v7.0.0 + http4sbridge (616 pass) — all green.
Three gotchas asserted "Lift never enforced doc roles" — wrong. The truth:
OBPRestHelper.registerRoutes wraps every endpoint in
ResourceDoc.wrappedWithAuthCheck (APIUtil.scala:1780), which enforces
doc roles whenever rolesForCheck.nonEmpty && _autoValidateRoles. So
Lift and ResourceDocMiddleware enforce doc roles the same way for
practically every endpoint.

Caught when v5 createAccount AccountTest's "user2 without role → 403"
scenario failed against my (mis)application of the "Conditional role
check" gotcha — I'd taken canCreateAccount out of the doc on the
theory the inline conditional was the real gate. It wasn't; both
Lift's wrappedWithAuthCheck AND the inline conditional fire, and
Lift's doc enforcement is what makes the test pass. Restoring
Some(List(canCreateAccount)) fixed it.

Edits:

- Add a new top-level note ("Lift DOES enforce ResourceDoc roles")
  citing APIUtil.scala:1780 and explicitly retracting the prior claim,
  so future migrators don't repeat the mistake.

- Narrow "Conditional role check (403)" to genuinely-conditional
  roles (different role for different paths). Add the corollary: if
  the inline check uses the SAME role as the doc, the inline check
  is dead-code-but-harmless — keep both, mirror Lift exactly.

- Rewrite "ResourceDoc role and handler role disagreement": Lift
  enforces both X (doc) and Y (inline). If a test "passed with only
  Y", it's because (a) .disableAutoValidateRoles() was set, (b) the
  doc role was actually different than assumed, or (c) the test
  granted both. The error-message wording is the more common drift
  to watch for.

- Fix "Bypass roles vs required roles": clarify WHY bypass roles
  stay out of the doc — adding them would make Lift enforce them as
  required (since Lift does enforce doc roles), breaking the OR-chain
  intent. The reflex-copy trap is the real warning, not "Lift didn't
  enforce".
… dispatch

Lays the foundation for the v5.1.0 Lift→http4s migration. Wires
v510Routes into Http4sApp.baseServices ahead of v500Routes, with
own-routes only (the v510→v500 path-rewriting bridge is deliberately
left disconnected — see below). Required two infrastructure fixes
along the way:

ResourceDocMiddleware authMode dispatch
---------------------------------------
Mirror Lift's wrappedWithAuthCheck (APIUtil.scala:1783-1788) by
dispatching the auth helper on resourceDoc.authMode:

  ApplicationOnly | UserOrApplication → APIUtil.applicationAccess
  UserOnly | UserAndApplication       → APIUtil.anonymousAccess

Without this every endpoint behaved as UserOnly. v5.1.0 createConsumer
and getConsumers (both UserOrApplication) returned
AuthenticatedUserIsRequired for unauth instead of
ApplicationNotIdentified, breaking the ConsumerTest "We test the
authentication errors" body-content assertion. Also bypass the
"empty user + needsAuth" 401 branch when isAppMode is true, so
consumer-only auth (no User) passes through as Lift does.

Bridge cascade is currently DISABLED for v5.1.0
-----------------------------------------------
While the bulk of the 110 v5.1.0 endpoints (96 are new in v5.1.0,
14 are overrides of older versions) still live in Lift, enabling
the v510→v500 path-rewriting bridge would route unmigrated own
endpoints (e.g. updateConsumerRedirectURL) down through the cascade
to a wrong-version handler or a 404. Without the bridge, unmatched
v5.1.0 URLs fall through to Http4sLiftWebBridge with the original
path intact and Lift's OBPAPI5_1_0 dispatch picks them up — same
behaviour as before this commit. The bridge code is still defined
in Implementations5_1_0.v510ToV500Bridge for the eventual flip.

Migrated overrides (12 of 14)
-----------------------------
- root, getMyConsentsByBank, getAggregateMetrics
- createAtm, updateAtm, deleteAtm
- createConsumer, getConsumer, getConsumers
- getTransactionRequests
- getBankAccountsBalances, getAllBankAccountBalances

Deliberately NOT migrated yet (2 of 14)
---------------------------------------
- getAtms, getAtm: ResponseHeadersTest exercises ETag / If-None-Match /
  If-Modified-Since on /banks/BANK_ID/atms. Lift's response builder
  computes ETag (APIUtil.getRequestHeadersNewStyle:534) and honours
  conditional headers (APIUtil.checkConditionalRequest:471).
  ResourceDocMiddleware doesn't yet do either, so migrating these
  here would regress 4 tests. Leaving them in Lift for now;
  APIMethods510 still has the ResourceDocs registered so resource-docs
  aggregation is unaffected.

Inline notes from the override batch
------------------------------------
- v5.1 PostAtmJsonV510 requires `id`; updateAtm uses AtmJsonV510 and
  takes the id from the URL. The v5.1 atm shape adds atm_type,
  license, opening hours, and attributes — wider than v4 — which is
  why the bridge cascade hijack to Http4s400.getAtms surfaced
  immediately with MappingException: "No usable value for atm_type."
  before this commit.
- getMyConsentsByBank pulls a private rowToConsentInfoJsonV510 from
  APIMethods510. Copied verbatim; no shared factory exists yet.
- getTransactionRequests v5.1 returns TransactionRequestsJsonV510
  (with attributes), not the v4-shape. The "Get Transaction Requests
  with Attributes" scenario hits this path.
- createConsumer / getConsumers (UserOrApplication mode) declare
  Some(List(canCreateConsumer)) / Some(List(canGetConsumers)) in the
  doc and rely on the middleware to enforce them after the new
  authMode dispatch.

Tests
-----
- v5.1.0:    239 pass / 0 fail (full suite)
- v5.0.0:    AccountTest, BankTests, CustomerTest, ConsentRequestTest,
             SystemViewsTests, UserAuthContextTest — all pass
- v6.0.0:    ConsumerTest, SystemViewsTest — pass
- v7.0.0:    Http4s700RoutesTest — pass
- bridge:    Http4sLiftBridgePropertyTest — pass

Total cross-version: 455 tests, 0 failures.

Next session(s): migrate the remaining 96 new v5.1.0 endpoints
(consents family, regulated-entities + attributes, atm-attributes,
agents, customer/user attribute helpers, balance CRUD, view-access,
counterparty-limits, etc.), implement ETag support in the http4s
response wrapper, re-migrate getAtms/getAtm, then enable
v510ToV500Bridge.
Adds the second migration batch on top of the scaffold commit (8cea9b6).
Total v5.1.0 own endpoints now in http4s: 27 (was 12).

Newly migrated (15):
  System / UI / metrics
  - suggestedSessionTimeout (GET /ui/suggested-session-timeout)
  - getOAuth2ServerWellKnown (GET /well-known)
  - waitingForGodot (GET /waiting-for-godot)
  - getAllApiCollections (GET /management/api-collections)
  - updateMyApiCollection (PUT /my/api-collections/API_COLLECTION_ID)
  - getApiTags (GET /tags)
  - getMetrics (GET /management/metrics)
  - getWebUiProps (GET /webui-props)
  - mtlsClientCertificateInfo (GET /my/mtls/certificate/current)
  Regulated entities
  - regulatedEntities, getRegulatedEntityById, createRegulatedEntity,
    deleteRegulatedEntity (already in earlier batch — these stay)
  - createRegulatedEntityAttribute, deleteRegulatedEntityAttribute,
    getRegulatedEntityAttributeById, getAllRegulatedEntityAttributes,
    updateRegulatedEntityAttribute (5 new)
  Log cache (6 — shared logCacheHandler helper for trace/debug/info/warning/error/all)
  ATM attributes (5)
  - createAtmAttribute, getAtmAttributes, getAtmAttribute,
    updateAtmAttribute, deleteAtmAttribute
  Agents (3 of 4)
  - createAgent, getAgent, getAgents

Deferred to Lift (3 of 14 + previous batch's 2)
  - getAtms, getAtm: ETag / If-None-Match / If-Modified-Since handling
    in Lift's response builder (APIUtil.checkConditionalRequest +
    getRequestHeadersNewStyle). ResourceDocMiddleware doesn't yet emit
    ETag or honour conditional headers, so migrating these regresses
    ResponseHeadersTest.
  - updateAgentStatus: AgentTest "wrong Bankid" expects 404 BankNotFound
    for unauthorised user1, which means Lift's wrappedWithAuthCheck
    role check passes here even though user1 lacks
    canUpdateAgentStatusAtAnyBank/canUpdateAgentStatusAtOneBank.
    ResourceDocMiddleware applies the same access-control function
    (with JIT entitlements) and returns 403 — the strict reading of
    the doc roles. Leaving updateAgentStatus in Lift preserves the
    established test contract until the discrepancy is root-caused
    (suspect: per-version Lift environment has additional fixtures).

Tests: 239 v5.1.0 tests pass, 0 failures.

The v510→v500 bridge is still disabled. Re-enable once all 110 v5.1.0
endpoints are migrated, the role-check parity is resolved, and ETag
support lands in the http4s response wrapper.
…rity)

Adds the third migration batch on top of 4229b74. Total v5.1.0 own
endpoints now in http4s: 46.

Newly migrated (19):
  Non-personal user attributes (3)
  - createNonPersonalUserAttribute, deleteNonPersonalUserAttribute,
    getNonPersonalUserAttributes
  User / lock / sync (8)
  - syncExternalUser, getEntitlementsAndPermissions,
    getUserByProviderAndUsername, getUserLockStatus,
    unlockUserByProviderAndUsername, lockUserByProviderAndUsername,
    validateUserByUserId, getAccountAccessByUserId
  Customer helpers (2)
  - getCustomersForUserIdsOnly, getCustomersByLegalName
  System integrity (5)
  - customViewNamesCheck, systemViewNamesCheck,
    accountAccessUniqueIndexCheck, accountCurrencyCheck,
    orphanedAccountCheck
  Currencies (1)
  - getCurrenciesAtBank — note: scope check uses
    `failCode = 403`, since `Helper.booleanToFuture` defaults to 400
    and CurrenciesTest's "without a proper scope" scenario asserts 403.

Deferred (2)
  - getAccountsHeldByUserAtBank / getAccountsHeldByUser depend on
    AccountsHelper.getFilteredCoreAccounts which takes a Lift `Req`.
    Need to port the filter logic before migrating.

Tests: 239 v5.1.0 tests pass, 0 failures.
Adds the fourth migration batch on top of 8919213. Total v5.1.0
own endpoints now in http4s: 58.

Newly migrated (12):
  Consumer mgmt (7)
  - updateConsumerRedirectURL, updateConsumerLogoURL,
    updateConsumerCertificate, updateConsumerName, getCallsLimit,
    createMyConsumer, createConsumerDynamicRegistration
  View access (3)
  - grantUserAccessToViewById, revokeUserAccessToViewById,
    createUserWithAccountAccessById
  Transaction request management (2)
  - getTransactionRequestById, updateTransactionRequestStatus

Tests: 239 v5.1.0 tests pass, 0 failures.
… + perms)

Adds the fifth migration batch on top of 6b27fd3. Total v5.1.0 own
endpoints now in http4s: 76 of 110.

Newly migrated (18):
  View account/balance reads (3)
  - getCoreAccountByIdThroughView
  - getBankAccountBalances
  - getBankAccountsBalancesThroughView
  Counterparty limits (4 of 5)
  - createCounterpartyLimit, updateCounterpartyLimit,
    getCounterpartyLimit, deleteCounterpartyLimit
    (getCounterpartyLimitStatus deferred — 200+ line monthly/yearly
     transaction aggregation, lots of Date arithmetic)
  Custom view CRUD (4)
  - createCustomView (uses withViewCreated for 201),
    updateCustomView (200), getCustomView (200),
    deleteCustomView (uses executeDelete for 204 — middleware
     populates cc.view, cc.bankAccount; reads them inline)
  Bank account balance CRUD (4)
  - createBankAccountBalance, getBankAccountBalanceById,
    updateBankAccountBalance, deleteBankAccountBalance
  System view permissions (2)
  - addSystemViewPermission, deleteSystemViewPermission

Note on createCustomView: original Lift handler returns 201, so used
withViewCreated. Initial draft used withView (returns 200), which
broke the CustomViewTest "We will call the endpoint" assertion.

Tests: 239 v5.1.0 tests pass, 0 failures.

Remaining unmigrated (34): consents family (12), getCounterpartyLimitStatus
(1), getAtms/getAtm (2 — ETag), updateAgentStatus (1 — role-check
parity), getAccountsHeldByUserAtBank/getAccountsHeldByUser (2 —
account-type filter port), plus tail of various other endpoints.
Adds the consents family on top of c791499. Total v5.1.0 own
endpoints now in http4s: 105 of 111 (95%).

Newly migrated (13):
  Read endpoints (5)
  - getMyConsents (GET /my/consents)
  - getConsentsAtBank (GET /management/consents/banks/BANK_ID)
  - getConsents (GET /management/consents)
  - getConsentByConsentId (GET /user/current/consents/CONSENT_ID)
  - getConsentByConsentIdViaConsumer (GET /consumer/current/consents/CONSENT_ID)
  Update / management (3)
  - updateConsentStatusByConsent
    (PUT /management/banks/BANK_ID/consents/CONSENT_ID)
  - updateConsentAccountAccessByConsentId
    (PUT /management/banks/BANK_ID/consents/CONSENT_ID/account-access)
  - updateConsentUserIdByConsentId
    (PUT /management/banks/BANK_ID/consents/CONSENT_ID/created-by-user)
  Revoke (3)
  - revokeConsentAtBank (DELETE /banks/BANK_ID/consents/CONSENT_ID)
  - selfRevokeConsent (DELETE /my/consent/current — pulls Consent-Id
    from request header)
  - revokeMyConsent (DELETE /my/consents/CONSENT_ID — initially missed
    the audit; added as a follow-up after the post-batch endpoint
    inventory found 7 unmigrated rather than 6)
  Create (2)
  - createConsentImplicit (POST /my/consents/IMPLICIT — also handles
    SCA = EMAIL/SMS via the same handler, dispatched by the literal
    URL segment; originally aliased Lift's createConsent)
  - createVRPConsentRequest (POST /consumer/vrp-consent-requests)

createConsentImplicit was the largest port — ~200 lines around
challenge/SCA dispatch, JIT entitlement check, JWT stamping,
skipConsentScaForConsumerIdPairs handling, etc. Mirrors the Lift
shape with one structural simplification: the inner SCA dispatch
loses Lift's early `Future` wrapping of `failMsg` strings (no longer
needed in the http4s for-comp).

Tests: 239 v5.1.0 tests pass, 0 failures (incl. 29 consent-suite
tests across ConsentObpTest, ConsentsTest, VRPConsentRequestTest).

Remaining unmigrated (6 of 111): getAtms / getAtm (ETag),
updateAgentStatus (role-check parity), getAccountsHeldByUserAtBank /
getAccountsHeldByUser (Lift Req filter port), getCounterpartyLimitStatus
(complex monthly/yearly aggregation). Bridge re-enable still pending
on these.
…e checks

Closes the v5.1.0 endpoint migration. Total v5.1.0 own endpoints in
http4s: 111 of 111 (100%). All 239 v5.1.0 tests pass; 447
cross-version tests (v5.0/v6/v7/bridge) pass.

Middleware change: ResourceDocMiddleware bank/roles ordering
-----------------------------------------------------------
Reorder the validation chain to put bank validation BEFORE role
authorization, matching Lift's wrappedWithAuthCheck (APIUtil.scala
line 1934-1969). Lift's own comment explains why:

  "A Bank MUST be checked before Roles. In opposite case we get
  next paradox: We set non existing bank → We get error message
  that we don't have a proper role → We cannot assign the role
  to non existing bank."

This unblocks AgentTest's "wrong Bankid" scenario (PUT
/banks/BANK_ID/agents/agentId for unauthorised user1, expects 404
BankNotFound — previously got 403 from the role check firing first).
447 cross-version tests still pass after the reorder.

Newly migrated endpoints (6)
----------------------------
- updateAgentStatus — now passes the role/bank ordering thanks to
  the middleware reorder above. Same handler shape as before.
- getAccountsHeldByUserAtBank, getAccountsHeldByUser — port
  AccountsHelper.filterWithAccountType inline to operate on http4s
  query params instead of Lift Req. Helper lives as
  Implementations5_1_0.filteredCoreAccountsByQueryParams.
- getCounterpartyLimitStatus — straight 1-for-1 port of the 200-line
  monthly/yearly/total transaction aggregation. Reuses java.time
  for date math, falls back to APIUtil.theEpochTime/ToDateInFuture
  for the all-time bounds.
- getAtms, getAtm — added a private respondWithETag helper that
  inlines the conditional-request flow:
    1. Compute body, then ETag = SHA256(URL + body).
    2. If-None-Match header matches → 304 with ETag header.
    3. If-Modified-Since header → look up MappedETag cache key
       (mirror of APIUtil.checkIfModifiedSinceHeader:390 — same
       composite cache key shape, same async update/create on
       miss) and 304 if cached value is fresh.
    4. Otherwise 200 with ETag header.
  ResponseHeadersTest's 4 scenarios exercise every branch and all
  pass. The helper is inline rather than in Http4sSupport because
  ETag/conditional support is currently single-use; lift to a
  shared helper when the second endpoint needs it.

Bridge state
------------
v510→v500 path-rewriting bridge is DEFINED in
Implementations5_1_0.v510ToV500Bridge but **not wired** into
wrappedRoutesV510Services. With all 111 endpoints migrated the
bridge could in principle be enabled, but doing so surfaces two
cross-version interaction bugs that need root-causing first:

  1. MetricTest's `include_implemented_by_partial_functions=getBanks`
     filter returns 0 instead of 12. Setup calls /obp/v5.1.0/banks
     should be hijacked to v500.getBanks via the bridge, recording
     metrics with partial_function_name=getBanks. Filter doesn't
     match — likely the bridged metric records carry a different
     URL/version field shape that the filter is keying off.
  2. VRPConsentRequestTest's "Create Consent By CONSENT_REQUEST_ID
     (EMAIL)" returns 400 instead of 201 (5 scenarios). The v5.1
     createVRPConsentRequest stores a payload with `consent_type:
     VRP` merged in; the test then calls
     /obp/v5.1.0/consumer/consent-requests/X/EMAIL/consents, which
     the bridge rewrites to v500.createConsentByConsentRequestId.
     That handler tries to parse the payload as
     PostVRPConsentRequestJsonInternalV510 and apparently fails —
     suspect the merged consent_type field changes the JSON shape
     in a way the v500 extractor doesn't tolerate.

Without the bridge, these v5.1.0 URLs fall through Http4sApp's
chain to Http4sLiftWebBridge with the original /obp/v5.1.0/ path,
where Lift's OBPAPI5_1_0 dispatch picks them up via the inherited
APIMethods500 partial functions and serves them correctly. To
re-enable the bridge, append
`.orElse(Implementations5_1_0.v510ToV500Bridge.run(req))` to
wrappedRoutesV510Services and address the two failures above.

Tests: 239 v5.1.0 + 447 cross-version (v5.0/v6/v7/bridge) all green.
… checks

CI shard 1 surfaced ForceErrorValidationTest's "We will call the endpoint
with user credentials" scenario failing — `errorMessage contains
canCreateCustomer.toString() should be (true)` was false. Root cause:
the bank/roles reorder I made in commit b274ce7 (matching Lift's
"bank before roles" rule) left the three after-authenticate
interceptors — Force-Error, AuthType, JsonSchema — running BEFORE
the bank/role checks. That made Force-Error fire first, returning the
canonical error text "OBP-20006: User is missing one or more roles: "
without the role names appended.

Lift's wrappedWithAuthCheck (APIUtil.scala:1934-1969) runs the
afterAuthenticateInterceptors INSIDE the for-comp's yield block, i.e.
only after every checker (auth → bank → roles → account → view →
counterparty) has succeeded. When the natural role check fails first,
the role-check error message — which formats `UserHasMissingRoles +
roles.mkString(" or ")` — short-circuits before the Force-Error
interceptor gets a chance.

Fix: move processForceError / validateAuthType / validateJsonSchema to
the END of the validation chain, after counterparty. Final order now
matches Lift exactly:

  auth → bank → roles → account → view → counterparty
       → processForceError → validateAuthType → validateJsonSchema

ForceErrorValidationTest, MakerCheckerTransactionRequestTest, and
BankAttributeTests all pass locally with the new order. 348-test
broad regression across v5.0/v5.1/v6/v7/bridge also clean.

(MakerCheckerTransactionRequestTest's "Multiple challenges" scenario
was the second CI failure but passes locally — likely existing v4
transaction-request flakiness, not middleware-related.)
constantine2nd and others added 4 commits May 14, 2026 16:02
Single batch covering one Critical, six High, and fourteen Moderate
advisories across the JDBC, crypto, gRPC, HTTP-client, JWT, mail,
log4j, and Elasticsearch stacks. API-compatible bumps throughout;
compile + test-compile clean on both modules; BC, JDBC and http4s
integration smoke suites (24 tests) pass.

- postgresql 42.7.7 -> 42.7.11
- mysql-connector-j 8.1.0 -> 9.4.0
- bcpg-jdk18on, bcpkix-jdk18on 1.78.1 -> 1.81
- com.sun.mail:jakarta.mail:2.0.1 -> org.eclipse.angus:jakarta.mail:2.0.5
  (com.sun line abandoned at 2.0.2 still vulnerable; identical
  jakarta.mail.* API surface, no source changes needed)
- grpc-{netty-shaded,protobuf,stub,services} 1.48.1 -> 1.71.0
  (closes Netty MadeYouReset HTTP/2 DDoS via newer shaded Netty)
- nimbus-jose-jwt 9.37.2 -> 9.48 (latest 9.x; defers 10.x API migration)
- async-http-client 2.10.4 -> 2.15.0 (latest 2.x; 3.x would be a rewrite)
- log4j-api, log4j-core 2.24.3 -> 2.26.0 (pin still required to override
  ES's transitive 2.19.0; bumped past the 2.24 Rfc5424/XmlLayout/TLS-host
  advisories)
- elasticsearch, elasticsearch-rest-client 8.14.0 -> 8.19.15
- elastic4s-client-esjava 8.5.2 -> 8.11.5
…erts

Dependabot rescan after d4c0af7 found that 1.81 / 1.71.0 / 9.48 were
not high enough — the actual minimum-patched versions are later. Picks
below follow the OSV "fixed" ranges directly.

- bcpg-jdk18on, bcpkix-jdk18on 1.81 -> 1.84 (uncontrolled resource
  consumption + risky-algo fixes only land in 1.84)
- grpc-{netty-shaded,protobuf,stub,services} 1.71.0 -> 1.75.0 (shaded
  Netty's MadeYouReset HTTP/2 DDoS fix lands at grpc 1.75.0)
- nimbus-jose-jwt 9.48 -> 10.5 (the 9.x branch past 9.37.4 is
  unmaintained and remains vulnerable to deeply-nested-JSON DoS;
  10.0.2+ has the fix)
- oauth2-oidc-sdk 9.27 -> 11.37.1 (paired with nimbus 10 — sdk 9.x was
  compiled against nimbus-jose-jwt 9.x and would NoSuchMethodError at
  runtime against nimbus 10; 11.x is the matching major)
- commons-beanutils 1.10.1 -> 1.11.0 (Improper Access Control)

CertificateUtil.scala: replaced wildcard `import com.nimbusds.jose._`
with explicit imports — nimbus 10 added a `com.nimbusds.jose.Option`
class which shadowed `scala.Option` in this file.

Verified: PasswordResetTest (JWT/Nimbus 10.5), RegulatedEntityTest
(BC 1.84), Http4sServerIntegrationTest, Http4s700TransactionTest —
41 tests pass.
The `import com.nimbusds.jose._` at line 265 was functionally dead —
every JOSE type referenced in the method (JOSEException, BadJOSEException,
JWSAlgorithm, JWSVerificationKeySelector, SecurityContext) is imported
explicitly elsewhere in the file. Same hazard the CertificateUtil fix
addressed: under Nimbus 10 the wildcard pulls in `com.nimbusds.jose.Option`
which shadows `scala.Option`, so any future Option[X] usage in this method
would silently break.
@sonarqubecloud
Copy link
Copy Markdown

@simonredfern simonredfern merged commit 4c93b42 into OpenBankProject:develop May 15, 2026
2 checks passed
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.

2 participants