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
4 changes: 3 additions & 1 deletion obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ import code.productAttributeattribute.MappedProductAttribute
import code.productcollection.MappedProductCollection
import code.productcollectionitem.MappedProductCollectionItem
import code.productfee.ProductFee
import code.products.MappedProduct
import code.products.{MappedProduct, ProductTag}
import code.ratelimiting.RateLimiting
import code.regulatedentities.MappedRegulatedEntity
import code.regulatedentities.attribute.RegulatedEntityAttribute
Expand Down Expand Up @@ -543,6 +543,7 @@ class Boot extends MdcLoggable {
// If set to true we will write each URL with params to a datastore / log file
if (APIUtil.getPropsAsBoolValue("write_connector_metrics", false)) {
logger.info("writeConnectorMetrics is true. We will write connector metrics")
code.metrics.ConnectorMetricBatchWriter.start()
} else {
logger.info("writeConnectorMetrics is false. We will NOT write connector metrics")
}
Expand Down Expand Up @@ -1149,6 +1150,7 @@ object ToSchemify {
DynamicMessageDoc,
EndpointTag,
ProductFee,
ProductTag,
ViewPermission,
UserInitAction,
CounterpartyLimit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5344,7 +5344,8 @@ object SwaggerDefinitionsJSON {
per_hour_call_limit = Some(1000L),
per_day_call_limit = Some(10000L),
per_week_call_limit = Some(50000L),
per_month_call_limit = Some(200000L)
per_month_call_limit = Some(200000L),
tags = Some(List("featured", "beta"))
)
lazy val apiProductJsonV600 = ApiProductJsonV600(
api_product_id = "api-product-id-123",
Expand All @@ -5365,10 +5366,28 @@ object SwaggerDefinitionsJSON {
per_day_call_limit = 10000L,
per_week_call_limit = 50000L,
per_month_call_limit = 200000L,
tags = List("featured", "beta"),
attributes = Some(List(apiProductAttributeResponseJsonV600))
)
lazy val apiProductsJsonV600 = ApiProductsJsonV600(List(apiProductJsonV600))

lazy val productJsonV600 = ProductJsonV600(
bank_id = bankIdExample.value,
product_code = productCodeExample.value,
parent_product_code = "parent",
name = "product name",
more_info_url = "www.example.com/prod1/more-info.html",
terms_and_conditions_url = "www.example.com/prod1/terms.html",
description = "Description",
meta = metaJson,
tags = List("featured", "new"),
attributes = None,
fees = None
)
lazy val productsJsonV600 = ProductsJsonV600(products = List(productJsonV600))

lazy val productTagsJsonV600 = ProductTagsJsonV600(tags = List("featured", "new"))

lazy val featuresJsonV600 = FeaturesJsonV600(
allow_public_views = true,
allow_abac_account_access = false,
Expand Down
23 changes: 23 additions & 0 deletions obp-api/src/main/scala/code/api/cache/Caching.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,29 @@ object Caching extends MdcLoggable {
def setStaticSwaggerDocCache(key:String, value: String)= {
use(JedisMethod.SET, (STATIC_SWAGGER_DOC_CACHE_KEY_PREFIX+key).intern(), Some(GET_STATIC_RESOURCE_DOCS_TTL), Some(value))
}
// Fail-safe wrappers around Redis.use for product caches. If Redis is unreachable (dev without a
// running Redis, transient failure, etc.) we treat it as a miss and recompute instead of failing
// the whole request.
private def tryGet(prefix: String, key: String, ttlSeconds: Int): Option[String] =
try use(JedisMethod.GET, (prefix + key).intern(), Some(ttlSeconds))
catch { case e: Throwable => logger.debug(s"Cache GET failed for $prefix$key: ${e.getMessage}"); None }

private def trySet(prefix: String, key: String, ttlSeconds: Int, value: String): Unit =
try { use(JedisMethod.SET, (prefix + key).intern(), Some(ttlSeconds), Some(value)); () }
catch { case e: Throwable => logger.debug(s"Cache SET failed for $prefix$key: ${e.getMessage}") }

def getFinancialProductsCache(key: String, ttlSeconds: Int): Option[String] =
tryGet(FINANCIAL_PRODUCTS_PREFIX, key, ttlSeconds)

def setFinancialProductsCache(key: String, value: String, ttlSeconds: Int): Unit =
trySet(FINANCIAL_PRODUCTS_PREFIX, key, ttlSeconds, value)

def getApiProductsCache(key: String, ttlSeconds: Int): Option[String] =
tryGet(API_PRODUCTS_PREFIX, key, ttlSeconds)

def setApiProductsCache(key: String, value: String, ttlSeconds: Int): Unit =
trySet(API_PRODUCTS_PREFIX, key, ttlSeconds, value)

/**
* Invalidate all rate limit cache entries for a specific consumer.
* Uses pattern matching to delete all cache keys with prefix: rl_active_{consumerId}_*
Expand Down
11 changes: 10 additions & 1 deletion obp-api/src/main/scala/code/api/constant/constant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@
final val ABAC_RULE_NAMESPACE = "abac_rule"
final val CONNECTOR_OUTBOUND_NAMESPACE = "connector_outbound"
final val CONNECTOR_INBOUND_NAMESPACE = "connector_inbound"
final val FINANCIAL_PRODUCTS_NAMESPACE = "financial_products"
final val API_PRODUCTS_NAMESPACE = "api_products"

// List of all versioned cache namespaces
final val ALL_CACHE_NAMESPACES = List(
Expand All @@ -238,7 +240,9 @@
METRICS_RECENT_NAMESPACE,
ABAC_RULE_NAMESPACE,
CONNECTOR_OUTBOUND_NAMESPACE,
CONNECTOR_INBOUND_NAMESPACE
CONNECTOR_INBOUND_NAMESPACE,
FINANCIAL_PRODUCTS_NAMESPACE,
API_PRODUCTS_NAMESPACE
)

// Cache key prefixes with global namespace and versioning for easy invalidation
Expand Down Expand Up @@ -274,6 +278,11 @@
def CONNECTOR_OUTBOUND_PREFIX: String = getVersionedCachePrefix(CONNECTOR_OUTBOUND_NAMESPACE)
def CONNECTOR_INBOUND_PREFIX: String = getVersionedCachePrefix(CONNECTOR_INBOUND_NAMESPACE)

// Financial Product list cache (bank-scoped and all-banks share this namespace).
def FINANCIAL_PRODUCTS_PREFIX: String = getVersionedCachePrefix(FINANCIAL_PRODUCTS_NAMESPACE)

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZ2-Z8rqv7GqBo0lzUtf&open=AZ2-Z8rqv7GqBo0lzUtf&pullRequest=2766
// Api Product list cache.
def API_PRODUCTS_PREFIX: String = getVersionedCachePrefix(API_PRODUCTS_NAMESPACE)

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

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

// ABAC Policy Constants
final val ABAC_POLICY_ACCOUNT_ACCESS = "account-access"

Expand Down
6 changes: 6 additions & 0 deletions obp-api/src/main/scala/code/api/util/ApiRole.scala
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,12 @@ object ApiRole extends MdcLoggable{
case class CanCreateProductAtAnyBank(requiresBankId: Boolean = false) extends ApiRole
lazy val canCreateProductAtAnyBank = CanCreateProductAtAnyBank()

case class CanUpdateProductTagsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canUpdateProductTagsAtOneBank = CanUpdateProductTagsAtOneBank()

case class CanUpdateProductTagsAtAnyBank(requiresBankId: Boolean = false) extends ApiRole
lazy val canUpdateProductTagsAtAnyBank = CanUpdateProductTagsAtAnyBank()

case class CanCreateFxRate(requiresBankId: Boolean = true) extends ApiRole
lazy val canCreateFxRate = CanCreateFxRate()

Expand Down
16 changes: 8 additions & 8 deletions obp-api/src/main/scala/code/api/util/ErrorMessages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -769,18 +769,18 @@ object ErrorMessages {

val ConnectorMethodNotFound = "OBP-40036: ConnectorMethod not found, please specify valid CONNECTOR_METHOD_ID. "
val ConnectorMethodAlreadyExists = "OBP-40037: ConnectorMethod already exists. "
val ConnectorMethodBodyCompileFail = "OBP-40038: ConnectorMethod methodBody is illegal scala code, compilation failed. "
val DynamicResourceDocAlreadyExists = "OBP-40039: DynamicResourceDoc already exists."
val ConnectorMethodBodyCompileFail = "OBP-40038: ConnectorMethod methodBody is not valid Scala code, compilation failed. "
val DynamicResourceDocAlreadyExists = "OBP-40039: DynamicResourceDoc already exists. "
val DynamicResourceDocNotFound = "OBP-40040: DynamicResourceDoc not found, please specify valid DYNAMIC_RESOURCE_DOC_ID. "
val DynamicResourceDocDeleteError = "OBP-40041: DynamicResourceDoc can not be deleted. "
val DynamicResourceDocDeleteError = "OBP-40041: DynamicResourceDoc could not be deleted. "

val DynamicMessageDocAlreadyExists = "OBP-40042: DynamicMessageDoc already exists."
val DynamicMessageDocAlreadyExists = "OBP-40042: DynamicMessageDoc already exists. "
val DynamicMessageDocNotFound = "OBP-40043: DynamicMessageDoc not found, please specify valid DYNAMIC_MESSAGE_DOC_ID. "
val DynamicMessageDocDeleteError = "OBP-40044: DynamicMessageDoc can not be deleted. "
val DynamicCodeCompileFail = "OBP-40045: The code to do compile is illegal scala code, compilation failed. "
val DynamicMessageDocDeleteError = "OBP-40044: DynamicMessageDoc could not be deleted. "
val DynamicCodeCompileFail = "OBP-40045: The code to compile is not valid Scala code, compilation failed. "

val DynamicResourceDocMethodDependency = "OBP-40046: DynamicResourceDoc method call forbidden methods. "
val DynamicResourceDocMethodPermission = "OBP-40047: DynamicResourceDoc method have no enough permissions. "
val DynamicResourceDocMethodDependency = "OBP-40046: DynamicResourceDoc method calls a forbidden method. "
val DynamicResourceDocMethodPermission = "OBP-40047: DynamicResourceDoc method does not have sufficient permissions. "
val DynamicCodeLangNotSupport = "OBP-40049: This language of dynamic code is not supported. "

val InvalidOperationId = "OBP-40048: Invalid operation_id, please specify valid operation_id."
Expand Down
62 changes: 62 additions & 0 deletions obp-api/src/main/scala/code/api/util/Glossary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3282,6 +3282,24 @@ object Glossary extends MdcLoggable {
|*Create a Guard with a named Role on the Endpoint to protect it from unauthorised users.
|*Grant you an Entitlement to the required Role so you can call the endpoint and pass its Guard.
|
|### Served URL
|
|Dynamic Endpoints are served under a dedicated path prefix, *not* under `/obp/vX.Y.Z/`:
|
|`/obp/dynamic-endpoint` + optional `dynamic_endpoints_url_prefix` (from props) + the path declared in the uploaded Swagger file.
|
|For example, if your Swagger declares `/fashion-brand-list/{brandId}` and `dynamic_endpoints_url_prefix` is unset (the default), the endpoint will be available at:
|
|`/obp/dynamic-endpoint/fashion-brand-list/{brandId}`
|
|For Bank / Space level Dynamic Endpoints, OBP automatically prepends `/banks/BANK_ID` to each path in the Swagger file at creation time. So a Swagger path of `/fashion-brand-list` created for bank `gh.29.uk` is served at:
|
|`/obp/dynamic-endpoint/banks/gh.29.uk/fashion-brand-list`
|
|(plus `dynamic_endpoints_url_prefix` if set.)
|
|Note: the `/obp/vX.Y.Z/management/banks/BANK_ID/dynamic-endpoints` routes are only the administrative CRUD endpoints for creating and managing Dynamic Endpoints — they are not the served URLs of the endpoints themselves.
|
|The following videos are available:
|
| * [Introduction to Dynamic Endpoints](https://vimeo.com/426235612)
Expand All @@ -3302,6 +3320,50 @@ object Glossary extends MdcLoggable {
|Once the `host` is thus set, you can use the Endpoint Mapping endpoints to map the Dynamic Endpoint fields to Dynamic Entity data.
|
|See the [Create Endpoint Mapping](/index#OBPv4.0.0-createEndpointMapping) JSON body. You will need to know the operation_id in advance and you can prepare the request_mapping and response_mapping objects. You can get the operation ID from the API Explorer or Get Dynamic Endpoints endpoints.
|
|### Mapping structure
|
|Each entry in `request_mapping` / `response_mapping` is keyed by the JSON field name you want in the Dynamic Endpoint's request or response payload, and its value is an object of the form:
|
|```
|"<jsonFieldName>": {
| "entity": "<DynamicEntityName>",
| "field": "<dynamicEntityFieldName>",
| "query": "<dynamicEntityLookupField>"
|}
|```
|
|What each key does at runtime:
|
|* **`entity`** — the name of the Dynamic Entity to read from / write to. The current implementation only supports **one entity per mapping**; only the first `entity` value encountered is used.
|* **`field`** — the Dynamic Entity field whose value populates the `<jsonFieldName>` in the output JSON. When the Dynamic Endpoint URL includes a query string (e.g. `?status=available`), `field` is also the Dynamic Entity field used to filter records.
|* **`query`** — the Dynamic Entity field used as the **lookup key** when the URL has a path parameter whose name contains "id" (e.g. `/pet/{petId}`). OBP uses the **first** `query` value in the mapping as this lookup key, so by convention all entries in a mapping repeat the same `query` value (it is per-mapping, not per-field, in practice).
|
|Worked example. Endpoint served URL `/obp/dynamic-endpoint/pet/{petId}` with mapping:
|
|```
|{
| "operation_id": "OBPv4.0.0-dynamicEndpoint_GET_pet_PET_ID",
| "request_mapping": {},
| "response_mapping": {
| "id": { "entity": "PetEntity", "field": "field1", "query": "field1" },
| "name": { "entity": "PetEntity", "field": "field4", "query": "field1" },
| "status": { "entity": "PetEntity", "field": "field8", "query": "field1" }
| }
|}
|```
|
|When called as `GET /pet/123`, OBP:
|
|1. Takes the first `query` value — `"field1"` — and the URL path value `123`.
|2. Finds the `PetEntity` record where `field1 == 123`.
|3. Builds the response body by copying that record's `field1` → `id`, `field4` → `name`, `field8` → `status`.
|
|Notes and caveats:
|
|* Non-id-named path parameters (anything that does not contain "id") are not used for lookup.
|* URL query-string filtering uses `field`, **not** `query`. A call like `GET /pets?status=available` filters `PetEntity` records by the `field` value on the mapping entry whose key matches `status` — in the example above, by `field8 == "available"`.
|* `request_mapping` is used on write operations (POST/PUT) to translate the inbound payload to a Dynamic Entity record; leave it as `{}` for read-only operations.
|
|For more details and a walk through, please see the following video:
|
Expand Down
8 changes: 7 additions & 1 deletion obp-api/src/main/scala/code/api/util/NewStyle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3949,6 +3949,10 @@ object NewStyle extends MdcLoggable{
Future(MappedApiProductsProvider.getApiProductsByBankId(bankId), callContext)
}

def getApiProductsByBankId(bankId: String, tag: Option[String], callContext: Option[CallContext]): OBPReturnType[List[ApiProductTrait]] = {
Future(MappedApiProductsProvider.getApiProductsByBankId(bankId, tag), callContext)
}

def createOrUpdateApiProduct(
bankId: String,
apiProductCode: String,
Expand All @@ -3967,14 +3971,16 @@ object NewStyle extends MdcLoggable{
perDayCallLimit: Long,
perWeekCallLimit: Long,
perMonthCallLimit: Long,
tags: List[String],
callContext: Option[CallContext]
): OBPReturnType[ApiProductTrait] = {
Future(MappedApiProductsProvider.createOrUpdateApiProduct(
bankId, apiProductCode, parentApiProductCode, name, category,
moreInfoUrl, termsAndConditionsUrl, description,
collectionId, monthlySubscriptionCurrency, monthlySubscriptionAmount,
perSecondCallLimit, perMinuteCallLimit, perHourCallLimit,
perDayCallLimit, perWeekCallLimit, perMonthCallLimit
perDayCallLimit, perWeekCallLimit, perMonthCallLimit,
tags
)) map {
i => (unboxFullOrFail(i, callContext, CreateApiProductError), callContext)
}
Expand Down
Loading
Loading