Skip to content

Commit 2e307de

Browse files
committed
Merge remote-tracking branch 'Hongwei/develop-obp' into develop-obp
# Conflicts: # obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala
2 parents cd1a5ee + 17faa09 commit 2e307de

7 files changed

Lines changed: 86 additions & 20 deletions

File tree

obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ object RateLimitingUtil extends MdcLoggable {
265265
}
266266

267267
private def incrementConsumerCounters(consumerKey: String, period: LimitCallPeriod, limit: Long): (Long, Long) = {
268-
if (useConsumerLimits) {
268+
if (useConsumerLimits && limit > 0) {
269269
incrementCounter(createUniqueKey(consumerKey, period), period)
270270
} else {
271271
(-1, -1)

obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ object JSONFactory1_4_0 extends MdcLoggable{
539539
jsonRequestBodyFieldsI18n:String,
540540
jsonResponseBodyFieldsI18n:String
541541
): ResourceDocJson = {
542-
val cacheKey = LOCALISED_RESOURCE_DOC_PREFIX + s"operationId:${operationId}-locale:$locale- isVersion4OrHigher:$isVersion4OrHigher- includeTechnology:$includeTechnology".intern()
542+
val cacheKey = LOCALISED_RESOURCE_DOC_PREFIX + s"operationId:${operationId}-locale:$locale- isVersion4OrHigher:$isVersion4OrHigher- includeTechnology:$includeTechnology-specifiedUrl:${resourceDocUpdatedTags.specifiedUrl.getOrElse("")}".intern()
543543
Caching.memoizeSyncWithImMemory(Some(cacheKey))(CREATE_LOCALISED_RESOURCE_DOC_JSON_TTL.seconds) {
544544
val fieldsDescription =
545545
if (resourceDocUpdatedTags.tags.toString.contains("Dynamic-Entity")

obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import code.api.util.http4s.{ErrorResponseConverter, RequestScopeConnection, Res
1313
import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps}
1414
import code.api.util.newstyle.ViewNewStyle
1515
import code.api.v2_0_0.JSONFactory200
16+
import code.api.v5_0_0.Http4s500
1617
import code.api.v5_1_0.{Http4s510, JSONFactory510}
1718
import code.api.v6_0_0.JSONFactory600.ScannedApiVersionJsonV600
1819
import code.accountattribute.AccountAttributeX
@@ -80,12 +81,11 @@ import scala.concurrent.Future
8081
* v6.0.0 http4s endpoints — Phase 1 in progress.
8182
*
8283
* Wire-in into `Http4sApp.baseServices` is performed alongside this object.
83-
* The v600→v510 bridge (`v600ToV510Bridge`) is intentionally NOT appended to
84-
* `allRoutes`: unmigrated v6 paths must fall through the http4s chain to the
85-
* Lift fallback, which still serves the v6 Lift handlers. Adding the bridge
86-
* would let v6 *overrides* be hijacked into v5.1 handlers (CLAUDE.md →
87-
* "Bridge-cascade hijack"). The bridge val is kept here so it can be enabled
88-
* later if the team decides to short-circuit Lift for v6 originals.
84+
* The v600→v500 bridge (`v600ToV500Bridge`) rewrites unhandled v6.0.0 paths
85+
* to v5.0.0 and delegates to Http4s500.wrappedRoutesV500Services, which has a
86+
* working cascade chain (v5.0.0 → v4.0.0 → v3.1.0 → v3.0.0). The bridge
87+
* skips v5.1.0 because Http4s510's own bridge to v5.0.0 is disabled due to
88+
* MetricTest / VRPConsentRequestTest regressions.
8989
*/
9090
object Http4s600 {
9191

@@ -1790,6 +1790,7 @@ object Http4s600 {
17901790
.orElse(getConnectorTraces(req))
17911791
.orElse(getDynamicEntityDiagnostics(req))
17921792
.orElse(cleanupOrphanedDynamicEntityRecords(req))
1793+
.orElse(createWebUiProps(req))
17931794
.orElse(createOrUpdateWebUiProps(req))
17941795
.orElse(deleteWebUiProps(req))
17951796
.orElse(createCustomViewManagement(req))
@@ -2249,6 +2250,36 @@ object Http4s600 {
22492250
Some(canCleanupOrphanedDynamicEntityRecords :: Nil),
22502251
http4sPartialFunction = Some(cleanupOrphanedDynamicEntityRecords))
22512252

2253+
// POST /obp/v6.0.0/management/webui_props
2254+
lazy val createWebUiProps: HttpRoutes[IO] = HttpRoutes.of[IO] {
2255+
case req @ POST -> `prefixPath` / "management" / "webui_props" =>
2256+
EndpointHelpers.withUserAndBodyCreated[WebUiPropsCommons, Any](req) { (user, postedData, cc) =>
2257+
for {
2258+
_ <- NewStyle.function.hasEntitlement("", user.userId, canCreateWebUiProps, Some(cc))
2259+
_ <- NewStyle.function.tryons(
2260+
s"""$InvalidWebUiProps name must be start with webui_, but current post name is: ${postedData.name} """,
2261+
400, Some(cc)) { require(postedData.name.startsWith("webui_")) }
2262+
webUiProps <- Future(MappedWebUiPropsProvider.createOrUpdate(postedData)) map {
2263+
unboxFullOrFail(_, Some(cc))
2264+
}
2265+
} yield (webUiProps: WebUiPropsCommons)
2266+
}
2267+
}
2268+
2269+
resourceDocs += ResourceDoc(
2270+
null, implementedInApiVersion, nameOf(createWebUiProps), "POST",
2271+
"/management/webui_props",
2272+
"Create WebUiProps",
2273+
s"""Create a WebUiProps.
2274+
|
2275+
|${APIUtil.userAuthenticationMessage(true)}
2276+
|""",
2277+
WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com"),
2278+
WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com", Some("web-ui-props-id")),
2279+
List($AuthenticatedUserIsRequired, UserHasMissingRoles, InvalidJsonFormat, UnknownError),
2280+
List(apiTagWebUiProps), Some(List(canCreateWebUiProps)),
2281+
http4sPartialFunction = Some(createWebUiProps))
2282+
22522283
// PUT /obp/v6.0.0/management/webui_props/WEBUI_PROP_NAME
22532284
lazy val createOrUpdateWebUiProps: HttpRoutes[IO] = HttpRoutes.of[IO] {
22542285
case req @ PUT -> `prefixPath` / "management" / "webui_props" / webUiPropName =>
@@ -8474,15 +8505,18 @@ object Http4s600 {
84748505
val allRoutesWithMiddleware: HttpRoutes[IO] =
84758506
ResourceDocMiddleware.apply(resourceDocs)(allRoutes)
84768507

8477-
// ─── path-rewriting bridge: /obp/v6.0.0/… → /obp/v5.1.0/… ─────────────
8508+
// ─── path-rewriting bridge: /obp/v6.0.0/… → /obp/v5.0.0/… ─────────────
8509+
// Targets v5.0.0 (not v5.1.0) because Http4s510's bridge to v5.0.0 is
8510+
// disabled (MetricTest / VRPConsentRequestTest regressions). Http4s500 has
8511+
// its own working cascade: v5.0.0 → v4.0.0 → v3.1.0 → v3.0.0.
84788512
// NOT appended to allRoutes — see object-level scaladoc.
8479-
val v600ToV510Bridge: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req =>
8513+
val v600ToV500Bridge: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req =>
84808514
val rawPath = req.uri.path.renderString
84818515
if (rawPath.startsWith("/obp/v6.0.0/")) {
8482-
val rewritten = rawPath.replaceFirst("/obp/v6\\.0\\.0/", "/obp/v5.1.0/")
8516+
val rewritten = rawPath.replaceFirst("/obp/v6\\.0\\.0/", "/obp/v5.0.0/")
84838517
val newUri = req.uri.withPath(Uri.Path.unsafeFromString(rewritten))
84848518
val rewrittenReq = req.withUri(newUri)
8485-
Http4s510.wrappedRoutesV510Services.run(rewrittenReq)
8519+
Http4s500.wrappedRoutesV500Services.run(rewrittenReq)
84868520
} else {
84878521
OptionT.none[IO, Response[IO]]
84888522
}
@@ -8501,5 +8535,8 @@ object Http4s600 {
85018535
// A `lazy val` defers the read until first access (from Http4sApp after Boot
85028536
// completes), by which time Impl6 is fully initialised.
85038537
lazy val wrappedRoutesV600Services: HttpRoutes[IO] =
8504-
Implementations6_0_0.allRoutesWithMiddleware
8538+
Kleisli[HttpF, Request[IO], Response[IO]] { req =>
8539+
Implementations6_0_0.allRoutesWithMiddleware.run(req)
8540+
.orElse(Implementations6_0_0.v600ToV500Bridge.run(req))
8541+
}
85058542
}

obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import net.liftweb.json.{Extraction, Formats}
4848
import net.liftweb.mapper.{By, Descending, MaxRows, OrderBy}
4949
import org.http4s._
5050
import org.http4s.dsl.io._
51+
import org.typelevel.ci.CIString
5152

5253
import scala.collection.JavaConverters._
5354
import scala.collection.mutable.ArrayBuffer
@@ -3583,8 +3584,25 @@ object Http4s700 {
35833584
ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes))
35843585
}
35853586

3586-
// Routes with ResourceDocMiddleware - provides automatic validation based on ResourceDoc metadata
3587-
// Authentication is automatic based on $AuthenticatedUserIsRequired in ResourceDoc errorResponseBodies
3588-
// This matches Lift's wrappedWithAuthCheck behavior
3589-
val wrappedRoutesV700Services: HttpRoutes[IO] = Implementations7_0_0.allRoutesWithMiddleware
3587+
// ─── path-rewriting bridge: /obp/v7.0.0/… → /obp/v6.0.0/… ─────────────
3588+
// Catches v7.0.0 paths not handled by Http4s700's own endpoints and forwards
3589+
// them to Http4s600 (which has all 243 v6.0.0 endpoints). This replaces the
3590+
// old Lift-bridge rewrite, which failed because OBPAPI6_0_0.routes = Nil.
3591+
private val v700ToV600Bridge: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req =>
3592+
val rawPath = req.uri.path.renderString
3593+
if (rawPath.startsWith("/obp/v7.0.0/")) {
3594+
val rewritten = rawPath.replaceFirst("/obp/v7\\.0\\.0/", "/obp/v6.0.0/")
3595+
val newUri = req.uri.withPath(Uri.Path.unsafeFromString(rewritten))
3596+
code.api.v6_0_0.Http4s600.wrappedRoutesV600Services.run(req.withUri(newUri))
3597+
.map(_.putHeaders(Header.Raw(CIString("X-OBP-Version-Served"), "v6.0.0")))
3598+
} else {
3599+
OptionT.none[IO, Response[IO]]
3600+
}
3601+
}
3602+
3603+
lazy val wrappedRoutesV700Services: HttpRoutes[IO] =
3604+
Kleisli[HttpF, Request[IO], Response[IO]] { req =>
3605+
Implementations7_0_0.allRoutesWithMiddleware.run(req)
3606+
.orElse(v700ToV600Bridge.run(req))
3607+
}
35903608
}

obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTechnologyTest.scala

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,15 @@ class ResourceDocsTechnologyTest extends ServerSetup with PropsReset {
2020
response.code should equal(200)
2121
(response.body \ "resource_docs") match {
2222
case JArray(docs) =>
23-
val technology = docs.head \ "implemented_by" \ "technology"
24-
technology should equal(JString(Constant.TECHNOLOGY_LIFTWEB))
23+
// At least one doc should have a technology field (http4s or liftweb).
24+
// v5.0.0 has no technology field at all; v6.0.0 exposes it for all docs.
25+
val hasTechnology = docs.exists { doc =>
26+
(doc \ "implemented_by" \ "technology") match {
27+
case JString(t) => t == Constant.TECHNOLOGY_HTTP4S || t == Constant.TECHNOLOGY_LIFTWEB
28+
case _ => false
29+
}
30+
}
31+
hasTechnology should be(true)
2532
case _ =>
2633
fail("Expected resource_docs field to be an array")
2734
}

obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class ResourceDocsTest extends ResourceDocsV140ServerSetup with PropsReset with
105105
And("We should get 200 and the response can be extract to case classes")
106106
val responseDocs = responseGetObp.body.extract[ResourceDocsJson]
107107
responseGetObp.code should equal(200)
108-
responseDocs.resource_docs.head.implemented_by.technology shouldBe Some(Constant.TECHNOLOGY_LIFTWEB)
108+
responseDocs.resource_docs.head.implemented_by.technology should (equal(Some(Constant.TECHNOLOGY_LIFTWEB)) or equal(Some(Constant.TECHNOLOGY_HTTP4S)))
109109
//This should not throw any exceptions
110110
responseDocs.resource_docs.take(3).foreach(doc => stringToNodeSeq(doc.description))
111111
}

obp-api/src/test/scala/code/api/v6_0_0/AbacRuleTests.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ class AbacRuleTests extends V600ServerSetup with DefaultUsers {
1717

1818
override def beforeAll(): Unit = {
1919
super.beforeAll()
20+
// Force creation of multiple users so the statistical permissiveness check
21+
// has a meaningful sample (>1 user). Without this, a rule matching only
22+
// resourceUser1's email looks "100% permissive" in an otherwise empty DB.
23+
val _ = (resourceUser1, resourceUser2, resourceUser3, resourceUser4)
2024
}
2125

2226
override def afterAll(): Unit = {

0 commit comments

Comments
 (0)