From d9a7736a33048373c6504ba7cc1a408e612a70b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 10 Oct 2025 13:12:41 +0200 Subject: [PATCH 01/18] docfix/Improve Create Bank docs --- obp-api/src/main/scala/code/api/util/APIUtil.scala | 5 ++++- .../src/main/scala/code/api/v4_0_0/APIMethods400.scala | 4 ++-- .../src/main/scala/code/api/v5_0_0/APIMethods500.scala | 4 ++-- .../src/main/scala/code/api/v6_0_0/APIMethods600.scala | 10 ++-------- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 1564734f44..6351120068 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -3119,7 +3119,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } // Direct Login Deprecated i.e Authorization: DirectLogin token=eyJhbGciOiJIUzI1NiJ9.eyIiOiIifQ.Y0jk1EQGB4XgdqmYZUHT6potmH3mKj5mEaA9qrIXXWQ else if (getPropsAsBoolValue("allow_direct_login", true) && hasDirectLoginHeader(cc.authReqHeaderField) && !url.contains("/my/logins/direct")) { DirectLogin.getUserFromDirectLoginHeaderFuture(cc) - } else if (getPropsAsBoolValue("allow_direct_login", true) && + } + // Endpoint /my/logins/direct is onboarding endpoint for Direct Login Flow authentication + // You POST your credentials (username, password, and consumer key) to the DirectLogin endpoint and receive a token in return. + else if (getPropsAsBoolValue("allow_direct_login", true) && (has2021DirectLoginHeader(cc.requestHeaders) || hasDirectLoginHeader(cc.authReqHeaderField)) && url.contains("/my/logins/direct")) { Future{(Empty, Some(cc))} diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index db729eb4ae..75b0b12560 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -3578,8 +3578,8 @@ trait APIMethods400 extends MdcLoggable { |The user creating this will be automatically assigned the Role CanCreateEntitlementAtOneBank. |Thus the User can manage the bank they create and assign Roles to other Users. | - |Only SANDBOX mode - |The settlement accounts are created specified by the bank in the POST body. + |Only SANDBOX mode (i.e. when connector=mapped in properties file) + |The settlement accounts are automatically created by the system when the bank is created. |Name and account id are created in accordance to the next rules: | - Incoming account (name: Default incoming settlement account, Account ID: OBP_DEFAULT_INCOMING_ACCOUNT_ID, currency: EUR) | - Outgoing account (name: Default outgoing settlement account, Account ID: OBP_DEFAULT_OUTGOING_ACCOUNT_ID, currency: EUR) diff --git a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala index 9a23dac09e..f985b2402c 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala @@ -153,8 +153,8 @@ trait APIMethods500 { |The user creating this will be automatically assigned the Role CanCreateEntitlementAtOneBank. |Thus the User can manage the bank they create and assign Roles to other Users. | - |Only SANDBOX mode - |The settlement accounts are created specified by the bank in the POST body. + |Only SANDBOX mode (i.e. when connector=mapped in properties file) + |The settlement accounts are automatically created by the system when the bank is created. |Name and account id are created in accordance to the next rules: | - Incoming account (name: Default incoming settlement account, Account ID: OBP_DEFAULT_INCOMING_ACCOUNT_ID, currency: EUR) | - Outgoing account (name: Default outgoing settlement account, Account ID: OBP_DEFAULT_OUTGOING_ACCOUNT_ID, currency: EUR) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 7d50f502f7..706ae4ca17 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -423,13 +423,7 @@ trait APIMethods600 { |The user creating this will be automatically assigned the Role CanCreateEntitlementAtOneBank. |Thus the User can manage the bank they create and assign Roles to other Users. | - |Only SANDBOX mode - |The settlement accounts are created specified by the bank in the POST body. - |Name and account id are created in accordance to the next rules: - | - Incoming account (name: Default incoming settlement account, Account ID: OBP_DEFAULT_INCOMING_ACCOUNT_ID, currency: EUR) - | - Outgoing account (name: Default outgoing settlement account, Account ID: OBP_DEFAULT_OUTGOING_ACCOUNT_ID, currency: EUR) - | - |""", + postBankJson600, bankJson500, List( @@ -520,7 +514,7 @@ trait APIMethods600 { |DirectLogin is a simple authentication flow. You POST your credentials (username, password, and consumer key) |to the DirectLogin endpoint and receive a token in return. | - |This is an alias to the legacy DirectLogin endpoint that includes the standard API versioning prefix. + |This is an alias to the DirectLogin endpoint that includes the standard API versioning prefix. | |This endpoint requires the following headers: |- DirectLogin: username=YOUR_USERNAME, password=YOUR_PASSWORD, consumer_key=YOUR_CONSUMER_KEY From 6707f0b30ad4928e32bc9f462bd5fa324cb2e60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 14 Oct 2025 14:23:55 +0200 Subject: [PATCH 02/18] docfix/Improve Create Bank docs 2 --- obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 706ae4ca17..6f199613f4 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -423,6 +423,13 @@ trait APIMethods600 { |The user creating this will be automatically assigned the Role CanCreateEntitlementAtOneBank. |Thus the User can manage the bank they create and assign Roles to other Users. | + Only SANDBOX mode (i.e. when connector=mapped in properties file) + |The settlement accounts are automatically created by the system when the bank is created. + |Name and account id are created in accordance to the next rules: + | - Incoming account (name: Default incoming settlement account, Account ID: OBP_DEFAULT_INCOMING_ACCOUNT_ID, currency: EUR) + | - Outgoing account (name: Default outgoing settlement account, Account ID: OBP_DEFAULT_OUTGOING_ACCOUNT_ID, currency: EUR) + | + |""", postBankJson600, bankJson500, From 456a0c02154d6b2532e6bc14f765d4edf5aafc4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 16 Oct 2025 14:58:15 +0200 Subject: [PATCH 03/18] feature/Apply Secure Logging Masking Configuration in case of CallContext.toString --- .../main/scala/code/api/util/ApiSession.scala | 5 +- .../main/scala/code/util/SecureLogging.scala | 63 ++++++++++++++++++- .../test/scala/code/util/ApiSessionTest.scala | 26 ++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiSession.scala b/obp-api/src/main/scala/code/api/util/ApiSession.scala index d6f5774911..71d60b9eca 100644 --- a/obp-api/src/main/scala/code/api/util/ApiSession.scala +++ b/obp-api/src/main/scala/code/api/util/ApiSession.scala @@ -12,6 +12,7 @@ import code.context.UserAuthContextProvider import code.customer.CustomerX import code.model.{Consumer, _} import code.util.Helper.MdcLoggable +import code.util.SecureLogging import code.views.Views import com.openbankproject.commons.model._ import com.openbankproject.commons.util.{EnumValue, OBPEnumeration} @@ -56,7 +57,9 @@ case class CallContext( paginationOffset : Option[String] = None, paginationLimit : Option[String] = None ) extends MdcLoggable { - + override def toString: String = SecureLogging.maskSensitive( + s"${this.getClass.getSimpleName}(${this.productIterator.mkString(", ")})" + ) //This is only used to connect the back adapter. not useful for sandbox mode. def toOutboundAdapterCallContext: OutboundAdapterCallContext= { for{ diff --git a/obp-api/src/main/scala/code/util/SecureLogging.scala b/obp-api/src/main/scala/code/util/SecureLogging.scala index 98ec9bfe0f..00f6257325 100644 --- a/obp-api/src/main/scala/code/util/SecureLogging.scala +++ b/obp-api/src/main/scala/code/util/SecureLogging.scala @@ -37,6 +37,12 @@ object SecureLogging { conditionalPattern("securelogging_mask_client_secret") { (Pattern.compile("(?i)(client_secret[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, + conditionalPattern("securelogging_mask_client_secret") { + (Pattern.compile("(?i)(client_secret\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") + }, + conditionalPattern("securelogging_mask_client_secret") { + (Pattern.compile("(?i)(client_secret->)([^,\\s&\\)]+)"), "$1***") + }, // Authorization / Tokens conditionalPattern("securelogging_mask_authorization") { @@ -45,31 +51,79 @@ object SecureLogging { conditionalPattern("securelogging_mask_access_token") { (Pattern.compile("(?i)(access_token[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, + conditionalPattern("securelogging_mask_access_token") { + (Pattern.compile("(?i)(access_token\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") + }, + conditionalPattern("securelogging_mask_access_token") { + (Pattern.compile("(?i)(access_token->)([^,\\s&\\)]+)"), "$1***") + }, conditionalPattern("securelogging_mask_refresh_token") { (Pattern.compile("(?i)(refresh_token[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, + conditionalPattern("securelogging_mask_refresh_token") { + (Pattern.compile("(?i)(refresh_token\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") + }, + conditionalPattern("securelogging_mask_refresh_token") { + (Pattern.compile("(?i)(refresh_token->)([^,\\s&\\)]+)"), "$1***") + }, conditionalPattern("securelogging_mask_id_token") { (Pattern.compile("(?i)(id_token[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, + conditionalPattern("securelogging_mask_id_token") { + (Pattern.compile("(?i)(id_token\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") + }, + conditionalPattern("securelogging_mask_id_token") { + (Pattern.compile("(?i)(id_token->)([^,\\s&\\)]+)"), "$1***") + }, conditionalPattern("securelogging_mask_token") { (Pattern.compile("(?i)(token[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, + conditionalPattern("securelogging_mask_token") { + (Pattern.compile("(?i)(token\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") + }, + conditionalPattern("securelogging_mask_token") { + (Pattern.compile("(?i)(token->)([^,\\s&\\)]+)"), "$1***") + }, // Passwords conditionalPattern("securelogging_mask_password") { (Pattern.compile("(?i)(password[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, + conditionalPattern("securelogging_mask_password") { + (Pattern.compile("(?i)(password\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") + }, + conditionalPattern("securelogging_mask_password") { + (Pattern.compile("(?i)(password->)([^,\\s&\\)]+)"), "$1***") + }, // API keys conditionalPattern("securelogging_mask_api_key") { (Pattern.compile("(?i)(api_key[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, + conditionalPattern("securelogging_mask_api_key") { + (Pattern.compile("(?i)(api_key\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") + }, + conditionalPattern("securelogging_mask_api_key") { + (Pattern.compile("(?i)(api_key->)([^,\\s&\\)]+)"), "$1***") + }, conditionalPattern("securelogging_mask_key") { (Pattern.compile("(?i)(key[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, + conditionalPattern("securelogging_mask_key") { + (Pattern.compile("(?i)(key\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") + }, + conditionalPattern("securelogging_mask_key") { + (Pattern.compile("(?i)(key->)([^,\\s&\\)]+)"), "$1***") + }, conditionalPattern("securelogging_mask_private_key") { (Pattern.compile("(?i)(private_key[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, + conditionalPattern("securelogging_mask_private_key") { + (Pattern.compile("(?i)(private_key\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") + }, + conditionalPattern("securelogging_mask_private_key") { + (Pattern.compile("(?i)(private_key->)([^,\\s&\\)]+)"), "$1***") + }, // Database conditionalPattern("securelogging_mask_jdbc") { @@ -158,7 +212,14 @@ object SecureLogging { "api_key=sk_test_1234567890abcdef", "Error connecting to jdbc:mysql://localhost:3306/obp?user=admin:secretpassword@dbhost", "Credit card: 4532-1234-5678-9012 was processed", - "User email: sensitive@example.com in auth context" + "User email: sensitive@example.com in auth context", + "Map(client_secret -> my_client_secret, token -> secret_token)", + "Map(client_secret->my_client_secret, access_token->oauth_token_123)", + "directLoginParams=Map(password -> secret123, api_key -> sk_live_key)", + "client_secret -> my_client_secret", + "client_secret->my_client_secret", + "CallContext(oAuthParams=Map(access_token -> bearer_token, client_secret->sensitive_key))", + "Map(token->private_token, password -> supersecret, api_key->sk_live_123)" ) testMessages.map(msg => (msg, maskSensitive(msg))) } diff --git a/obp-api/src/test/scala/code/util/ApiSessionTest.scala b/obp-api/src/test/scala/code/util/ApiSessionTest.scala index c0d2c0a1ed..b8eb4564d1 100644 --- a/obp-api/src/test/scala/code/util/ApiSessionTest.scala +++ b/obp-api/src/test/scala/code/util/ApiSessionTest.scala @@ -56,4 +56,30 @@ class ApiSessionTest extends FeatureSpec with Matchers with GivenWhenThen with M callContextUpdated.get.sessionId should be (Some("12345")) } } + + feature("test CallContext toString secure logging masking") + { + scenario("toString should mask sensitive data") + { + val callContextWithSensitiveData = CallContext( + directLoginParams = Map("password" -> "supersecret"), + oAuthParams = Map("client_secret" -> "my_client_secret") + ) + + val toStringResult = callContextWithSensitiveData.toString + + // Verify that sensitive data is masked - should NOT contain the actual sensitive values + toStringResult should not contain "supersecret" + toStringResult should not contain "my_client_secret" + + // Verify that the result contains the case class structure (not just object reference) + toStringResult should include("CallContext") + + // Verify that masking occurs by checking for masked patterns or lack of sensitive data + val containsActualSensitiveData = toStringResult.contains("supersecret") || + toStringResult.contains("my_client_secret") + + containsActualSensitiveData should be (false) + } + } } \ No newline at end of file From 42fb588cd2c792498970d72ac9a0f951e23341d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 29 Oct 2025 07:55:13 +0100 Subject: [PATCH 04/18] bugfix/Create Call Limits for a Consumer v6.0.0 --- .../scala/code/api/v6_0_0/APIMethods600.scala | 2 +- .../ratelimiting/MappedRateLimiting.scala | 39 +++++++++++++++++++ .../code/ratelimiting/RateLimiting.scala | 12 ++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 6f199613f4..ee904048e8 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -127,7 +127,7 @@ trait APIMethods600 { json.extract[CallLimitPostJsonV600] } _ <- NewStyle.function.getConsumerByConsumerId(consumerId, callContext) - rateLimiting <- RateLimitingDI.rateLimiting.vend.createOrUpdateConsumerCallLimits( + rateLimiting <- RateLimitingDI.rateLimiting.vend.createConsumerCallLimits( consumerId, postJson.from_date, postJson.to_date, diff --git a/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala b/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala index 03f46f6182..0c267983a7 100644 --- a/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala +++ b/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala @@ -123,6 +123,45 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait { ).headOption } + def createConsumerCallLimits(consumerId: String, + fromDate: Date, + toDate: Date, + apiVersion: Option[String], + apiName: Option[String], + bankId: Option[String], + perSecond: Option[String], + perMinute: Option[String], + perHour: Option[String], + perDay: Option[String], + perWeek: Option[String], + perMonth: Option[String]): Future[Box[RateLimiting]] = Future { + + def createRateLimit(c: RateLimiting): Box[RateLimiting] = { + tryo { + c.FromDate(fromDate) + c.ToDate(toDate) + + perSecond.foreach(v => c.PerSecondCallLimit(v.toLong)) + perMinute.foreach(v => c.PerMinuteCallLimit(v.toLong)) + perHour.foreach(v => c.PerHourCallLimit(v.toLong)) + perDay.foreach(v => c.PerDayCallLimit(v.toLong)) + perWeek.foreach(v => c.PerWeekCallLimit(v.toLong)) + perMonth.foreach(v => c.PerMonthCallLimit(v.toLong)) + + c.BankId(bankId.orNull) + c.ApiName(apiName.orNull) + c.ApiVersion(apiVersion.orNull) + c.ConsumerId(consumerId) + + // 👇 bump timestamp for last-write-wins + c.updatedAt(new Date()) + + c.saveMe() + } + } + val result = createRateLimit(RateLimiting.create) + result + } def createOrUpdateConsumerCallLimits(consumerId: String, fromDate: Date, toDate: Date, diff --git a/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala b/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala index 5dfbf0ba27..175b0fb90e 100644 --- a/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala +++ b/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala @@ -30,6 +30,18 @@ trait RateLimitingProviderTrait { perDay: Option[String], perWeek: Option[String], perMonth: Option[String]): Future[Box[RateLimiting]] + def createConsumerCallLimits(consumerId: String, + fromDate: Date, + toDate: Date, + apiVersion: Option[String], + apiName: Option[String], + bankId: Option[String], + perSecond: Option[String], + perMinute: Option[String], + perHour: Option[String], + perDay: Option[String], + perWeek: Option[String], + perMonth: Option[String]): Future[Box[RateLimiting]] def deleteByRateLimitingId(rateLimitingId: String): Future[Box[Boolean]] def getByRateLimitingId(rateLimitingId: String): Future[Box[RateLimiting]] def getActiveCallLimitsByConsumerIdAtDate(consumerId: String, date: Date): Future[List[RateLimiting]] From b31b6fff24b4ebf0ffd5a8c6508147e37e7b2d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 29 Oct 2025 15:31:02 +0100 Subject: [PATCH 05/18] feature/Rate Limiting endpoint tweaks --- README.md | 4 +- completed_developments.md | 2 +- .../main/scala/code/api/util/ApiRole.scala | 187 +++++++++--------- .../scala/code/api/v3_1_0/APIMethods310.scala | 18 +- .../scala/code/api/v4_0_0/APIMethods400.scala | 8 +- .../scala/code/api/v5_1_0/APIMethods510.scala | 6 +- .../scala/code/api/v6_0_0/APIMethods600.scala | 38 ++-- .../scala/code/api/v3_1_0/RateLimitTest.scala | 64 +++--- .../code/api/v4_0_0/RateLimitingTest.scala | 24 +-- .../code/api/v4_0_0/V400ServerSetup.scala | 8 +- .../code/api/v5_1_0/RateLimitingTest.scala | 8 +- .../code/api/v5_1_0/V510ServerSetup.scala | 4 +- .../code/api/v6_0_0/CallLimitsTest.scala | 34 ++-- 13 files changed, 204 insertions(+), 201 deletions(-) diff --git a/README.md b/README.md index 54882e5444..c045d4540f 100644 --- a/README.md +++ b/README.md @@ -557,8 +557,8 @@ user_consumer_limit_anonymous_access=100, In case isn't defined default value is Te set up Rate Limiting in case of the authorized access use these endpoints: -1. `GET ../management/consumers/CONSUMER_ID/consumer/call-limits` - Get Call Limits for a Consumer -2. `PUT ../management/consumers/CONSUMER_ID/consumer/call-limits` - Set Call Limits for a Consumer +1. `GET ../management/consumers/CONSUMER_ID/consumer/rate-limits` - Get Rate Limits for a Consumer +2. `PUT ../management/consumers/CONSUMER_ID/consumer/rate-limits` - Set Rate Limits for a Consumer In order to make it work edit your props file in next way: diff --git a/completed_developments.md b/completed_developments.md index 314d55f1a2..e6b8a46a9b 100644 --- a/completed_developments.md +++ b/completed_developments.md @@ -137,7 +137,7 @@ Consent Consumer - Get Call Limits for a Consumer + Get Rate Limits for a Consumer Get Consumer Get Consumers Get Consumers (logged in User) diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index d07208a039..c711c22633 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -480,15 +480,18 @@ object ApiRole extends MdcLoggable{ case class CanReadUserLockedStatus(requiresBankId: Boolean = false) extends ApiRole lazy val canReadUserLockedStatus = CanReadUserLockedStatus() - case class CanSetCallLimits(requiresBankId: Boolean = false) extends ApiRole - lazy val canSetCallLimits = CanSetCallLimits() + case class CanUpdateRateLimits(requiresBankId: Boolean = false) extends ApiRole + lazy val canUpdateRateLimits = CanUpdateRateLimits() + + case class CanCreateRateLimits(requiresBankId: Boolean = false) extends ApiRole + lazy val canCreateRateLimits = CanCreateRateLimits() case class CanDeleteRateLimiting(requiresBankId: Boolean = false) extends ApiRole - lazy val canDeleteRateLimiting = CanDeleteRateLimiting() - + lazy val canDeleteRateLimits = CanDeleteRateLimiting() + case class CanCreateCustomerMessage(requiresBankId: Boolean = true) extends ApiRole - lazy val canCreateCustomerMessage = CanCreateCustomerMessage() - + lazy val canCreateCustomerMessage = CanCreateCustomerMessage() + case class CanGetCustomerMessages(requiresBankId: Boolean = true) extends ApiRole lazy val canGetCustomerMessages = CanGetCustomerMessages() @@ -503,10 +506,10 @@ object ApiRole extends MdcLoggable{ case class CanCreateSystemAccountNotificationWebhook(requiresBankId: Boolean = false) extends ApiRole lazy val canCreateSystemAccountNotificationWebhook = CanCreateSystemAccountNotificationWebhook() - + case class CanCreateAccountNotificationWebhookAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateAccountNotificationWebhookAtOneBank = CanCreateAccountNotificationWebhookAtOneBank() - + case class CanUpdateWebhook(requiresBankId: Boolean = true) extends ApiRole lazy val canUpdateWebhook = CanUpdateWebhook() @@ -551,22 +554,22 @@ object ApiRole extends MdcLoggable{ case class CanUpdateProductAttribute(requiresBankId: Boolean = true) extends ApiRole lazy val canUpdateProductAttribute = CanUpdateProductAttribute() - + case class CanUpdateBankAttribute(requiresBankId: Boolean = true) extends ApiRole - lazy val canUpdateBankAttribute = CanUpdateBankAttribute() - + lazy val canUpdateBankAttribute = CanUpdateBankAttribute() + case class CanUpdateAtmAttribute(requiresBankId: Boolean = true) extends ApiRole - lazy val canUpdateAtmAttribute = CanUpdateAtmAttribute() - + lazy val canUpdateAtmAttribute = CanUpdateAtmAttribute() + case class CanUpdateAtmAttributeAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canUpdateAtmAttributeAtAnyBank = CanUpdateAtmAttributeAtAnyBank() - + case class CanGetBankAttribute(requiresBankId: Boolean = true) extends ApiRole - lazy val canGetBankAttribute = CanGetBankAttribute() - + lazy val canGetBankAttribute = CanGetBankAttribute() + case class CanGetAtmAttribute(requiresBankId: Boolean = true) extends ApiRole - lazy val canGetAtmAttribute = CanGetAtmAttribute() - + lazy val canGetAtmAttribute = CanGetAtmAttribute() + case class CanGetAtmAttributeAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canGetAtmAttributeAtAnyBank = CanGetAtmAttributeAtAnyBank() @@ -575,25 +578,25 @@ object ApiRole extends MdcLoggable{ case class CanDeleteProductAttribute(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteProductAttribute = CanDeleteProductAttribute() - + case class CanDeleteBankAttribute(requiresBankId: Boolean = true) extends ApiRole - lazy val canDeleteBankAttribute = CanDeleteBankAttribute() - + lazy val canDeleteBankAttribute = CanDeleteBankAttribute() + case class CanDeleteAtmAttribute(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteAtmAttribute = CanDeleteAtmAttribute() - + case class CanDeleteAtmAttributeAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canDeleteAtmAttributeAtAnyBank = CanDeleteAtmAttributeAtAnyBank() case class CanCreateProductAttribute(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateProductAttribute = CanCreateProductAttribute() - + case class CanCreateBankAttribute(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateBankAttribute = CanCreateBankAttribute() - + case class CanCreateAtmAttribute(requiresBankId: Boolean = true) extends ApiRole - lazy val canCreateAtmAttribute = CanCreateAtmAttribute() - + lazy val canCreateAtmAttribute = CanCreateAtmAttribute() + case class CanCreateAtmAttributeAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canCreateAtmAttributeAtAnyBank = CanCreateAtmAttributeAtAnyBank() @@ -608,7 +611,7 @@ object ApiRole extends MdcLoggable{ case class CanCreateProductFee(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateProductFee = CanCreateProductFee() - + case class CanMaintainProductCollection(requiresBankId: Boolean = true) extends ApiRole lazy val canMaintainProductCollection = CanMaintainProductCollection() @@ -669,10 +672,10 @@ object ApiRole extends MdcLoggable{ case class CanCreateBankLevelDynamicEntity(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateBankLevelDynamicEntity = CanCreateBankLevelDynamicEntity() - + case class CanUpdateSystemLevelDynamicEntity(requiresBankId: Boolean = false) extends ApiRole lazy val canUpdateSystemDynamicEntity = CanUpdateSystemLevelDynamicEntity() - + case class CanUpdateBankLevelDynamicEntity(requiresBankId: Boolean = true) extends ApiRole lazy val canUpdateBankLevelDynamicEntity = CanUpdateBankLevelDynamicEntity() @@ -687,13 +690,13 @@ object ApiRole extends MdcLoggable{ case class CanGetDynamicEndpoint(requiresBankId: Boolean = false) extends ApiRole lazy val canGetDynamicEndpoint = CanGetDynamicEndpoint() - + case class CanGetDynamicEndpoints(requiresBankId: Boolean = false) extends ApiRole lazy val canGetDynamicEndpoints = CanGetDynamicEndpoints() case class CanGetBankLevelDynamicEndpoint(requiresBankId: Boolean = true) extends ApiRole lazy val canGetBankLevelDynamicEndpoint = CanGetBankLevelDynamicEndpoint() - + case class CanGetBankLevelDynamicEndpoints(requiresBankId: Boolean = true) extends ApiRole lazy val canGetBankLevelDynamicEndpoints = CanGetBankLevelDynamicEndpoints() @@ -714,7 +717,7 @@ object ApiRole extends MdcLoggable{ case class CanDeleteBankLevelDynamicEndpoint(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteBankLevelDynamicEndpoint = CanDeleteBankLevelDynamicEndpoint() - + case class CanCreateResetPasswordUrl(requiresBankId: Boolean = false) extends ApiRole lazy val canCreateResetPasswordUrl = CanCreateResetPasswordUrl() @@ -744,7 +747,7 @@ object ApiRole extends MdcLoggable{ case class CanCreateDirectDebitAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateDirectDebitAtOneBank = CanCreateDirectDebitAtOneBank() - + case class CanCreateStandingOrderAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateStandingOrderAtOneBank = CanCreateStandingOrderAtOneBank() @@ -762,7 +765,7 @@ object ApiRole extends MdcLoggable{ case class CanDeleteCustomerAttributeAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteCustomerAttributeAtOneBank = CanDeleteCustomerAttributeAtOneBank() - + case class CanDeleteCustomerAttributeAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canDeleteCustomerAttributeAtAnyBank = CanDeleteCustomerAttributeAtAnyBank() @@ -810,61 +813,61 @@ object ApiRole extends MdcLoggable{ case class CanGetDoubleEntryTransactionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canGetDoubleEntryTransactionAtOneBank = CanGetDoubleEntryTransactionAtOneBank() - + case class CanGetDoubleEntryTransactionAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canGetDoubleEntryTransactionAtAnyBank = CanGetDoubleEntryTransactionAtAnyBank() case class CanReadResourceDoc(requiresBankId: Boolean = false) extends ApiRole lazy val canReadResourceDoc = CanReadResourceDoc() - + case class CanReadStaticResourceDoc(requiresBankId: Boolean = false) extends ApiRole lazy val canReadStaticResourceDoc = CanReadStaticResourceDoc() - + case class CanReadDynamicResourceDocsAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canReadDynamicResourceDocsAtOneBank = CanReadDynamicResourceDocsAtOneBank() - + case class CanReadGlossary(requiresBankId: Boolean = false) extends ApiRole lazy val canReadGlossary = CanReadGlossary() case class CanCreateCustomerAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateCustomerAttributeDefinitionAtOneBank = CanCreateCustomerAttributeDefinitionAtOneBank() - + case class CanDeleteCustomerAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteCustomerAttributeDefinitionAtOneBank = CanDeleteCustomerAttributeDefinitionAtOneBank() - + case class CanGetCustomerAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canGetCustomerAttributeDefinitionAtOneBank = CanGetCustomerAttributeDefinitionAtOneBank() - + case class CanCreateAccountAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole - lazy val canCreateAccountAttributeDefinitionAtOneBank = CanCreateAccountAttributeDefinitionAtOneBank() - + lazy val canCreateAccountAttributeDefinitionAtOneBank = CanCreateAccountAttributeDefinitionAtOneBank() + case class CanDeleteAccountAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteAccountAttributeDefinitionAtOneBank = CanDeleteAccountAttributeDefinitionAtOneBank() - + case class CanGetAccountAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole - lazy val canGetAccountAttributeDefinitionAtOneBank = CanGetAccountAttributeDefinitionAtOneBank() - + lazy val canGetAccountAttributeDefinitionAtOneBank = CanGetAccountAttributeDefinitionAtOneBank() + case class CanDeleteProductAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole - lazy val canDeleteProductAttributeDefinitionAtOneBank = CanDeleteProductAttributeDefinitionAtOneBank() - + lazy val canDeleteProductAttributeDefinitionAtOneBank = CanDeleteProductAttributeDefinitionAtOneBank() + case class CanGetProductAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canGetProductAttributeDefinitionAtOneBank = CanGetProductAttributeDefinitionAtOneBank() - + case class CanCreateProductAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateProductAttributeDefinitionAtOneBank = CanCreateProductAttributeDefinitionAtOneBank() - + case class CanCreateBankAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateBankAttributeDefinitionAtOneBank = CanCreateBankAttributeDefinitionAtOneBank() - + case class CanCreateTransactionAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateTransactionAttributeDefinitionAtOneBank = CanCreateTransactionAttributeDefinitionAtOneBank() - + case class CanDeleteTransactionAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteTransactionAttributeDefinitionAtOneBank = CanDeleteTransactionAttributeDefinitionAtOneBank() - + case class CanGetTransactionAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole - lazy val canGetTransactionAttributeDefinitionAtOneBank = CanGetTransactionAttributeDefinitionAtOneBank() - + lazy val canGetTransactionAttributeDefinitionAtOneBank = CanGetTransactionAttributeDefinitionAtOneBank() + case class CanCreateTransactionRequestAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateTransactionRequestAttributeDefinitionAtOneBank = CanCreateTransactionRequestAttributeDefinitionAtOneBank() @@ -882,19 +885,19 @@ object ApiRole extends MdcLoggable{ case class CanCreateCardAttributeDefinitionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateCardAttributeDefinitionAtOneBank = CanCreateCardAttributeDefinitionAtOneBank() - + case class CanDeleteTransactionCascade(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteTransactionCascade = CanDeleteTransactionCascade() - + case class CanDeleteAccountCascade(requiresBankId: Boolean = true) extends ApiRole - lazy val canDeleteAccountCascade = CanDeleteAccountCascade() - + lazy val canDeleteAccountCascade = CanDeleteAccountCascade() + case class CanDeleteBankCascade(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteBankCascade = CanDeleteBankCascade() - + case class CanDeleteProductCascade(requiresBankId: Boolean = true) extends ApiRole - lazy val canDeleteProductCascade = CanDeleteProductCascade() - + lazy val canDeleteProductCascade = CanDeleteProductCascade() + case class CanDeleteCustomerCascade(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteCustomerCascade = CanDeleteCustomerCascade() @@ -933,10 +936,10 @@ object ApiRole extends MdcLoggable{ case class CanUpdateConnectorMethod(requiresBankId: Boolean = false) extends ApiRole lazy val canUpdateConnectorMethod = CanUpdateConnectorMethod() - + case class CanGetAllConnectorMethods(requiresBankId: Boolean = false) extends ApiRole lazy val canGetAllConnectorMethods = CanGetAllConnectorMethods() - + case class CanCreateDynamicResourceDoc(requiresBankId: Boolean = false) extends ApiRole lazy val canCreateDynamicResourceDoc = CanCreateDynamicResourceDoc() @@ -951,7 +954,7 @@ object ApiRole extends MdcLoggable{ case class CanDeleteDynamicResourceDoc(requiresBankId: Boolean = false) extends ApiRole lazy val canDeleteDynamicResourceDoc = CanDeleteDynamicResourceDoc() - + case class CanCreateBankLevelDynamicResourceDoc(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateBankLevelDynamicResourceDoc = CanCreateBankLevelDynamicResourceDoc() @@ -969,7 +972,7 @@ object ApiRole extends MdcLoggable{ case class CanCreateDynamicMessageDoc(requiresBankId: Boolean = false) extends ApiRole lazy val canCreateDynamicMessageDoc = CanCreateDynamicMessageDoc() - + case class CanCreateBankLevelDynamicMessageDoc(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateBankLevelDynamicMessageDoc = CanCreateBankLevelDynamicMessageDoc() @@ -1020,21 +1023,21 @@ object ApiRole extends MdcLoggable{ case class CanDeleteBankLevelEndpointMapping(requiresBankId: Boolean = true) extends ApiRole lazy val canDeleteBankLevelEndpointMapping = CanDeleteBankLevelEndpointMapping() - + case class CanCreateUserInvitation(requiresBankId: Boolean = true) extends ApiRole - lazy val canCreateUserInvitation = CanCreateUserInvitation() + lazy val canCreateUserInvitation = CanCreateUserInvitation() case class CanGetUserInvitation(requiresBankId: Boolean = true) extends ApiRole lazy val canGetUserInvitation = CanGetUserInvitation() case class CanCreateSystemLevelEndpointTag(requiresBankId: Boolean = false) extends ApiRole lazy val canCreateSystemLevelEndpointTag = CanCreateSystemLevelEndpointTag() - + case class CanUpdateSystemLevelEndpointTag(requiresBankId: Boolean = false) extends ApiRole lazy val canUpdateSystemLevelEndpointTag = CanUpdateSystemLevelEndpointTag() - + case class CanDeleteSystemLevelEndpointTag(requiresBankId: Boolean = false) extends ApiRole lazy val canDeleteSystemLevelEndpointTag = CanDeleteSystemLevelEndpointTag() - + case class CanGetSystemLevelEndpointTag(requiresBankId: Boolean = false) extends ApiRole lazy val canGetSystemLevelEndpointTag = CanGetSystemLevelEndpointTag() @@ -1071,7 +1074,7 @@ object ApiRole extends MdcLoggable{ case class CanGetAccountsMinimalForCustomerAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canGetAccountsMinimalForCustomerAtAnyBank = CanGetAccountsMinimalForCustomerAtAnyBank() - + case class CanUpdateConsentStatusAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canUpdateConsentStatusAtOneBank = CanUpdateConsentStatusAtOneBank() case class CanUpdateConsentStatusAtAnyBank(requiresBankId: Boolean = false) extends ApiRole @@ -1096,7 +1099,7 @@ object ApiRole extends MdcLoggable{ case class CanGetSystemIntegrity(requiresBankId: Boolean = false) extends ApiRole lazy val canGetSystemIntegrity = CanGetSystemIntegrity() - + private val dynamicApiRoles = new ConcurrentHashMap[String, ApiRole] private case class DynamicApiRole(role: String, requiresBankId: Boolean = false) extends ApiRole{ @@ -1142,35 +1145,35 @@ object ApiRole extends MdcLoggable{ } object Util { - + def checkWrongDefinedNames: List[List[Unit]] = { import scala.meta._ val source: Source = new java.io.File("obp-api/src/main/scala/code/api/util/ApiRole.scala").parse[Source].get - val allowedPrefixes = + val allowedPrefixes = List( "CanCreate", - "CanGet", - "CanUpdate", - "CanDelete", - "CanMaintain", - "CanSearch", - "CanEnable", + "CanGet", + "CanUpdate", + "CanDelete", + "CanMaintain", + "CanSearch", + "CanEnable", "CanDisable" ) - val allowedExistingNames = + val allowedExistingNames = List( "CanQueryOtherUser", - "CanAddSocialMediaHandle", - "CanReadMetrics", - "CanUseFirehoseAtAnyBank", - "CanReadAggregateMetrics", - "CanUnlockUser", - "CanReadUserLockedStatus", - "CanReadCallLimits", - "CanCheckFundsAvailable", - "CanRefreshUser", - "CanReadFx", + "CanAddSocialMediaHandle", + "CanReadMetrics", + "CanUseFirehoseAtAnyBank", + "CanReadAggregateMetrics", + "CanUnlockUser", + "CanReadUserLockedStatus", + "CanReadCallLimits", + "CanCheckFundsAvailable", + "CanRefreshUser", + "CanReadFx", "CanSetCallLimits", "CanDeleteRateLimiting" ) diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index 283cb12640..ed3490de02 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -508,12 +508,12 @@ trait APIMethods310 { implementedInApiVersion, nameOf(callsLimit), "PUT", - "/management/consumers/CONSUMER_ID/consumer/call-limits", - "Set Rate Limiting (call limits) per Consumer", + "/management/consumers/CONSUMER_ID/consumer/rate-limits", + "Set Rate Limits (call limits) per Consumer", s""" |Set the API rate limiting (call limits) per Consumer: | - |Call limits can be set: + |Rate limits can be set: | |Per Second |Per Minute @@ -537,14 +537,14 @@ trait APIMethods310 { UnknownError ), List(apiTagConsumer), - Some(List(canSetCallLimits))) + Some(List(canUpdateRateLimits))) lazy val callsLimit : OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonPut json -> _ => { + case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: Nil JsonPut json -> _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { (Full(u), callContext) <- authenticatedAccess(cc) - _ <- NewStyle.function.hasEntitlement("", u.userId, canSetCallLimits, callContext) + _ <- NewStyle.function.hasEntitlement("", u.userId, canUpdateRateLimits, callContext) postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $CallLimitPostJson ", 400, callContext) { json.extract[CallLimitPostJson] } @@ -578,7 +578,7 @@ trait APIMethods310 { nameOf(getCallsLimit), "GET", "/management/consumers/CONSUMER_ID/consumer/call-limits", - "Get Call Limits for a Consumer", + "Get Rate Limits for a Consumer", s""" |Get Calls limits per Consumer. |${userAuthenticationMessage(true)} @@ -596,12 +596,12 @@ trait APIMethods310 { UnknownError ), List(apiTagConsumer), - Some(List(canSetCallLimits))) + Some(List(canUpdateRateLimits))) lazy val getCallsLimit : OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonGet _ => { + case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: Nil JsonGet _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { (Full(u), callContext) <- authenticatedAccess(cc) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 75b0b12560..6aa2f024a3 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -173,7 +173,7 @@ trait APIMethods400 extends MdcLoggable { implementedInApiVersion, nameOf(callsLimit), "PUT", - "/management/consumers/CONSUMER_ID/consumer/call-limits", + "/management/consumers/CONSUMER_ID/consumer/rate-limits", "Set Rate Limits / Call Limits per Consumer", s""" |Set the API rate limits / call limits for a Consumer: @@ -202,14 +202,14 @@ trait APIMethods400 extends MdcLoggable { UnknownError ), List(apiTagConsumer, apiTagRateLimits), - Some(List(canSetCallLimits))) + Some(List(canUpdateRateLimits))) lazy val callsLimit : OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonPut json -> _ => { + case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: Nil JsonPut json -> _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { (Full(u), callContext) <- authenticatedAccess(cc) - _ <- NewStyle.function.handleEntitlementsAndScopes("", u.userId, List(canSetCallLimits), callContext) + _ <- NewStyle.function.handleEntitlementsAndScopes("", u.userId, List(canUpdateRateLimits), callContext) postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $CallLimitPostJsonV400 ", 400, callContext) { json.extract[CallLimitPostJsonV400] } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 82bbbe2c4d..8805a93313 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -3329,8 +3329,8 @@ trait APIMethods510 { implementedInApiVersion, nameOf(getCallsLimit), "GET", - "/management/consumers/CONSUMER_ID/consumer/call-limits", - "Get Call Limits for a Consumer", + "/management/consumers/CONSUMER_ID/consumer/rate-limits", + "Get Rate Limits for a Consumer", s""" |Get Calls limits per Consumer. |${userAuthenticationMessage(true)} @@ -3352,7 +3352,7 @@ trait APIMethods510 { lazy val getCallsLimit: OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonGet _ => + case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: Nil JsonGet _ => cc => implicit val ec = EndpointContext(Some(cc)) for { diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index ee904048e8..02ed6a72ef 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -4,7 +4,7 @@ import code.api.{APIFailureNewStyle, DirectLogin, ObpApiFailure} import code.api.v6_0_0.JSONFactory600 import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil._ -import code.api.util.ApiRole.{CanCreateEntitlementAtOneBank, CanReadDynamicResourceDocsAtOneBank, canCreateBank, canDeleteRateLimiting, canReadCallLimits, canSetCallLimits} +import code.api.util.ApiRole.{CanCreateEntitlementAtOneBank, CanReadDynamicResourceDocsAtOneBank, canCreateBank, canDeleteRateLimits, canReadCallLimits, canCreateRateLimits} import code.api.util.ApiTag._ import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidDateFormat, InvalidJsonFormat, UnknownError, _} import code.api.util.FutureUtil.EndpointContext @@ -55,9 +55,9 @@ trait APIMethods600 { nameOf(getCurrentCallsLimit), "GET", "/management/consumers/CONSUMER_ID/consumer/current-usage", - "Get Call Limits for a Consumer Usage", + "Get Rate Limits for a Consumer Usage", s""" - |Get Call Limits for a Consumer Usage. + |Get Rate Limits for a Consumer Usage. |${userAuthenticationMessage(true)} | |""".stripMargin, @@ -94,10 +94,10 @@ trait APIMethods600 { implementedInApiVersion, nameOf(createCallLimits), "POST", - "/management/consumers/CONSUMER_ID/consumer/call-limits", - "Create Call Limits for a Consumer", + "/management/consumers/CONSUMER_ID/consumer/rate-limits", + "Create Rate Limits for a Consumer", s""" - |Create Call Limits for a Consumer + |Create Rate Limits for a Consumer | |${userAuthenticationMessage(true)} | @@ -113,16 +113,16 @@ trait APIMethods600 { UnknownError ), List(apiTagConsumer), - Some(List(canSetCallLimits))) + Some(List(canCreateRateLimits))) lazy val createCallLimits: OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonPost json -> _ => + case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: Nil JsonPost json -> _ => cc => implicit val ec = EndpointContext(Some(cc)) for { (Full(u), callContext) <- authenticatedAccess(cc) - _ <- NewStyle.function.hasEntitlement("", u.userId, canSetCallLimits, callContext) + _ <- NewStyle.function.hasEntitlement("", u.userId, canCreateRateLimits, callContext) postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $CallLimitPostJsonV600 ", 400, callContext) { json.extract[CallLimitPostJsonV600] } @@ -155,10 +155,10 @@ trait APIMethods600 { implementedInApiVersion, nameOf(deleteCallLimits), "DELETE", - "/management/consumers/CONSUMER_ID/consumer/call-limits/RATE_LIMITING_ID", - "Delete Call Limit by Rate Limiting ID", + "/management/consumers/CONSUMER_ID/consumer/rate-limits/RATE_LIMITING_ID", + "Delete Rate Limit by Rate Limiting ID", s""" - |Delete a specific Call Limit by Rate Limiting ID + |Delete a specific Rate Limit by Rate Limiting ID | |${userAuthenticationMessage(true)} | @@ -173,16 +173,16 @@ trait APIMethods600 { UnknownError ), List(apiTagConsumer), - Some(List(canDeleteRateLimiting))) + Some(List(canDeleteRateLimits))) lazy val deleteCallLimits: OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: rateLimitingId :: Nil JsonDelete _ => + case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: rateLimitingId :: Nil JsonDelete _ => cc => implicit val ec = EndpointContext(Some(cc)) for { (Full(u), callContext) <- authenticatedAccess(cc) - _ <- NewStyle.function.hasEntitlement("", u.userId, canDeleteRateLimiting, callContext) + _ <- NewStyle.function.hasEntitlement("", u.userId, canDeleteRateLimits, callContext) _ <- NewStyle.function.getConsumerByConsumerId(consumerId, callContext) rateLimiting <- RateLimitingDI.rateLimiting.vend.getByRateLimitingId(rateLimitingId) _ <- rateLimiting match { @@ -208,10 +208,10 @@ trait APIMethods600 { implementedInApiVersion, nameOf(getActiveCallLimitsAtDate), "GET", - "/management/consumers/CONSUMER_ID/consumer/call-limits/active-at-date/DATE", - "Get Active Call Limits at Date", + "/management/consumers/CONSUMER_ID/consumer/rate-limits/active-at-date/DATE", + "Get Active Rate Limits at Date", s""" - |Get the sum of call limits at a certain date time. This returns a SUM of all the records that span that time. + |Get the sum of rate limits at a certain date time. This returns a SUM of all the records that span that time. | |Date format: YYYY-MM-DDTHH:MM:SSZ (e.g. 1099-12-31T23:00:00Z) | @@ -233,7 +233,7 @@ trait APIMethods600 { lazy val getActiveCallLimitsAtDate: OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: "active-at-date" :: dateString :: Nil JsonGet _ => + case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: "active-at-date" :: dateString :: Nil JsonGet _ => cc => implicit val ec = EndpointContext(Some(cc)) for { diff --git a/obp-api/src/test/scala/code/api/v3_1_0/RateLimitTest.scala b/obp-api/src/test/scala/code/api/v3_1_0/RateLimitTest.scala index 612d95f866..a781d376af 100644 --- a/obp-api/src/test/scala/code/api/v3_1_0/RateLimitTest.scala +++ b/obp-api/src/test/scala/code/api/v3_1_0/RateLimitTest.scala @@ -32,7 +32,7 @@ import java.time.{ZoneId, ZonedDateTime} import java.util.Date import code.api.util.APIUtil.OAuth._ import code.api.util.{ApiRole, RateLimitingUtil} -import code.api.util.ApiRole.{CanReadCallLimits, CanSetCallLimits} +import code.api.util.ApiRole.{CanReadCallLimits, CanUpdateRateLimits} import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn} import code.api.v3_1_0.OBPAPI3_1_0.Implementations3_1_0 import code.consumer.Consumers @@ -148,30 +148,30 @@ class RateLimitTest extends V310ServerSetup with PropsReset { When("We make a request v3.1.0") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT val response310 = makePutRequest(request310, write(callLimitJson1)) Then("We should get a 401") response310.code should equal(401) And("error should be " + UserNotLoggedIn) response310.body.extract[ErrorMessage].message should equal (UserNotLoggedIn) } - scenario("We will try to set calls limit per minute without a proper Role " + ApiRole.canSetCallLimits, ApiEndpoint, VersionOfApi) { - When("We make a request v3.1.0 without a Role " + ApiRole.canSetCallLimits) + scenario("We will try to set calls limit per minute without a proper Role " + ApiRole.canUpdateRateLimits, ApiEndpoint, VersionOfApi) { + When("We make a request v3.1.0 without a Role " + ApiRole.canUpdateRateLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) val response310 = makePutRequest(request310, write(callLimitJson1)) Then("We should get a 403") response310.code should equal(403) - And("error should be " + UserHasMissingRoles + CanSetCallLimits) - response310.body.extract[ErrorMessage].message should equal (UserHasMissingRoles + CanSetCallLimits) + And("error should be " + UserHasMissingRoles + CanUpdateRateLimits) + response310.body.extract[ErrorMessage].message should equal (UserHasMissingRoles + CanUpdateRateLimits) } - scenario("We will try to set calls limit per minute with a proper Role " + ApiRole.canSetCallLimits, ApiEndpoint, VersionOfApi) { - When("We make a request v3.1.0 with a Role " + ApiRole.canSetCallLimits) + scenario("We will try to set calls limit per minute with a proper Role " + ApiRole.canUpdateRateLimits, ApiEndpoint, VersionOfApi) { + When("We make a request v3.1.0 with a Role " + ApiRole.canUpdateRateLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanSetCallLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) val response310 = makePutRequest(request310, write(callLimitJson1)) Then("We should get a 200") response310.code should equal(200) @@ -179,12 +179,12 @@ class RateLimitTest extends V310ServerSetup with PropsReset { } scenario("We will set calls limit per second for a Consumer", ApiEndpoint, VersionOfApi) { - When("We make a request v3.1.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v3.1.0 with a Role " + ApiRole.canUpdateRateLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanSetCallLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitSecondJson)) Then("We should get a 200") response01.code should equal(200) @@ -204,12 +204,12 @@ class RateLimitTest extends V310ServerSetup with PropsReset { } scenario("We will set calls limit per minute for a Consumer", ApiEndpoint, VersionOfApi) { - When("We make a request v3.1.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v3.1.0 with a Role " + ApiRole.canUpdateRateLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanSetCallLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitMinuteJson)) Then("We should get a 200") response01.code should equal(200) @@ -229,12 +229,12 @@ class RateLimitTest extends V310ServerSetup with PropsReset { } scenario("We will set calls limit per hour for a Consumer", ApiEndpoint, VersionOfApi) { - When("We make a request v3.1.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v3.1.0 with a Role " + ApiRole.canUpdateRateLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanSetCallLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitHourJson)) Then("We should get a 200") response01.code should equal(200) @@ -254,12 +254,12 @@ class RateLimitTest extends V310ServerSetup with PropsReset { } scenario("We will set calls limit per day for a Consumer", ApiEndpoint, VersionOfApi) { - When("We make a request v3.1.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v3.1.0 with a Role " + ApiRole.canUpdateRateLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanSetCallLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitDayJson)) Then("We should get a 200") response01.code should equal(200) @@ -279,12 +279,12 @@ class RateLimitTest extends V310ServerSetup with PropsReset { } scenario("We will set calls limit per week for a Consumer", ApiEndpoint, VersionOfApi) { - When("We make a request v3.1.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v3.1.0 with a Role " + ApiRole.canUpdateRateLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanSetCallLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitWeekJson)) Then("We should get a 200") response01.code should equal(200) @@ -304,12 +304,12 @@ class RateLimitTest extends V310ServerSetup with PropsReset { } scenario("We will set calls limit per month for a Consumer", ApiEndpoint, VersionOfApi) { - When("We make a request v3.1.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v3.1.0 with a Role " + ApiRole.canUpdateRateLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanSetCallLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitMonthJson)) Then("We should get a 200") response01.code should equal(200) @@ -335,7 +335,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { When("We make a request v3.1.0") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").GET val response310 = makeGetRequest(request310) Then("We should get a 401") response310.code should equal(401) @@ -346,7 +346,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { When("We make a request v3.1.0 without a Role " + ApiRole.canReadCallLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").GET <@(user1) val response310 = makeGetRequest(request310) Then("We should get a 403") response310.code should equal(403) @@ -358,7 +358,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanReadCallLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").GET <@(user1) val response310 = makeGetRequest(request310) Then("We should get a 200") response310.code should equal(200) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/RateLimitingTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/RateLimitingTest.scala index 36a93050c6..f268a8b7d3 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/RateLimitingTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/RateLimitingTest.scala @@ -27,7 +27,7 @@ package code.api.v4_0_0 import code.api.cache.Redis import code.api.util.APIUtil.OAuth._ -import code.api.util.ApiRole.{CanSetCallLimits, canCreateDynamicEndpoint} +import code.api.util.ApiRole.{CanUpdateRateLimits, canCreateDynamicEndpoint} import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn} import code.api.util.{ApiRole, ExampleValue, RateLimitingUtil} import code.api.v3_0_0.OBPAPI3_0_0.Implementations3_0_0.getCurrentUser @@ -101,18 +101,18 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { And("error should be " + UserNotLoggedIn) response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn) } - scenario("We will try to set Rate Limiting per minute without a proper Role " + ApiRole.canSetCallLimits, ApiCallsLimit, ApiVersion400) { + scenario("We will try to set Rate Limiting per minute without a proper Role " + ApiRole.canUpdateRateLimits, ApiCallsLimit, ApiVersion400) { - When("We make a request v4.0.0 without a Role " + ApiRole.canSetCallLimits) + When("We make a request v4.0.0 without a Role " + ApiRole.canUpdateRateLimits) val response400 = setRateLimitingWithoutRole(user1, callLimitJsonInitial) Then("We should get a 403") response400.code should equal(403) - And("error should be " + UserHasMissingRoles + CanSetCallLimits) - response400.body.extract[ErrorMessage].message should equal (UserHasMissingRoles + CanSetCallLimits) + And("error should be " + UserHasMissingRoles + CanUpdateRateLimits) + response400.body.extract[ErrorMessage].message should equal (UserHasMissingRoles + CanUpdateRateLimits) } - scenario("We will try to set Rate Limiting per minute with a proper Role " + ApiRole.canSetCallLimits, ApiCallsLimit, ApiVersion400) { + scenario("We will try to set Rate Limiting per minute with a proper Role " + ApiRole.canUpdateRateLimits, ApiCallsLimit, ApiVersion400) { - When("We make a request v4.0.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v4.0.0 with a Role " + ApiRole.canUpdateRateLimits) val response400 = setRateLimiting(user1, callLimitJsonInitial) Then("We should get a 200") response400.code should equal(200) @@ -120,7 +120,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { } scenario("We will set Rate Limiting per second for an Endpoint", ApiCallsLimit, ApiVersion400) { - When("We make a request v4.0.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v4.0.0 with a Role " + ApiRole.canUpdateRateLimits) val response01 = setRateLimiting(user1, callLimitJsonSecond) Then("We should get a 200") response01.code should equal(200) @@ -143,7 +143,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { } scenario("We will set Rate Limiting per minute for an Endpoint", ApiCallsLimit, ApiVersion400) { - When("We make a request v4.0.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v4.0.0 with a Role " + ApiRole.canUpdateRateLimits) val response01 = setRateLimiting(user1, callLimitJsonMinute) Then("We should get a 200") response01.code should equal(200) @@ -165,7 +165,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { } scenario("We will set Rate Limiting per hour for an Endpoint", ApiCallsLimit, ApiVersion400) { - When("We make a request v4.0.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v4.0.0 with a Role " + ApiRole.canUpdateRateLimits) val response01 = setRateLimiting(user1, callLimitJsonHour) Then("We should get a 200") response01.code should equal(200) @@ -187,7 +187,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { } scenario("We will set Rate Limiting per week for an Endpoint", ApiCallsLimit, ApiVersion400) { - When("We make a request v4.0.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v4.0.0 with a Role " + ApiRole.canUpdateRateLimits) val response01 = setRateLimiting(user1, callLimitJsonWeek) Then("We should get a 200") response01.code should equal(200) @@ -209,7 +209,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { } scenario("We will set Rate Limiting per month for an Endpoint", ApiCallsLimit, ApiVersion400) { - When("We make a request v4.0.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v4.0.0 with a Role " + ApiRole.canUpdateRateLimits) val response01 = setRateLimiting(user1, callLimitJsonMonth) Then("We should get a 200") response01.code should equal(200) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala index 86d5ec1117..aa07a85466 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala @@ -104,18 +104,18 @@ trait V400ServerSetup extends ServerSetupWithTestData with DefaultUsers { def setRateLimiting(consumerAndToken: Option[(Consumer, Token)], putJson: CallLimitPostJsonV400): APIResponse = { val Some((c, _)) = consumerAndToken val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanSetCallLimits.toString) - val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(consumerAndToken) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) + val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(consumerAndToken) makePutRequest(request400, write(putJson)) } def setRateLimitingWithoutRole(consumerAndToken: Option[(Consumer, Token)], putJson: CallLimitPostJsonV400): APIResponse = { val Some((c, _)) = consumerAndToken val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(consumerAndToken) + val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(consumerAndToken) makePutRequest(request400, write(putJson)) } def setRateLimitingAnonymousAccess(putJson: CallLimitPostJsonV400): APIResponse = { - val request400 = (v4_0_0_Request / "management" / "consumers" / "some_consumer_id" / "consumer" / "call-limits").PUT + val request400 = (v4_0_0_Request / "management" / "consumers" / "some_consumer_id" / "consumer" / "rate-limits").PUT makePutRequest(request400, write(putJson)) } diff --git a/obp-api/src/test/scala/code/api/v5_1_0/RateLimitingTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/RateLimitingTest.scala index 9991aeba54..bda8c01781 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/RateLimitingTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/RateLimitingTest.scala @@ -91,7 +91,7 @@ class RateLimitingTest extends V510ServerSetup with PropsReset { When(s"We make a request $ApiVersion510") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request510 = (v5_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET + val request510 = (v5_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").GET val response510 = makeGetRequest(request510) Then("We should get a 401") response510.code should equal(401) @@ -102,7 +102,7 @@ class RateLimitingTest extends V510ServerSetup with PropsReset { When("We make a request v3.1.0 without a Role " + ApiRole.canReadCallLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request510 = (v5_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET <@ (user1) + val request510 = (v5_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").GET <@ (user1) val response510 = makeGetRequest(request510) Then("We should get a 403") response510.code should equal(403) @@ -111,7 +111,7 @@ class RateLimitingTest extends V510ServerSetup with PropsReset { } scenario("We will try to get calls limit per minute with a proper Role " + ApiRole.canReadCallLimits, ApiCallsLimit, ApiVersion510) { - When("We make a request v5.1.0 with a Role " + ApiRole.canSetCallLimits) + When("We make a request v5.1.0 with a Role " + ApiRole.canUpdateRateLimits) val response01 = setRateLimiting(user1, callLimitJsonMonth) Then("We should get a 200") response01.code should equal(200) @@ -120,7 +120,7 @@ class RateLimitingTest extends V510ServerSetup with PropsReset { val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanReadCallLimits.toString) - val request510 = (v5_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET <@ (user1) + val request510 = (v5_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").GET <@ (user1) val response510 = makeGetRequest(request510) Then("We should get a 200") response510.code should equal(200) diff --git a/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala b/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala index 32fa520658..7ac569a749 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala @@ -37,8 +37,8 @@ trait V510ServerSetup extends ServerSetupWithTestData with DefaultUsers { def setRateLimiting(consumerAndToken: Option[(Consumer, Token)], putJson: CallLimitPostJsonV400): APIResponse = { val Some((c, _)) = consumerAndToken val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanSetCallLimits.toString) - val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@ (consumerAndToken) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) + val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@ (consumerAndToken) makePutRequest(request400, write(putJson)) } diff --git a/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala index 915d4b2b9d..90d7594995 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala @@ -26,7 +26,7 @@ TESOBE (http://www.tesobe.com/) package code.api.v6_0_0 import code.api.util.APIUtil.OAuth._ -import code.api.util.ApiRole.{CanDeleteRateLimiting, CanReadCallLimits, CanSetCallLimits} +import code.api.util.ApiRole.{CanDeleteRateLimiting, CanReadCallLimits, CanCreateRateLimits} import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn} import code.api.v6_0_0.OBPAPI6_0_0.Implementations6_0_0 import code.consumer.Consumers @@ -75,7 +75,7 @@ class CallLimitsTest extends V600ServerSetup { When("We make a request v6.0.0 without user credentials") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").POST + val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").POST val response600 = makePostRequest(request600, write(postCallLimitJsonV600)) Then("We should get a 401") response600.code should equal(401) @@ -89,20 +89,20 @@ class CallLimitsTest extends V600ServerSetup { When("We make a request v6.0.0 without a proper role") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").POST <@ (user1) + val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").POST <@ (user1) val response600 = makePostRequest(request600, write(postCallLimitJsonV600)) Then("We should get a 403") response600.code should equal(403) - And("error should be " + UserHasMissingRoles + CanSetCallLimits) - response600.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanSetCallLimits) + And("error should be " + UserHasMissingRoles + CanCreateRateLimits) + response600.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanCreateRateLimits) } scenario("We will call the endpoint with proper Role", ApiEndpoint1, VersionOfApi) { When("We make a request v6.0.0 with a proper role") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanSetCallLimits.toString) - val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").POST <@ (user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateRateLimits.toString) + val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").POST <@ (user1) val response600 = makePostRequest(request600, write(postCallLimitJsonV600)) Then("We should get a 201") response600.code should equal(201) @@ -119,15 +119,15 @@ class CallLimitsTest extends V600ServerSetup { Given("We create a call limit first") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanSetCallLimits.toString) - val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").POST <@ (user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateRateLimits.toString) + val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").POST <@ (user1) val createResponse = makePostRequest(request600, write(postCallLimitJsonV600)) createResponse.code should equal(201) val createdCallLimit = createResponse.body.extract[CallLimitJsonV600] When("We delete the call limit") Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteRateLimiting.toString) - val deleteRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits" / createdCallLimit.rate_limiting_id).DELETE <@ (user1) + val deleteRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits" / createdCallLimit.rate_limiting_id).DELETE <@ (user1) val deleteResponse = makeDeleteRequest(deleteRequest) Then("We should get a 204") @@ -138,14 +138,14 @@ class CallLimitsTest extends V600ServerSetup { Given("We create a call limit first") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanSetCallLimits.toString) - val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").POST <@ (user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateRateLimits.toString) + val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").POST <@ (user1) val createResponse = makePostRequest(request600, write(postCallLimitJsonV600)) createResponse.code should equal(201) val createdCallLimit = createResponse.body.extract[CallLimitJsonV600] When("We try to delete without proper role") - val deleteRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits" / createdCallLimit.rate_limiting_id).DELETE <@ (user1) + val deleteRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits" / createdCallLimit.rate_limiting_id).DELETE <@ (user1) val deleteResponse = makeDeleteRequest(deleteRequest) Then("We should get a 403") @@ -160,8 +160,8 @@ class CallLimitsTest extends V600ServerSetup { Given("We create a call limit first") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanSetCallLimits.toString) - val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").POST <@ (user1) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateRateLimits.toString) + val request600 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").POST <@ (user1) val createResponse = makePostRequest(request600, write(postCallLimitJsonV600)) createResponse.code should equal(201) @@ -170,7 +170,7 @@ class CallLimitsTest extends V600ServerSetup { val currentDateString = ZonedDateTime .now(ZoneOffset.UTC) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")) - val getRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits" / "active-at-date" / currentDateString).GET <@ (user1) + val getRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits" / "active-at-date" / currentDateString).GET <@ (user1) val getResponse = makeGetRequest(getRequest) Then("We should get a 200") @@ -188,7 +188,7 @@ class CallLimitsTest extends V600ServerSetup { val currentDateString = ZonedDateTime .now(ZoneOffset.UTC) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")) - val getRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits" / "active-at-date" / currentDateString).GET <@ (user1) + val getRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits" / "active-at-date" / currentDateString).GET <@ (user1) val getResponse = makeGetRequest(getRequest) Then("We should get a 403") From 4ca97c778b3b6d042f708071ebffc713d82902bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 30 Oct 2025 13:12:00 +0100 Subject: [PATCH 06/18] feature/Rate Limiting endpoint tweaks 2 --- .../scala/code/api/v6_0_0/APIMethods600.scala | 73 ++++++++++++++++++- .../ratelimiting/MappedRateLimiting.scala | 34 +++++++++ .../code/ratelimiting/RateLimiting.scala | 12 +++ .../code/api/v6_0_0/CallLimitsTest.scala | 1 + 4 files changed, 119 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 02ed6a72ef..ed82a74de9 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -4,12 +4,14 @@ import code.api.{APIFailureNewStyle, DirectLogin, ObpApiFailure} import code.api.v6_0_0.JSONFactory600 import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil._ -import code.api.util.ApiRole.{CanCreateEntitlementAtOneBank, CanReadDynamicResourceDocsAtOneBank, canCreateBank, canDeleteRateLimits, canReadCallLimits, canCreateRateLimits} +import code.api.util.ApiRole.{CanCreateEntitlementAtOneBank, CanReadDynamicResourceDocsAtOneBank, canCreateBank, canCreateRateLimits, canDeleteRateLimits, canReadCallLimits, canUpdateRateLimits} import code.api.util.ApiTag._ import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidDateFormat, InvalidJsonFormat, UnknownError, _} import code.api.util.FutureUtil.EndpointContext import code.api.util.{APIUtil, ErrorMessages, NewStyle, RateLimitingUtil} import code.api.util.NewStyle.HttpCode +import code.api.v4_0_0.CallLimitPostJsonV400 +import code.api.v4_0_0.JSONFactory400.createCallsLimitJson import code.api.v5_0_0.{JSONFactory500, PostBankJson500} import code.api.v6_0_0.JSONFactory600.{createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson} import code.bankconnectors.LocalMappedConnectorInternal @@ -150,6 +152,75 @@ trait APIMethods600 { } + staticResourceDocs += ResourceDoc( + callsLimit, + implementedInApiVersion, + nameOf(callsLimit), + "PUT", + "/management/consumers/CONSUMER_ID/consumer/rate-limits/RATE_LIMITING_ID", + "Set Rate Limits / Call Limits per Consumer", + s""" + |Set the API rate limits / call limits for a Consumer: + | + |Rate limiting can be set: + | + |Per Second + |Per Minute + |Per Hour + |Per Week + |Per Month + | + | + |${userAuthenticationMessage(true)} + | + |""".stripMargin, + callLimitPostJsonV400, + callLimitPostJsonV400, + List( + UserNotLoggedIn, + InvalidJsonFormat, + InvalidConsumerId, + ConsumerNotFoundByConsumerId, + UserHasMissingRoles, + UpdateConsumerError, + UnknownError + ), + List(apiTagConsumer, apiTagRateLimits), + Some(List(canUpdateRateLimits))) + + lazy val callsLimit: OBPEndpoint = { + case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: rateLimitingId :: Nil JsonPut json -> _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.handleEntitlementsAndScopes("", u.userId, List(canUpdateRateLimits), callContext) + postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $CallLimitPostJsonV400 ", 400, callContext) { + json.extract[CallLimitPostJsonV400] + } + _ <- NewStyle.function.getConsumerByConsumerId(consumerId, callContext) + rateLimiting <- RateLimitingDI.rateLimiting.vend.updateConsumerCallLimits( + rateLimitingId, + postJson.from_date, + postJson.to_date, + postJson.api_version, + postJson.api_name, + postJson.bank_id, + Some(postJson.per_second_call_limit), + Some(postJson.per_minute_call_limit), + Some(postJson.per_hour_call_limit), + Some(postJson.per_day_call_limit), + Some(postJson.per_week_call_limit), + Some(postJson.per_month_call_limit)) map { + unboxFullOrFail(_, callContext, UpdateConsumerError) + } + } yield { + (createCallsLimitJson(rateLimiting), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( deleteCallLimits, implementedInApiVersion, diff --git a/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala b/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala index 0c267983a7..23bd41ba50 100644 --- a/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala +++ b/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala @@ -206,6 +206,40 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait { result } + def updateConsumerCallLimits(rateLimitingId: String, + fromDate: Date, + toDate: Date, + apiVersion: Option[String], + apiName: Option[String], + bankId: Option[String], + perSecond: Option[String], + perMinute: Option[String], + perHour: Option[String], + perDay: Option[String], + perWeek: Option[String], + perMonth: Option[String]): Future[Box[RateLimiting]] = Future { + RateLimiting.find( + By(RateLimiting.RateLimitingId, rateLimitingId) + ) map { c => + c.FromDate(fromDate) + c.ToDate(toDate) + + perSecond.foreach(v => c.PerSecondCallLimit(v.toLong)) + perMinute.foreach(v => c.PerMinuteCallLimit(v.toLong)) + perHour.foreach(v => c.PerHourCallLimit(v.toLong)) + perDay.foreach(v => c.PerDayCallLimit(v.toLong)) + perWeek.foreach(v => c.PerWeekCallLimit(v.toLong)) + perMonth.foreach(v => c.PerMonthCallLimit(v.toLong)) + + c.BankId(bankId.orNull) + c.ApiName(apiName.orNull) + c.ApiVersion(apiVersion.orNull) + + c.updatedAt(new Date()) + + c.saveMe() + } + } def deleteByRateLimitingId(rateLimitingId: String): Future[Box[Boolean]] = Future { tryo { diff --git a/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala b/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala index 175b0fb90e..f27b106ea6 100644 --- a/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala +++ b/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala @@ -30,6 +30,18 @@ trait RateLimitingProviderTrait { perDay: Option[String], perWeek: Option[String], perMonth: Option[String]): Future[Box[RateLimiting]] + def updateConsumerCallLimits(rateLimitingId: String, + fromDate: Date, + toDate: Date, + apiVersion: Option[String], + apiName: Option[String], + bankId: Option[String], + perSecond: Option[String], + perMinute: Option[String], + perHour: Option[String], + perDay: Option[String], + perWeek: Option[String], + perMonth: Option[String]): Future[Box[RateLimiting]] def createConsumerCallLimits(consumerId: String, fromDate: Date, toDate: Date, diff --git a/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala index 90d7594995..f95847a0a7 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala @@ -46,6 +46,7 @@ class CallLimitsTest extends V600ServerSetup { object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations6_0_0.createCallLimits)) object ApiEndpoint2 extends Tag(nameOf(Implementations6_0_0.deleteCallLimits)) + object UpdateCallLimits extends Tag(nameOf(Implementations6_0_0.updateCallLimits)) object ApiEndpoint3 extends Tag(nameOf(Implementations6_0_0.getActiveCallLimitsAtDate)) lazy val postCallLimitJsonV600 = CallLimitPostJsonV600( From 4f42180298ae9483e247c5f1def94954ba58f6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 31 Oct 2025 09:45:01 +0100 Subject: [PATCH 07/18] feature/Rate Limiting endpoint tweaks 3 --- obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala | 6 +++--- obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index ed82a74de9..665dbb7712 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -153,9 +153,9 @@ trait APIMethods600 { staticResourceDocs += ResourceDoc( - callsLimit, + updateRateLimits, implementedInApiVersion, - nameOf(callsLimit), + nameOf(updateRateLimits), "PUT", "/management/consumers/CONSUMER_ID/consumer/rate-limits/RATE_LIMITING_ID", "Set Rate Limits / Call Limits per Consumer", @@ -188,7 +188,7 @@ trait APIMethods600 { List(apiTagConsumer, apiTagRateLimits), Some(List(canUpdateRateLimits))) - lazy val callsLimit: OBPEndpoint = { + lazy val updateRateLimits: OBPEndpoint = { case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: rateLimitingId :: Nil JsonPut json -> _ => { cc => implicit val ec = EndpointContext(Some(cc)) diff --git a/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala index f95847a0a7..f8869e8817 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala @@ -46,7 +46,7 @@ class CallLimitsTest extends V600ServerSetup { object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations6_0_0.createCallLimits)) object ApiEndpoint2 extends Tag(nameOf(Implementations6_0_0.deleteCallLimits)) - object UpdateCallLimits extends Tag(nameOf(Implementations6_0_0.updateCallLimits)) + object UpdateRateLimits extends Tag(nameOf(Implementations6_0_0.updateRateLimits)) object ApiEndpoint3 extends Tag(nameOf(Implementations6_0_0.getActiveCallLimitsAtDate)) lazy val postCallLimitJsonV600 = CallLimitPostJsonV600( From 0e5a0b4feaa978befe6f5db785f6f895ae384085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 4 Nov 2025 08:44:42 +0100 Subject: [PATCH 08/18] feature/Rate Limiting endpoint tweaks 4 --- .../scala/code/api/util/AfterApiAuth.scala | 74 ++-- .../code/api/util/RateLimitingUtil.scala | 374 ++++++++++++++---- 2 files changed, 340 insertions(+), 108 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala b/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala index 0fc70591c1..fb768b32a1 100644 --- a/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala +++ b/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala @@ -96,10 +96,10 @@ object AfterApiAuth extends MdcLoggable{ * Please note that first source is the table RateLimiting and second is the table Consumer */ def checkRateLimiting(userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = { - def getRateLimiting(consumerId: String, version: String, name: String): Future[Box[RateLimiting]] = { + def getActiveLimits(consumerId: String): Future[List[RateLimiting]] = { RateLimitingUtil.useConsumerLimits match { - case true => RateLimitingDI.rateLimiting.vend.getByConsumerId(consumerId, version, name, Some(new Date())) - case false => Future(Empty) + case true => RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerId, new Date()) + case false => Future(List.empty) } } for { @@ -111,34 +111,48 @@ object AfterApiAuth extends MdcLoggable{ name = cc.flatMap(_.resourceDocument.map(_.partialFunctionName)) // 1st try: function name at resource doc .orElse(operationId) // 2nd try: In case of Dynamic Endpoint we can only use operationId .getOrElse("None") // Not found any unique identifier - rateLimiting <- getRateLimiting(consumer.map(_.consumerId.get).getOrElse(""), version, name) + activeLimits <- getActiveLimits(consumer.map(_.consumerId.get).getOrElse("")) } yield { - val limit: Option[CallLimit] = rateLimiting match { - case Full(rl) => Some(CallLimit( - rl.consumerId, - rl.apiName, - rl.apiVersion, - rl.bankId, - rl.perSecondCallLimit, - rl.perMinuteCallLimit, - rl.perHourCallLimit, - rl.perDayCallLimit, - rl.perWeekCallLimit, - rl.perMonthCallLimit)) - case Empty => - Some(CallLimit( - consumer.map(_.consumerId.get).getOrElse(""), - None, - None, - None, - consumer.map(_.perSecondCallLimit.get).getOrElse(-1), - consumer.map(_.perMinuteCallLimit.get).getOrElse(-1), - consumer.map(_.perHourCallLimit.get).getOrElse(-1), - consumer.map(_.perDayCallLimit.get).getOrElse(-1), - consumer.map(_.perWeekCallLimit.get).getOrElse(-1), - consumer.map(_.perMonthCallLimit.get).getOrElse(-1) - )) - case _ => None + // Find the most specific rate limiting record for this request + def findBestMatch(limits: List[RateLimiting], version: String, name: String): Option[RateLimiting] = { + limits.find(rl => rl.apiVersion.contains(version) && rl.apiName.contains(name)) // 1st try: exact match + .orElse(limits.find(rl => rl.apiName.contains(name))) // 2nd try: match by name only + .orElse(limits.find(rl => rl.apiVersion.contains(version))) // 3rd try: match by version only + .orElse(limits.find(rl => rl.apiName.isEmpty && rl.apiVersion.isEmpty)) // 4th try: general consumer limit + .orElse(limits.headOption) // 5th try: any limit + } + + val limit: Option[CallLimit] = if (activeLimits.nonEmpty) { + findBestMatch(activeLimits, version, name) match { + case Some(rl) => Some(CallLimit( + Some(rl.rateLimitingId), + rl.consumerId, + rl.apiName, + rl.apiVersion, + rl.bankId, + rl.perSecondCallLimit, + rl.perMinuteCallLimit, + rl.perHourCallLimit, + rl.perDayCallLimit, + rl.perWeekCallLimit, + rl.perMonthCallLimit)) + case None => None + } + } else { + // Fallback to consumer limits if no database records found + consumer.map(c => CallLimit( + None, + c.consumerId.get, + None, + None, + None, + c.perSecondCallLimit.get, + c.perMinuteCallLimit.get, + c.perHourCallLimit.get, + c.perDayCallLimit.get, + c.perWeekCallLimit.get, + c.perMonthCallLimit.get + )) } (user, cc.map(_.copy(rateLimiting = limit))) } diff --git a/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala b/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala index a6ecb1df48..2e73144ebc 100644 --- a/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala @@ -8,7 +8,7 @@ import code.api.util.RateLimitingJson.CallLimit import code.util.Helper.MdcLoggable import com.openbankproject.commons.model.User import net.liftweb.common.{Box, Empty} -import redis.clients.jedis.Jedis + import scala.collection.immutable import scala.collection.immutable.{List, Nil} @@ -56,6 +56,7 @@ object RateLimitingPeriod extends Enumeration { object RateLimitingJson { case class CallLimit( + rate_limiting_id : Option[String], consumer_id : String, api_name : Option[String], api_version : Option[String], @@ -75,6 +76,42 @@ object RateLimitingUtil extends MdcLoggable { def useConsumerLimits = APIUtil.getPropsAsBoolValue("use_consumer_limits", false) private def createUniqueKey(consumerKey: String, period: LimitCallPeriod) = consumerKey + RateLimitingPeriod.toString(period) + + private def createUniqueKeyWithId(rateLimitingId: Option[String], consumerKey: String, period: LimitCallPeriod) = { + rateLimitingId match { + case Some(id) => s"${consumerKey}_${id}_${RateLimitingPeriod.toString(period)}" + case None => consumerKey + RateLimitingPeriod.toString(period) + } + } + + private def underConsumerLimitsWithId(rateLimitingId: Option[String], consumerKey: String, period: LimitCallPeriod, limit: Long): Boolean = { + if (useConsumerLimits) { + try { + (limit) match { + case l if l > 0 => // Redis is available and limit is set + val key = createUniqueKeyWithId(rateLimitingId, consumerKey, period) + val exists = Redis.use(JedisMethod.EXISTS,key).map(_.toBoolean).get + exists match { + case true => + val underLimit = Redis.use(JedisMethod.GET,key).get.toLong + 1 <= limit // +1 means we count the current call as well. We increment later i.e after successful call. + underLimit + case false => // In case that key does not exist we return successful result + true + } + case _ => + // Rate Limiting for a Consumer <= 0 implies successful result + // Or any other unhandled case implies successful result + true + } + } catch { + case e : Throwable => + logger.error(s"Redis issue: $e") + true + } + } else { + true // Rate Limiting disabled implies successful result + } + } private def underConsumerLimits(consumerKey: String, period: LimitCallPeriod, limit: Long): Boolean = { if (useConsumerLimits) { @@ -105,13 +142,40 @@ object RateLimitingUtil extends MdcLoggable { } } + private def incrementConsumerCountersWithId(rateLimitingId: Option[String], consumerKey: String, period: LimitCallPeriod, limit: Long): (Long, Long) = { + if (useConsumerLimits) { + try { + (limit) match { + case -1 => // Limit is not set for the period - skip processing + (-1, -1) + case _ => // Redis is available and limit is set + val key = createUniqueKeyWithId(rateLimitingId, consumerKey, period) + val ttl = Redis.use(JedisMethod.TTL, key).get.toInt + ttl match { + case -2 => // if the Key does not exists, -2 is returned + val seconds = RateLimitingPeriod.toSeconds(period).toInt + Redis.use(JedisMethod.SET,key, Some(seconds), Some("1")) + (seconds, 1) + case _ => // otherwise increment the counter + val cnt = Redis.use(JedisMethod.INCR,key).get.toInt + (ttl, cnt) + } + } + } catch { + case e : Throwable => + logger.error(s"Redis issue: $e") + (-1, -1) + } + } else { + (-1, -1) + } + } + private def incrementConsumerCounters(consumerKey: String, period: LimitCallPeriod, limit: Long): (Long, Long) = { if (useConsumerLimits) { try { (limit) match { - case -1 => // Limit is not set for the period - val key = createUniqueKey(consumerKey, period) - Redis.use(JedisMethod.DELETE, key) + case -1 => // Limit is not set for the period - skip processing (-1, -1) case _ => // Redis is available and limit is set val key = createUniqueKey(consumerKey, period) @@ -147,31 +211,130 @@ object RateLimitingUtil extends MdcLoggable { } } + private def ttlWithId(rateLimitingId: Option[String], consumerKey: String, period: LimitCallPeriod): Long = { + val key = createUniqueKeyWithId(rateLimitingId, consumerKey, period) + val ttl = Redis.use(JedisMethod.TTL, key).get.toInt + ttl match { + case -2 => // if the Key does not exists, -2 is returned + 0 + case _ => // otherwise increment the counter + ttl + } + } + def consumerRateLimitState(consumerKey: String): immutable.Seq[((Option[Long], Option[Long]), LimitCallPeriod)] = { + import code.ratelimiting.RateLimitingDI + import java.util.Date + import scala.concurrent.Await + import scala.concurrent.duration._ + + def getAggregatedInfo(consumerKey: String, period: LimitCallPeriod): ((Option[Long], Option[Long]), LimitCallPeriod) = { + try { + // Get all active rate limiting records for this consumer + val allActiveLimits = Await.result( + RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerKey, new Date()), + 5.seconds + ) + + // Collect all Redis keys for this period across all rate limiting records + val allKeys = allActiveLimits.map { rateLimitRecord => + createUniqueKeyWithId(Some(rateLimitRecord.rateLimitingId), consumerKey, period) + } :+ createUniqueKey(consumerKey, period) // Also include legacy key format - def getInfo(consumerKey: String, period: LimitCallPeriod): ((Option[Long], Option[Long]), LimitCallPeriod) = { - val key = createUniqueKey(consumerKey, period) + // Get values and TTLs for all keys and aggregate + val allValues = allKeys.flatMap { key => + try { + val valueOpt = Redis.use(JedisMethod.GET, key).map(_.toLong) + valueOpt + } catch { + case _: Throwable => None + } + } - // get TTL - val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong) + val allTTLs = allKeys.flatMap { key => + try { + val ttlOpt = Redis.use(JedisMethod.TTL, key).map(_.toLong) + ttlOpt.filter(_ > -2) // Filter out non-existent keys (-2) + } catch { + case _: Throwable => None + } + } - // get value (assuming string storage) - val valueOpt: Option[Long] = Redis.use(JedisMethod.GET, key).map(_.toLong) + // Sum all values and take the maximum TTL (longest remaining time) + val aggregatedValue = if (allValues.nonEmpty) Some(allValues.sum) else None + val aggregatedTTL = if (allTTLs.nonEmpty) Some(allTTLs.max) else None - ((valueOpt, ttlOpt), period) + ((aggregatedValue, aggregatedTTL), period) + } catch { + case _: Throwable => + // Fallback to legacy behavior if there's any error + val key = createUniqueKey(consumerKey, period) + val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong) + val valueOpt: Option[Long] = Redis.use(JedisMethod.GET, key).map(_.toLong) + ((valueOpt, ttlOpt), period) + } } - getInfo(consumerKey, RateLimitingPeriod.PER_SECOND) :: - getInfo(consumerKey, RateLimitingPeriod.PER_MINUTE) :: - getInfo(consumerKey, RateLimitingPeriod.PER_HOUR) :: - getInfo(consumerKey, RateLimitingPeriod.PER_DAY) :: - getInfo(consumerKey, RateLimitingPeriod.PER_WEEK) :: - getInfo(consumerKey, RateLimitingPeriod.PER_MONTH) :: + getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_SECOND) :: + getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_MINUTE) :: + getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_HOUR) :: + getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_DAY) :: + getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_WEEK) :: + getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_MONTH) :: Nil } + /** + * This function provides detailed rate limiting state for a consumer, showing information for each individual rate limiting record + * @param consumerKey The consumer key to check + * @return A sequence of detailed rate limiting information for each active record + */ + def detailedConsumerRateLimitState(consumerKey: String): immutable.Seq[(String, String, immutable.Seq[((Option[Long], Option[Long]), LimitCallPeriod)])] = { + import code.ratelimiting.RateLimitingDI + import java.util.Date + import scala.concurrent.Await + import scala.concurrent.duration._ + + try { + // Get all active rate limiting records for this consumer + val allActiveLimits = Await.result( + RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerKey, new Date()), + 5.seconds + ) + + allActiveLimits.map { rateLimitRecord => + def getInfoForRecord(period: LimitCallPeriod): ((Option[Long], Option[Long]), LimitCallPeriod) = { + val key = createUniqueKeyWithId(Some(rateLimitRecord.rateLimitingId), consumerKey, period) + + try { + val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong).filter(_ > -2) + val valueOpt: Option[Long] = Redis.use(JedisMethod.GET, key).map(_.toLong) + ((valueOpt, ttlOpt), period) + } catch { + case _: Throwable => ((None, None), period) + } + } + + val recordInfo = + getInfoForRecord(RateLimitingPeriod.PER_SECOND) :: + getInfoForRecord(RateLimitingPeriod.PER_MINUTE) :: + getInfoForRecord(RateLimitingPeriod.PER_HOUR) :: + getInfoForRecord(RateLimitingPeriod.PER_DAY) :: + getInfoForRecord(RateLimitingPeriod.PER_WEEK) :: + getInfoForRecord(RateLimitingPeriod.PER_MONTH) :: + Nil + + val description = s"API: ${rateLimitRecord.apiName.getOrElse("*")} v${rateLimitRecord.apiVersion.getOrElse("*")} Bank: ${rateLimitRecord.bankId.getOrElse("*")}" + + (rateLimitRecord.rateLimitingId, description, recordInfo) + } + } catch { + case _: Throwable => List.empty + } + } + /** * This function checks rate limiting for a Consumer. * It will check rate limiting per minute, hour, day, week and month. @@ -195,22 +358,22 @@ object RateLimitingUtil extends MdcLoggable { case PER_MONTH => c.per_month case PER_YEAR => -1 } - userAndCallContext._2.map(_.copy(xRateLimitLimit = limit)) - .map(_.copy(xRateLimitReset = z._1)) - .map(_.copy(xRateLimitRemaining = limit - z._2)) + userAndCallContext._2.map { cc: CallContext => + cc.copy(xRateLimitLimit = limit, xRateLimitReset = z._1, xRateLimitRemaining = limit - z._2) + } } def setXRateLimitsAnonymous(id: String, z: (Long, Long), period: LimitCallPeriod): Option[CallContext] = { val limit = period match { case PER_HOUR => perHourLimitAnonymous case _ => -1 } - userAndCallContext._2.map(_.copy(xRateLimitLimit = limit)) - .map(_.copy(xRateLimitReset = z._1)) - .map(_.copy(xRateLimitRemaining = limit - z._2)) + userAndCallContext._2.map { cc: CallContext => + cc.copy(xRateLimitLimit = limit, xRateLimitReset = z._1, xRateLimitRemaining = limit - z._2) + } } def exceededRateLimit(c: CallLimit, period: LimitCallPeriod): Option[CallContextLight] = { - val remain = ttl(c.consumer_id, period) + val remain = ttlWithId(c.rate_limiting_id, c.consumer_id, period) val limit = period match { case PER_SECOND => c.per_second case PER_MINUTE => c.per_minute @@ -220,9 +383,9 @@ object RateLimitingUtil extends MdcLoggable { case PER_MONTH => c.per_month case PER_YEAR => -1 } - userAndCallContext._2.map(_.copy(xRateLimitLimit = limit)) - .map(_.copy(xRateLimitReset = remain)) - .map(_.copy(xRateLimitRemaining = 0)).map(_.toLight) + userAndCallContext._2.map { cc: CallContext => + cc.copy(xRateLimitLimit = limit, xRateLimitReset = remain, xRateLimitRemaining = 0).toLight + } } def exceededRateLimitAnonymous(id: String, period: LimitCallPeriod): Option[CallContextLight] = { @@ -231,64 +394,119 @@ object RateLimitingUtil extends MdcLoggable { case PER_HOUR => perHourLimitAnonymous case _ => -1 } - userAndCallContext._2.map(_.copy(xRateLimitLimit = limit)) - .map(_.copy(xRateLimitReset = remain)) - .map(_.copy(xRateLimitRemaining = 0)).map(_.toLight) + userAndCallContext._2.map { cc: CallContext => + cc.copy(xRateLimitLimit = limit, xRateLimitReset = remain, xRateLimitRemaining = 0).toLight + } } userAndCallContext._2 match { case Some(cc) => cc.rateLimiting match { case Some(rl) => // Authorized access - val rateLimitingKey = - rl.consumer_id + - rl.api_name.getOrElse("") + - rl.api_version.getOrElse("") + - rl.bank_id.getOrElse("") - val checkLimits = List( - underConsumerLimits(rateLimitingKey, PER_SECOND, rl.per_second), - underConsumerLimits(rateLimitingKey, PER_MINUTE, rl.per_minute), - underConsumerLimits(rateLimitingKey, PER_HOUR, rl.per_hour), - underConsumerLimits(rateLimitingKey, PER_DAY, rl.per_day), - underConsumerLimits(rateLimitingKey, PER_WEEK, rl.per_week), - underConsumerLimits(rateLimitingKey, PER_MONTH, rl.per_month) - ) - checkLimits match { - case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x1 == false => - (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_SECOND, rl.per_second), 429, exceededRateLimit(rl, PER_SECOND))), userAndCallContext._2) - case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x2 == false => - (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_MINUTE, rl.per_minute), 429, exceededRateLimit(rl, PER_MINUTE))), userAndCallContext._2) - case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x3 == false => - (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_HOUR, rl.per_hour), 429, exceededRateLimit(rl, PER_HOUR))), userAndCallContext._2) - case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x4 == false => - (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_DAY, rl.per_day), 429, exceededRateLimit(rl, PER_DAY))), userAndCallContext._2) - case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x5 == false => - (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_WEEK, rl.per_week), 429, exceededRateLimit(rl, PER_WEEK))), userAndCallContext._2) - case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x6 == false => - (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_MONTH, rl.per_month), 429, exceededRateLimit(rl, PER_MONTH))), userAndCallContext._2) - case _ => - val incrementCounters = List ( - incrementConsumerCounters(rateLimitingKey, PER_SECOND, rl.per_second), // Responses other than the 429 status code MUST be stored by a cache. - incrementConsumerCounters(rateLimitingKey, PER_MINUTE, rl.per_minute), // Responses other than the 429 status code MUST be stored by a cache. - incrementConsumerCounters(rateLimitingKey, PER_HOUR, rl.per_hour), // Responses other than the 429 status code MUST be stored by a cache. - incrementConsumerCounters(rateLimitingKey, PER_DAY, rl.per_day), // Responses other than the 429 status code MUST be stored by a cache. - incrementConsumerCounters(rateLimitingKey, PER_WEEK, rl.per_week), // Responses other than the 429 status code MUST be stored by a cache. - incrementConsumerCounters(rateLimitingKey, PER_MONTH, rl.per_month) // Responses other than the 429 status code MUST be stored by a cache. + // Get all active rate limiting records for this consumer + import code.ratelimiting.RateLimitingDI + import java.util.Date + import scala.concurrent.Await + import scala.concurrent.duration._ + + val consumerId = rl.consumer_id + val allActiveLimits = try { + Await.result( + RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerId, new Date()), + 5.seconds + ) + } catch { + case _: Throwable => List.empty + } + + // Group rate limiting records by period and combine limits + val periodLimits = allActiveLimits.flatMap { rateLimitRecord => + List( + (PER_SECOND, rateLimitRecord.perSecondCallLimit), + (PER_MINUTE, rateLimitRecord.perMinuteCallLimit), + (PER_HOUR, rateLimitRecord.perHourCallLimit), + (PER_DAY, rateLimitRecord.perDayCallLimit), + (PER_WEEK, rateLimitRecord.perWeekCallLimit), + (PER_MONTH, rateLimitRecord.perMonthCallLimit) + ).filter(_._2 > 0).map { case (period, limit) => + (period, rateLimitRecord, limit) + } + }.groupBy(_._1) // Group by period + + // Check combined usage for each period + val combinedLimitChecks = periodLimits.map { case (period, recordsForPeriod) => + val combinedLimit = recordsForPeriod.map(_._3).sum // Sum all limits for this period + val currentUsage = recordsForPeriod.map { case (_, rateLimitRecord, individualLimit) => + val rateLimitingKey = + rateLimitRecord.consumerId + + rateLimitRecord.apiName.getOrElse("") + + rateLimitRecord.apiVersion.getOrElse("") + + rateLimitRecord.bankId.getOrElse("") + + // Get current usage for this specific record + try { + val key = createUniqueKeyWithId(Some(rateLimitRecord.rateLimitingId), rateLimitingKey, period) + Redis.use(JedisMethod.GET, key).map(_.toLong).getOrElse(0L) + } catch { + case _: Throwable => 0L + } + }.sum + + (period, combinedLimit, currentUsage + 1, recordsForPeriod) // +1 for current request + } + + // Check if any combined limit is exceeded + val exceededCombinedLimit = combinedLimitChecks.find { case (_, combinedLimit, totalUsage, _) => + totalUsage > combinedLimit + } + + exceededCombinedLimit match { + case Some((period, combinedLimit, _, recordsForPeriod)) => + // Use the first record for error response + val firstRecord = recordsForPeriod.head._2 + val callLimit = CallLimit( + Some(firstRecord.rateLimitingId), + firstRecord.consumerId, + firstRecord.apiName, + firstRecord.apiVersion, + firstRecord.bankId, + firstRecord.perSecondCallLimit, + firstRecord.perMinuteCallLimit, + firstRecord.perHourCallLimit, + firstRecord.perDayCallLimit, + firstRecord.perWeekCallLimit, + firstRecord.perMonthCallLimit ) - incrementCounters match { - case first :: _ :: _ :: _ :: _ :: _ :: Nil if first._1 > 0 => - (userAndCallContext._1, setXRateLimits(rl, first, PER_SECOND)) - case _ :: second :: _ :: _ :: _ :: _ :: Nil if second._1 > 0 => - (userAndCallContext._1, setXRateLimits(rl, second, PER_MINUTE)) - case _ :: _ :: third :: _ :: _ :: _ :: Nil if third._1 > 0 => - (userAndCallContext._1, setXRateLimits(rl, third, PER_HOUR)) - case _ :: _ :: _ :: fourth :: _ :: _ :: Nil if fourth._1 > 0 => - (userAndCallContext._1, setXRateLimits(rl, fourth, PER_DAY)) - case _ :: _ :: _ :: _ :: fifth :: _ :: Nil if fifth._1 > 0 => - (userAndCallContext._1, setXRateLimits(rl, fifth, PER_WEEK)) - case _ :: _ :: _ :: _ :: _ :: sixth :: Nil if sixth._1 > 0 => - (userAndCallContext._1, setXRateLimits(rl, sixth, PER_MONTH)) - case _ => + val errorMsg = composeMsgAuthorizedAccess(period, combinedLimit) + (fullBoxOrException(Empty ~> APIFailureNewStyle(errorMsg, 429, exceededRateLimit(callLimit, period))), userAndCallContext._2) + case None => + // Increment counters for all active limits + val allIncrementCounters = periodLimits.flatMap { case (period, recordsForPeriod) => + recordsForPeriod.map { case (_, rateLimitRecord, limit) => + val rateLimitingKey = + rateLimitRecord.consumerId + + rateLimitRecord.apiName.getOrElse("") + + rateLimitRecord.apiVersion.getOrElse("") + + rateLimitRecord.bankId.getOrElse("") + (period, incrementConsumerCountersWithId(Some(rateLimitRecord.rateLimitingId), rateLimitingKey, period, limit)) + } + } + + // Find the first active counter to set rate limit headers + allIncrementCounters.find(_._2._1 > 0) match { + case Some((period, counter)) => + // Create a combined limit for headers + val combinedLimitForPeriod = periodLimits.get(period).map(_.map(_._3).sum).getOrElse(0L) + val modifiedRl = rl.copy( + per_second = if (period == PER_SECOND) combinedLimitForPeriod else rl.per_second, + per_minute = if (period == PER_MINUTE) combinedLimitForPeriod else rl.per_minute, + per_hour = if (period == PER_HOUR) combinedLimitForPeriod else rl.per_hour, + per_day = if (period == PER_DAY) combinedLimitForPeriod else rl.per_day, + per_week = if (period == PER_WEEK) combinedLimitForPeriod else rl.per_week, + per_month = if (period == PER_MONTH) combinedLimitForPeriod else rl.per_month + ) + (userAndCallContext._1, setXRateLimits(modifiedRl, counter, period)) + case None => (userAndCallContext._1, userAndCallContext._2) } } From d12dc9407e3eec6a8ab85876e98537446367e701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 4 Nov 2025 10:08:45 +0100 Subject: [PATCH 09/18] feature/implementation of calendar-aligned rate limiting This implementation modifies the OBP-API rate limiting system to use calendar boundaries instead of fixed time periods. Previously, if a user started using the API at 3 PM, their daily limit would reset at 3 PM the next day. Now it resets at midnight. --- .../src/main/scala/code/api/cache/Redis.scala | 22 +- .../scala/code/api/constant/constant.scala | 2 +- .../code/api/util/RateLimitingUtil.scala | 215 ++++++++++++++++-- 3 files changed, 224 insertions(+), 15 deletions(-) diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index 18fb9e9a58..4ec84eefc5 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -140,6 +140,13 @@ object Redis extends MdcLoggable { jedisConnection.head.ttl(key).toString }else if (method == JedisMethod.DELETE) { jedisConnection.head.del(key).toString + }else if (method == JedisMethod.EXPIREAT) { + // ttlSeconds is treated as Unix timestamp for EXPIREAT + if (ttlSeconds.isDefined) { + jedisConnection.head.expireAt(key, ttlSeconds.get.toLong).toString + } else { + throw new RuntimeException("EXPIREAT requires a timestamp in ttlSeconds parameter") + } }else if (method ==JedisMethod.GET) { jedisConnection.head.get(key) } else if(method ==JedisMethod.SET && value.isDefined){ @@ -160,7 +167,20 @@ object Redis extends MdcLoggable { if (jedisConnection.isDefined && jedisConnection.get != null) jedisConnection.map(_.close()) } - } + } + } + + /** + * Overloaded method for setting expiration using Unix timestamp + * + * @param method JedisMethod + * @param key the cache key + * @param timestampSeconds Unix timestamp in seconds when key should expire + * @param value the cache value + * @return + */ + def useWithTimestamp(method: JedisMethod.Value, key: String, timestampSeconds: Long, value: Option[String] = None): Option[String] = { + use(method, key, Some(timestampSeconds.toInt), value) } implicit val scalaCache = ScalaCache(RedisCache(url, port)) diff --git a/obp-api/src/main/scala/code/api/constant/constant.scala b/obp-api/src/main/scala/code/api/constant/constant.scala index 128f7b209d..009a7663c0 100644 --- a/obp-api/src/main/scala/code/api/constant/constant.scala +++ b/obp-api/src/main/scala/code/api/constant/constant.scala @@ -517,7 +517,7 @@ object PrivateKeyConstants { object JedisMethod extends Enumeration { type JedisMethod = Value - val GET, SET, EXISTS, DELETE, TTL, INCR, FLUSHDB= Value + val GET, SET, EXISTS, DELETE, TTL, INCR, FLUSHDB, EXPIREAT = Value } diff --git a/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala b/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala index 2e73144ebc..370d17437c 100644 --- a/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala @@ -8,6 +8,8 @@ import code.api.util.RateLimitingJson.CallLimit import code.util.Helper.MdcLoggable import com.openbankproject.commons.model.User import net.liftweb.common.{Box, Empty} +import java.time.{LocalDateTime, ZoneId, ZonedDateTime} +import java.time.temporal.{ChronoUnit, TemporalAdjusters} import scala.collection.immutable @@ -84,6 +86,37 @@ object RateLimitingUtil extends MdcLoggable { } } + /** + * Calculate the next calendar boundary for rate limiting periods + * @param period the rate limiting period + * @return Unix timestamp in seconds when the period should expire + */ + private def getNextCalendarBoundary(period: LimitCallPeriod): Long = { + val now = ZonedDateTime.now(ZoneId.systemDefault()) + val nextBoundary = period match { + case PER_SECOND => now.plus(1, ChronoUnit.SECONDS).truncatedTo(ChronoUnit.SECONDS) + case PER_MINUTE => now.plus(1, ChronoUnit.MINUTES).truncatedTo(ChronoUnit.MINUTES) + case PER_HOUR => now.plus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS) + case PER_DAY => now.plus(1, ChronoUnit.DAYS).truncatedTo(ChronoUnit.DAYS) + case PER_WEEK => now.`with`(TemporalAdjusters.nextOrSame(java.time.DayOfWeek.MONDAY)).truncatedTo(ChronoUnit.DAYS) + case PER_MONTH => now.`with`(TemporalAdjusters.firstDayOfNextMonth()).truncatedTo(ChronoUnit.DAYS) + case PER_YEAR => now.`with`(TemporalAdjusters.firstDayOfNextYear()).truncatedTo(ChronoUnit.DAYS) + } + nextBoundary.toEpochSecond + } + + /** + * Determine if a period should use calendar boundaries or fixed TTL + * @param period the rate limiting period + * @return true if calendar boundaries should be used + */ + private def shouldUseCalendarBoundary(period: LimitCallPeriod): Boolean = { + period match { + case PER_DAY | PER_WEEK | PER_MONTH | PER_YEAR => true + case _ => false + } + } + private def underConsumerLimitsWithId(rateLimitingId: Option[String], consumerKey: String, period: LimitCallPeriod, limit: Long): Boolean = { if (useConsumerLimits) { try { @@ -153,9 +186,19 @@ object RateLimitingUtil extends MdcLoggable { val ttl = Redis.use(JedisMethod.TTL, key).get.toInt ttl match { case -2 => // if the Key does not exists, -2 is returned - val seconds = RateLimitingPeriod.toSeconds(period).toInt - Redis.use(JedisMethod.SET,key, Some(seconds), Some("1")) - (seconds, 1) + if (shouldUseCalendarBoundary(period)) { + // Use calendar boundary expiration + val expirationTimestamp = getNextCalendarBoundary(period) + Redis.use(JedisMethod.SET, key, None, Some("1")) + Redis.use(JedisMethod.EXPIREAT, key, Some(expirationTimestamp.toInt), None) + val remainingSeconds = (expirationTimestamp - System.currentTimeMillis() / 1000).toInt + (remainingSeconds, 1) + } else { + // Use fixed TTL for shorter periods + val seconds = RateLimitingPeriod.toSeconds(period).toInt + Redis.use(JedisMethod.SET, key, Some(seconds), Some("1")) + (seconds, 1) + } case _ => // otherwise increment the counter val cnt = Redis.use(JedisMethod.INCR,key).get.toInt (ttl, cnt) @@ -182,9 +225,19 @@ object RateLimitingUtil extends MdcLoggable { val ttl = Redis.use(JedisMethod.TTL, key).get.toInt ttl match { case -2 => // if the Key does not exists, -2 is returned - val seconds = RateLimitingPeriod.toSeconds(period).toInt - Redis.use(JedisMethod.SET,key, Some(seconds), Some("1")) - (seconds, 1) + if (shouldUseCalendarBoundary(period)) { + // Use calendar boundary expiration + val expirationTimestamp = getNextCalendarBoundary(period) + Redis.use(JedisMethod.SET, key, None, Some("1")) + Redis.use(JedisMethod.EXPIREAT, key, Some(expirationTimestamp.toInt), None) + val remainingSeconds = (expirationTimestamp - System.currentTimeMillis() / 1000).toInt + (remainingSeconds, 1) + } else { + // Use fixed TTL for shorter periods + val seconds = RateLimitingPeriod.toSeconds(period).toInt + Redis.use(JedisMethod.SET, key, Some(seconds), Some("1")) + (seconds, 1) + } case _ => // otherwise increment the counter val cnt = Redis.use(JedisMethod.INCR,key).get.toInt (ttl, cnt) @@ -238,15 +291,20 @@ object RateLimitingUtil extends MdcLoggable { 5.seconds ) - // Collect all Redis keys for this period across all rate limiting records + // Collect all Redis keys for this specific period across all rate limiting records val allKeys = allActiveLimits.map { rateLimitRecord => createUniqueKeyWithId(Some(rateLimitRecord.rateLimitingId), consumerKey, period) } :+ createUniqueKey(consumerKey, period) // Also include legacy key format - // Get values and TTLs for all keys and aggregate + // Debug logging for troubleshooting + logger.debug(s"Getting aggregated info for period: $period, consumer: $consumerKey") + logger.debug(s"All keys for this period: $allKeys") + + // Get values and TTLs for all keys for this specific period val allValues = allKeys.flatMap { key => try { val valueOpt = Redis.use(JedisMethod.GET, key).map(_.toLong) + logger.debug(s"Key: $key, Value: $valueOpt") valueOpt } catch { case _: Throwable => None @@ -256,24 +314,78 @@ object RateLimitingUtil extends MdcLoggable { val allTTLs = allKeys.flatMap { key => try { val ttlOpt = Redis.use(JedisMethod.TTL, key).map(_.toLong) - ttlOpt.filter(_ > -2) // Filter out non-existent keys (-2) + val filteredTtl = ttlOpt.filter(_ > -2) // Filter out non-existent keys (-2) + + // Additional bounds checking - TTL should not exceed the period's maximum + val maxExpectedTTL = RateLimitingPeriod.toSeconds(period) + val boundedTtl = filteredTtl.filter { ttl => + if (ttl > maxExpectedTTL) { + logger.warn(s"Key $key has TTL ($ttl) exceeding expected maximum ($maxExpectedTTL) for period $period. Ignoring this TTL.") + false + } else { + true + } + } + + logger.debug(s"Key: $key, TTL: $ttlOpt, Filtered TTL: $filteredTtl, Bounded TTL: $boundedTtl") + boundedTtl } catch { - case _: Throwable => None + case e: Throwable => + logger.debug(s"Error getting TTL for key $key: $e") + None } } - // Sum all values and take the maximum TTL (longest remaining time) + // Sum all values and take the maximum TTL for this specific period only val aggregatedValue = if (allValues.nonEmpty) Some(allValues.sum) else None val aggregatedTTL = if (allTTLs.nonEmpty) Some(allTTLs.max) else None - ((aggregatedValue, aggregatedTTL), period) + logger.debug(s"Period: $period, Aggregated value: $aggregatedValue, Aggregated TTL: $aggregatedTTL") + + // Final validation: ensure TTL is reasonable for this period + val validatedTTL = aggregatedTTL.filter { ttl => + val maxExpected = RateLimitingPeriod.toSeconds(period) + if (ttl > maxExpected) { + logger.warn(s"Aggregated TTL ($ttl) exceeds maximum for $period ($maxExpected). Clearing inconsistent keys.") + // Clear potentially corrupted keys + allKeys.foreach { key => + try { + val keyTTL = Redis.use(JedisMethod.TTL, key).map(_.toLong).getOrElse(-1L) + if (keyTTL > maxExpected) { + Redis.use(JedisMethod.DELETE, key) + logger.info(s"Deleted inconsistent key: $key (TTL was $keyTTL)") + } + } catch { + case e: Throwable => logger.error(s"Error cleaning key $key: $e") + } + } + false + } else { + true + } + } + + ((aggregatedValue, validatedTTL), period) } catch { case _: Throwable => // Fallback to legacy behavior if there's any error val key = createUniqueKey(consumerKey, period) val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong) val valueOpt: Option[Long] = Redis.use(JedisMethod.GET, key).map(_.toLong) - ((valueOpt, ttlOpt), period) + + // Validate legacy key TTL as well + val validatedLegacyTTL = ttlOpt.filter { ttl => + val maxExpected = RateLimitingPeriod.toSeconds(period) + if (ttl > maxExpected) { + logger.warn(s"Legacy key $key has invalid TTL ($ttl) for $period. Deleting.") + Redis.use(JedisMethod.DELETE, key) + false + } else { + true + } + } + + ((valueOpt, validatedLegacyTTL), period) } } @@ -286,6 +398,83 @@ object RateLimitingUtil extends MdcLoggable { Nil } + /** + * Diagnostic method to validate rate limiting logic and boundaries + * This method helps debug issues with TTL calculation + * @param consumerKey The consumer key to test + * @param period The period to test + * @return Debug information about the rate limiting logic + */ + def debugRateLimitingLogic(consumerKey: String, period: LimitCallPeriod): String = { + val debugInfo = StringBuilder.newBuilder + debugInfo.append(s"=== Debug Rate Limiting Logic for $consumerKey, $period ===\n") + + // Test calendar boundary logic + val usesCalendarBoundary = shouldUseCalendarBoundary(period) + debugInfo.append(s"Uses calendar boundary: $usesCalendarBoundary\n") + + if (usesCalendarBoundary) { + val boundary = getNextCalendarBoundary(period) + val currentTime = System.currentTimeMillis() / 1000 + val remainingSeconds = boundary - currentTime + debugInfo.append(s"Calendar boundary timestamp: $boundary\n") + debugInfo.append(s"Current timestamp: $currentTime\n") + debugInfo.append(s"Remaining seconds to boundary: $remainingSeconds\n") + } else { + val fixedTTL = RateLimitingPeriod.toSeconds(period) + debugInfo.append(s"Fixed TTL seconds: $fixedTTL\n") + } + + // Test key creation + val legacyKey = createUniqueKey(consumerKey, period) + debugInfo.append(s"Legacy key: $legacyKey\n") + + // Check what's actually in Redis + try { + val ttlValue = Redis.use(JedisMethod.TTL, legacyKey) + val keyValue = Redis.use(JedisMethod.GET, legacyKey) + debugInfo.append(s"Redis TTL for legacy key: $ttlValue\n") + debugInfo.append(s"Redis value for legacy key: $keyValue\n") + } catch { + case e: Throwable => + debugInfo.append(s"Error accessing Redis: $e\n") + } + + // Test the actual aggregation logic + try { + import code.ratelimiting.RateLimitingDI + import java.util.Date + import scala.concurrent.Await + import scala.concurrent.duration._ + + val allActiveLimits = Await.result( + RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerKey, new Date()), + 5.seconds + ) + + debugInfo.append(s"Active rate limits count: ${allActiveLimits.length}\n") + + allActiveLimits.foreach { limit => + val keyWithId = createUniqueKeyWithId(Some(limit.rateLimitingId), consumerKey, period) + debugInfo.append(s"Rate limit ID: ${limit.rateLimitingId}, Key: $keyWithId\n") + + try { + val ttl = Redis.use(JedisMethod.TTL, keyWithId) + val value = Redis.use(JedisMethod.GET, keyWithId) + debugInfo.append(s" TTL: $ttl, Value: $value\n") + } catch { + case e: Throwable => + debugInfo.append(s" Error: $e\n") + } + } + } catch { + case e: Throwable => + debugInfo.append(s"Error getting active limits: $e\n") + } + + debugInfo.toString() + } + /** * This function provides detailed rate limiting state for a consumer, showing information for each individual rate limiting record * @param consumerKey The consumer key to check From 5fd3d51ecd08230eb8aed3ecfe6419379e7141fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 4 Nov 2025 12:47:50 +0100 Subject: [PATCH 10/18] Revert "feature/implementation of calendar-aligned rate limiting" This reverts commit d12dc9407e3eec6a8ab85876e98537446367e701. --- .../src/main/scala/code/api/cache/Redis.scala | 22 +- .../scala/code/api/constant/constant.scala | 2 +- .../code/api/util/RateLimitingUtil.scala | 215 ++---------------- 3 files changed, 15 insertions(+), 224 deletions(-) diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index 4ec84eefc5..18fb9e9a58 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -140,13 +140,6 @@ object Redis extends MdcLoggable { jedisConnection.head.ttl(key).toString }else if (method == JedisMethod.DELETE) { jedisConnection.head.del(key).toString - }else if (method == JedisMethod.EXPIREAT) { - // ttlSeconds is treated as Unix timestamp for EXPIREAT - if (ttlSeconds.isDefined) { - jedisConnection.head.expireAt(key, ttlSeconds.get.toLong).toString - } else { - throw new RuntimeException("EXPIREAT requires a timestamp in ttlSeconds parameter") - } }else if (method ==JedisMethod.GET) { jedisConnection.head.get(key) } else if(method ==JedisMethod.SET && value.isDefined){ @@ -167,20 +160,7 @@ object Redis extends MdcLoggable { if (jedisConnection.isDefined && jedisConnection.get != null) jedisConnection.map(_.close()) } - } - } - - /** - * Overloaded method for setting expiration using Unix timestamp - * - * @param method JedisMethod - * @param key the cache key - * @param timestampSeconds Unix timestamp in seconds when key should expire - * @param value the cache value - * @return - */ - def useWithTimestamp(method: JedisMethod.Value, key: String, timestampSeconds: Long, value: Option[String] = None): Option[String] = { - use(method, key, Some(timestampSeconds.toInt), value) + } } implicit val scalaCache = ScalaCache(RedisCache(url, port)) diff --git a/obp-api/src/main/scala/code/api/constant/constant.scala b/obp-api/src/main/scala/code/api/constant/constant.scala index 009a7663c0..128f7b209d 100644 --- a/obp-api/src/main/scala/code/api/constant/constant.scala +++ b/obp-api/src/main/scala/code/api/constant/constant.scala @@ -517,7 +517,7 @@ object PrivateKeyConstants { object JedisMethod extends Enumeration { type JedisMethod = Value - val GET, SET, EXISTS, DELETE, TTL, INCR, FLUSHDB, EXPIREAT = Value + val GET, SET, EXISTS, DELETE, TTL, INCR, FLUSHDB= Value } diff --git a/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala b/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala index 370d17437c..2e73144ebc 100644 --- a/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala @@ -8,8 +8,6 @@ import code.api.util.RateLimitingJson.CallLimit import code.util.Helper.MdcLoggable import com.openbankproject.commons.model.User import net.liftweb.common.{Box, Empty} -import java.time.{LocalDateTime, ZoneId, ZonedDateTime} -import java.time.temporal.{ChronoUnit, TemporalAdjusters} import scala.collection.immutable @@ -86,37 +84,6 @@ object RateLimitingUtil extends MdcLoggable { } } - /** - * Calculate the next calendar boundary for rate limiting periods - * @param period the rate limiting period - * @return Unix timestamp in seconds when the period should expire - */ - private def getNextCalendarBoundary(period: LimitCallPeriod): Long = { - val now = ZonedDateTime.now(ZoneId.systemDefault()) - val nextBoundary = period match { - case PER_SECOND => now.plus(1, ChronoUnit.SECONDS).truncatedTo(ChronoUnit.SECONDS) - case PER_MINUTE => now.plus(1, ChronoUnit.MINUTES).truncatedTo(ChronoUnit.MINUTES) - case PER_HOUR => now.plus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS) - case PER_DAY => now.plus(1, ChronoUnit.DAYS).truncatedTo(ChronoUnit.DAYS) - case PER_WEEK => now.`with`(TemporalAdjusters.nextOrSame(java.time.DayOfWeek.MONDAY)).truncatedTo(ChronoUnit.DAYS) - case PER_MONTH => now.`with`(TemporalAdjusters.firstDayOfNextMonth()).truncatedTo(ChronoUnit.DAYS) - case PER_YEAR => now.`with`(TemporalAdjusters.firstDayOfNextYear()).truncatedTo(ChronoUnit.DAYS) - } - nextBoundary.toEpochSecond - } - - /** - * Determine if a period should use calendar boundaries or fixed TTL - * @param period the rate limiting period - * @return true if calendar boundaries should be used - */ - private def shouldUseCalendarBoundary(period: LimitCallPeriod): Boolean = { - period match { - case PER_DAY | PER_WEEK | PER_MONTH | PER_YEAR => true - case _ => false - } - } - private def underConsumerLimitsWithId(rateLimitingId: Option[String], consumerKey: String, period: LimitCallPeriod, limit: Long): Boolean = { if (useConsumerLimits) { try { @@ -186,19 +153,9 @@ object RateLimitingUtil extends MdcLoggable { val ttl = Redis.use(JedisMethod.TTL, key).get.toInt ttl match { case -2 => // if the Key does not exists, -2 is returned - if (shouldUseCalendarBoundary(period)) { - // Use calendar boundary expiration - val expirationTimestamp = getNextCalendarBoundary(period) - Redis.use(JedisMethod.SET, key, None, Some("1")) - Redis.use(JedisMethod.EXPIREAT, key, Some(expirationTimestamp.toInt), None) - val remainingSeconds = (expirationTimestamp - System.currentTimeMillis() / 1000).toInt - (remainingSeconds, 1) - } else { - // Use fixed TTL for shorter periods - val seconds = RateLimitingPeriod.toSeconds(period).toInt - Redis.use(JedisMethod.SET, key, Some(seconds), Some("1")) - (seconds, 1) - } + val seconds = RateLimitingPeriod.toSeconds(period).toInt + Redis.use(JedisMethod.SET,key, Some(seconds), Some("1")) + (seconds, 1) case _ => // otherwise increment the counter val cnt = Redis.use(JedisMethod.INCR,key).get.toInt (ttl, cnt) @@ -225,19 +182,9 @@ object RateLimitingUtil extends MdcLoggable { val ttl = Redis.use(JedisMethod.TTL, key).get.toInt ttl match { case -2 => // if the Key does not exists, -2 is returned - if (shouldUseCalendarBoundary(period)) { - // Use calendar boundary expiration - val expirationTimestamp = getNextCalendarBoundary(period) - Redis.use(JedisMethod.SET, key, None, Some("1")) - Redis.use(JedisMethod.EXPIREAT, key, Some(expirationTimestamp.toInt), None) - val remainingSeconds = (expirationTimestamp - System.currentTimeMillis() / 1000).toInt - (remainingSeconds, 1) - } else { - // Use fixed TTL for shorter periods - val seconds = RateLimitingPeriod.toSeconds(period).toInt - Redis.use(JedisMethod.SET, key, Some(seconds), Some("1")) - (seconds, 1) - } + val seconds = RateLimitingPeriod.toSeconds(period).toInt + Redis.use(JedisMethod.SET,key, Some(seconds), Some("1")) + (seconds, 1) case _ => // otherwise increment the counter val cnt = Redis.use(JedisMethod.INCR,key).get.toInt (ttl, cnt) @@ -291,20 +238,15 @@ object RateLimitingUtil extends MdcLoggable { 5.seconds ) - // Collect all Redis keys for this specific period across all rate limiting records + // Collect all Redis keys for this period across all rate limiting records val allKeys = allActiveLimits.map { rateLimitRecord => createUniqueKeyWithId(Some(rateLimitRecord.rateLimitingId), consumerKey, period) } :+ createUniqueKey(consumerKey, period) // Also include legacy key format - // Debug logging for troubleshooting - logger.debug(s"Getting aggregated info for period: $period, consumer: $consumerKey") - logger.debug(s"All keys for this period: $allKeys") - - // Get values and TTLs for all keys for this specific period + // Get values and TTLs for all keys and aggregate val allValues = allKeys.flatMap { key => try { val valueOpt = Redis.use(JedisMethod.GET, key).map(_.toLong) - logger.debug(s"Key: $key, Value: $valueOpt") valueOpt } catch { case _: Throwable => None @@ -314,78 +256,24 @@ object RateLimitingUtil extends MdcLoggable { val allTTLs = allKeys.flatMap { key => try { val ttlOpt = Redis.use(JedisMethod.TTL, key).map(_.toLong) - val filteredTtl = ttlOpt.filter(_ > -2) // Filter out non-existent keys (-2) - - // Additional bounds checking - TTL should not exceed the period's maximum - val maxExpectedTTL = RateLimitingPeriod.toSeconds(period) - val boundedTtl = filteredTtl.filter { ttl => - if (ttl > maxExpectedTTL) { - logger.warn(s"Key $key has TTL ($ttl) exceeding expected maximum ($maxExpectedTTL) for period $period. Ignoring this TTL.") - false - } else { - true - } - } - - logger.debug(s"Key: $key, TTL: $ttlOpt, Filtered TTL: $filteredTtl, Bounded TTL: $boundedTtl") - boundedTtl + ttlOpt.filter(_ > -2) // Filter out non-existent keys (-2) } catch { - case e: Throwable => - logger.debug(s"Error getting TTL for key $key: $e") - None + case _: Throwable => None } } - // Sum all values and take the maximum TTL for this specific period only + // Sum all values and take the maximum TTL (longest remaining time) val aggregatedValue = if (allValues.nonEmpty) Some(allValues.sum) else None val aggregatedTTL = if (allTTLs.nonEmpty) Some(allTTLs.max) else None - logger.debug(s"Period: $period, Aggregated value: $aggregatedValue, Aggregated TTL: $aggregatedTTL") - - // Final validation: ensure TTL is reasonable for this period - val validatedTTL = aggregatedTTL.filter { ttl => - val maxExpected = RateLimitingPeriod.toSeconds(period) - if (ttl > maxExpected) { - logger.warn(s"Aggregated TTL ($ttl) exceeds maximum for $period ($maxExpected). Clearing inconsistent keys.") - // Clear potentially corrupted keys - allKeys.foreach { key => - try { - val keyTTL = Redis.use(JedisMethod.TTL, key).map(_.toLong).getOrElse(-1L) - if (keyTTL > maxExpected) { - Redis.use(JedisMethod.DELETE, key) - logger.info(s"Deleted inconsistent key: $key (TTL was $keyTTL)") - } - } catch { - case e: Throwable => logger.error(s"Error cleaning key $key: $e") - } - } - false - } else { - true - } - } - - ((aggregatedValue, validatedTTL), period) + ((aggregatedValue, aggregatedTTL), period) } catch { case _: Throwable => // Fallback to legacy behavior if there's any error val key = createUniqueKey(consumerKey, period) val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong) val valueOpt: Option[Long] = Redis.use(JedisMethod.GET, key).map(_.toLong) - - // Validate legacy key TTL as well - val validatedLegacyTTL = ttlOpt.filter { ttl => - val maxExpected = RateLimitingPeriod.toSeconds(period) - if (ttl > maxExpected) { - logger.warn(s"Legacy key $key has invalid TTL ($ttl) for $period. Deleting.") - Redis.use(JedisMethod.DELETE, key) - false - } else { - true - } - } - - ((valueOpt, validatedLegacyTTL), period) + ((valueOpt, ttlOpt), period) } } @@ -398,83 +286,6 @@ object RateLimitingUtil extends MdcLoggable { Nil } - /** - * Diagnostic method to validate rate limiting logic and boundaries - * This method helps debug issues with TTL calculation - * @param consumerKey The consumer key to test - * @param period The period to test - * @return Debug information about the rate limiting logic - */ - def debugRateLimitingLogic(consumerKey: String, period: LimitCallPeriod): String = { - val debugInfo = StringBuilder.newBuilder - debugInfo.append(s"=== Debug Rate Limiting Logic for $consumerKey, $period ===\n") - - // Test calendar boundary logic - val usesCalendarBoundary = shouldUseCalendarBoundary(period) - debugInfo.append(s"Uses calendar boundary: $usesCalendarBoundary\n") - - if (usesCalendarBoundary) { - val boundary = getNextCalendarBoundary(period) - val currentTime = System.currentTimeMillis() / 1000 - val remainingSeconds = boundary - currentTime - debugInfo.append(s"Calendar boundary timestamp: $boundary\n") - debugInfo.append(s"Current timestamp: $currentTime\n") - debugInfo.append(s"Remaining seconds to boundary: $remainingSeconds\n") - } else { - val fixedTTL = RateLimitingPeriod.toSeconds(period) - debugInfo.append(s"Fixed TTL seconds: $fixedTTL\n") - } - - // Test key creation - val legacyKey = createUniqueKey(consumerKey, period) - debugInfo.append(s"Legacy key: $legacyKey\n") - - // Check what's actually in Redis - try { - val ttlValue = Redis.use(JedisMethod.TTL, legacyKey) - val keyValue = Redis.use(JedisMethod.GET, legacyKey) - debugInfo.append(s"Redis TTL for legacy key: $ttlValue\n") - debugInfo.append(s"Redis value for legacy key: $keyValue\n") - } catch { - case e: Throwable => - debugInfo.append(s"Error accessing Redis: $e\n") - } - - // Test the actual aggregation logic - try { - import code.ratelimiting.RateLimitingDI - import java.util.Date - import scala.concurrent.Await - import scala.concurrent.duration._ - - val allActiveLimits = Await.result( - RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerKey, new Date()), - 5.seconds - ) - - debugInfo.append(s"Active rate limits count: ${allActiveLimits.length}\n") - - allActiveLimits.foreach { limit => - val keyWithId = createUniqueKeyWithId(Some(limit.rateLimitingId), consumerKey, period) - debugInfo.append(s"Rate limit ID: ${limit.rateLimitingId}, Key: $keyWithId\n") - - try { - val ttl = Redis.use(JedisMethod.TTL, keyWithId) - val value = Redis.use(JedisMethod.GET, keyWithId) - debugInfo.append(s" TTL: $ttl, Value: $value\n") - } catch { - case e: Throwable => - debugInfo.append(s" Error: $e\n") - } - } - } catch { - case e: Throwable => - debugInfo.append(s"Error getting active limits: $e\n") - } - - debugInfo.toString() - } - /** * This function provides detailed rate limiting state for a consumer, showing information for each individual rate limiting record * @param consumerKey The consumer key to check From 9ff78b2a7878863ab7ad2887e5b9eb7f26c33fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 4 Nov 2025 13:37:10 +0100 Subject: [PATCH 11/18] Revert "feature/Rate Limiting endpoint tweaks 4" This reverts commit 0e5a0b4feaa978befe6f5db785f6f895ae384085. --- .../scala/code/api/util/AfterApiAuth.scala | 74 ++-- .../code/api/util/RateLimitingUtil.scala | 374 ++++-------------- 2 files changed, 108 insertions(+), 340 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala b/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala index fb768b32a1..0fc70591c1 100644 --- a/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala +++ b/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala @@ -96,10 +96,10 @@ object AfterApiAuth extends MdcLoggable{ * Please note that first source is the table RateLimiting and second is the table Consumer */ def checkRateLimiting(userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = { - def getActiveLimits(consumerId: String): Future[List[RateLimiting]] = { + def getRateLimiting(consumerId: String, version: String, name: String): Future[Box[RateLimiting]] = { RateLimitingUtil.useConsumerLimits match { - case true => RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerId, new Date()) - case false => Future(List.empty) + case true => RateLimitingDI.rateLimiting.vend.getByConsumerId(consumerId, version, name, Some(new Date())) + case false => Future(Empty) } } for { @@ -111,48 +111,34 @@ object AfterApiAuth extends MdcLoggable{ name = cc.flatMap(_.resourceDocument.map(_.partialFunctionName)) // 1st try: function name at resource doc .orElse(operationId) // 2nd try: In case of Dynamic Endpoint we can only use operationId .getOrElse("None") // Not found any unique identifier - activeLimits <- getActiveLimits(consumer.map(_.consumerId.get).getOrElse("")) + rateLimiting <- getRateLimiting(consumer.map(_.consumerId.get).getOrElse(""), version, name) } yield { - // Find the most specific rate limiting record for this request - def findBestMatch(limits: List[RateLimiting], version: String, name: String): Option[RateLimiting] = { - limits.find(rl => rl.apiVersion.contains(version) && rl.apiName.contains(name)) // 1st try: exact match - .orElse(limits.find(rl => rl.apiName.contains(name))) // 2nd try: match by name only - .orElse(limits.find(rl => rl.apiVersion.contains(version))) // 3rd try: match by version only - .orElse(limits.find(rl => rl.apiName.isEmpty && rl.apiVersion.isEmpty)) // 4th try: general consumer limit - .orElse(limits.headOption) // 5th try: any limit - } - - val limit: Option[CallLimit] = if (activeLimits.nonEmpty) { - findBestMatch(activeLimits, version, name) match { - case Some(rl) => Some(CallLimit( - Some(rl.rateLimitingId), - rl.consumerId, - rl.apiName, - rl.apiVersion, - rl.bankId, - rl.perSecondCallLimit, - rl.perMinuteCallLimit, - rl.perHourCallLimit, - rl.perDayCallLimit, - rl.perWeekCallLimit, - rl.perMonthCallLimit)) - case None => None - } - } else { - // Fallback to consumer limits if no database records found - consumer.map(c => CallLimit( - None, - c.consumerId.get, - None, - None, - None, - c.perSecondCallLimit.get, - c.perMinuteCallLimit.get, - c.perHourCallLimit.get, - c.perDayCallLimit.get, - c.perWeekCallLimit.get, - c.perMonthCallLimit.get - )) + val limit: Option[CallLimit] = rateLimiting match { + case Full(rl) => Some(CallLimit( + rl.consumerId, + rl.apiName, + rl.apiVersion, + rl.bankId, + rl.perSecondCallLimit, + rl.perMinuteCallLimit, + rl.perHourCallLimit, + rl.perDayCallLimit, + rl.perWeekCallLimit, + rl.perMonthCallLimit)) + case Empty => + Some(CallLimit( + consumer.map(_.consumerId.get).getOrElse(""), + None, + None, + None, + consumer.map(_.perSecondCallLimit.get).getOrElse(-1), + consumer.map(_.perMinuteCallLimit.get).getOrElse(-1), + consumer.map(_.perHourCallLimit.get).getOrElse(-1), + consumer.map(_.perDayCallLimit.get).getOrElse(-1), + consumer.map(_.perWeekCallLimit.get).getOrElse(-1), + consumer.map(_.perMonthCallLimit.get).getOrElse(-1) + )) + case _ => None } (user, cc.map(_.copy(rateLimiting = limit))) } diff --git a/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala b/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala index 2e73144ebc..a6ecb1df48 100644 --- a/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala @@ -8,7 +8,7 @@ import code.api.util.RateLimitingJson.CallLimit import code.util.Helper.MdcLoggable import com.openbankproject.commons.model.User import net.liftweb.common.{Box, Empty} - +import redis.clients.jedis.Jedis import scala.collection.immutable import scala.collection.immutable.{List, Nil} @@ -56,7 +56,6 @@ object RateLimitingPeriod extends Enumeration { object RateLimitingJson { case class CallLimit( - rate_limiting_id : Option[String], consumer_id : String, api_name : Option[String], api_version : Option[String], @@ -76,42 +75,6 @@ object RateLimitingUtil extends MdcLoggable { def useConsumerLimits = APIUtil.getPropsAsBoolValue("use_consumer_limits", false) private def createUniqueKey(consumerKey: String, period: LimitCallPeriod) = consumerKey + RateLimitingPeriod.toString(period) - - private def createUniqueKeyWithId(rateLimitingId: Option[String], consumerKey: String, period: LimitCallPeriod) = { - rateLimitingId match { - case Some(id) => s"${consumerKey}_${id}_${RateLimitingPeriod.toString(period)}" - case None => consumerKey + RateLimitingPeriod.toString(period) - } - } - - private def underConsumerLimitsWithId(rateLimitingId: Option[String], consumerKey: String, period: LimitCallPeriod, limit: Long): Boolean = { - if (useConsumerLimits) { - try { - (limit) match { - case l if l > 0 => // Redis is available and limit is set - val key = createUniqueKeyWithId(rateLimitingId, consumerKey, period) - val exists = Redis.use(JedisMethod.EXISTS,key).map(_.toBoolean).get - exists match { - case true => - val underLimit = Redis.use(JedisMethod.GET,key).get.toLong + 1 <= limit // +1 means we count the current call as well. We increment later i.e after successful call. - underLimit - case false => // In case that key does not exist we return successful result - true - } - case _ => - // Rate Limiting for a Consumer <= 0 implies successful result - // Or any other unhandled case implies successful result - true - } - } catch { - case e : Throwable => - logger.error(s"Redis issue: $e") - true - } - } else { - true // Rate Limiting disabled implies successful result - } - } private def underConsumerLimits(consumerKey: String, period: LimitCallPeriod, limit: Long): Boolean = { if (useConsumerLimits) { @@ -142,40 +105,13 @@ object RateLimitingUtil extends MdcLoggable { } } - private def incrementConsumerCountersWithId(rateLimitingId: Option[String], consumerKey: String, period: LimitCallPeriod, limit: Long): (Long, Long) = { - if (useConsumerLimits) { - try { - (limit) match { - case -1 => // Limit is not set for the period - skip processing - (-1, -1) - case _ => // Redis is available and limit is set - val key = createUniqueKeyWithId(rateLimitingId, consumerKey, period) - val ttl = Redis.use(JedisMethod.TTL, key).get.toInt - ttl match { - case -2 => // if the Key does not exists, -2 is returned - val seconds = RateLimitingPeriod.toSeconds(period).toInt - Redis.use(JedisMethod.SET,key, Some(seconds), Some("1")) - (seconds, 1) - case _ => // otherwise increment the counter - val cnt = Redis.use(JedisMethod.INCR,key).get.toInt - (ttl, cnt) - } - } - } catch { - case e : Throwable => - logger.error(s"Redis issue: $e") - (-1, -1) - } - } else { - (-1, -1) - } - } - private def incrementConsumerCounters(consumerKey: String, period: LimitCallPeriod, limit: Long): (Long, Long) = { if (useConsumerLimits) { try { (limit) match { - case -1 => // Limit is not set for the period - skip processing + case -1 => // Limit is not set for the period + val key = createUniqueKey(consumerKey, period) + Redis.use(JedisMethod.DELETE, key) (-1, -1) case _ => // Redis is available and limit is set val key = createUniqueKey(consumerKey, period) @@ -211,130 +147,31 @@ object RateLimitingUtil extends MdcLoggable { } } - private def ttlWithId(rateLimitingId: Option[String], consumerKey: String, period: LimitCallPeriod): Long = { - val key = createUniqueKeyWithId(rateLimitingId, consumerKey, period) - val ttl = Redis.use(JedisMethod.TTL, key).get.toInt - ttl match { - case -2 => // if the Key does not exists, -2 is returned - 0 - case _ => // otherwise increment the counter - ttl - } - } - def consumerRateLimitState(consumerKey: String): immutable.Seq[((Option[Long], Option[Long]), LimitCallPeriod)] = { - import code.ratelimiting.RateLimitingDI - import java.util.Date - import scala.concurrent.Await - import scala.concurrent.duration._ - - def getAggregatedInfo(consumerKey: String, period: LimitCallPeriod): ((Option[Long], Option[Long]), LimitCallPeriod) = { - try { - // Get all active rate limiting records for this consumer - val allActiveLimits = Await.result( - RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerKey, new Date()), - 5.seconds - ) - - // Collect all Redis keys for this period across all rate limiting records - val allKeys = allActiveLimits.map { rateLimitRecord => - createUniqueKeyWithId(Some(rateLimitRecord.rateLimitingId), consumerKey, period) - } :+ createUniqueKey(consumerKey, period) // Also include legacy key format - // Get values and TTLs for all keys and aggregate - val allValues = allKeys.flatMap { key => - try { - val valueOpt = Redis.use(JedisMethod.GET, key).map(_.toLong) - valueOpt - } catch { - case _: Throwable => None - } - } + def getInfo(consumerKey: String, period: LimitCallPeriod): ((Option[Long], Option[Long]), LimitCallPeriod) = { + val key = createUniqueKey(consumerKey, period) - val allTTLs = allKeys.flatMap { key => - try { - val ttlOpt = Redis.use(JedisMethod.TTL, key).map(_.toLong) - ttlOpt.filter(_ > -2) // Filter out non-existent keys (-2) - } catch { - case _: Throwable => None - } - } + // get TTL + val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong) - // Sum all values and take the maximum TTL (longest remaining time) - val aggregatedValue = if (allValues.nonEmpty) Some(allValues.sum) else None - val aggregatedTTL = if (allTTLs.nonEmpty) Some(allTTLs.max) else None + // get value (assuming string storage) + val valueOpt: Option[Long] = Redis.use(JedisMethod.GET, key).map(_.toLong) - ((aggregatedValue, aggregatedTTL), period) - } catch { - case _: Throwable => - // Fallback to legacy behavior if there's any error - val key = createUniqueKey(consumerKey, period) - val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong) - val valueOpt: Option[Long] = Redis.use(JedisMethod.GET, key).map(_.toLong) - ((valueOpt, ttlOpt), period) - } + ((valueOpt, ttlOpt), period) } - getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_SECOND) :: - getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_MINUTE) :: - getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_HOUR) :: - getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_DAY) :: - getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_WEEK) :: - getAggregatedInfo(consumerKey, RateLimitingPeriod.PER_MONTH) :: + getInfo(consumerKey, RateLimitingPeriod.PER_SECOND) :: + getInfo(consumerKey, RateLimitingPeriod.PER_MINUTE) :: + getInfo(consumerKey, RateLimitingPeriod.PER_HOUR) :: + getInfo(consumerKey, RateLimitingPeriod.PER_DAY) :: + getInfo(consumerKey, RateLimitingPeriod.PER_WEEK) :: + getInfo(consumerKey, RateLimitingPeriod.PER_MONTH) :: Nil } - /** - * This function provides detailed rate limiting state for a consumer, showing information for each individual rate limiting record - * @param consumerKey The consumer key to check - * @return A sequence of detailed rate limiting information for each active record - */ - def detailedConsumerRateLimitState(consumerKey: String): immutable.Seq[(String, String, immutable.Seq[((Option[Long], Option[Long]), LimitCallPeriod)])] = { - import code.ratelimiting.RateLimitingDI - import java.util.Date - import scala.concurrent.Await - import scala.concurrent.duration._ - - try { - // Get all active rate limiting records for this consumer - val allActiveLimits = Await.result( - RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerKey, new Date()), - 5.seconds - ) - - allActiveLimits.map { rateLimitRecord => - def getInfoForRecord(period: LimitCallPeriod): ((Option[Long], Option[Long]), LimitCallPeriod) = { - val key = createUniqueKeyWithId(Some(rateLimitRecord.rateLimitingId), consumerKey, period) - - try { - val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong).filter(_ > -2) - val valueOpt: Option[Long] = Redis.use(JedisMethod.GET, key).map(_.toLong) - ((valueOpt, ttlOpt), period) - } catch { - case _: Throwable => ((None, None), period) - } - } - - val recordInfo = - getInfoForRecord(RateLimitingPeriod.PER_SECOND) :: - getInfoForRecord(RateLimitingPeriod.PER_MINUTE) :: - getInfoForRecord(RateLimitingPeriod.PER_HOUR) :: - getInfoForRecord(RateLimitingPeriod.PER_DAY) :: - getInfoForRecord(RateLimitingPeriod.PER_WEEK) :: - getInfoForRecord(RateLimitingPeriod.PER_MONTH) :: - Nil - - val description = s"API: ${rateLimitRecord.apiName.getOrElse("*")} v${rateLimitRecord.apiVersion.getOrElse("*")} Bank: ${rateLimitRecord.bankId.getOrElse("*")}" - - (rateLimitRecord.rateLimitingId, description, recordInfo) - } - } catch { - case _: Throwable => List.empty - } - } - /** * This function checks rate limiting for a Consumer. * It will check rate limiting per minute, hour, day, week and month. @@ -358,22 +195,22 @@ object RateLimitingUtil extends MdcLoggable { case PER_MONTH => c.per_month case PER_YEAR => -1 } - userAndCallContext._2.map { cc: CallContext => - cc.copy(xRateLimitLimit = limit, xRateLimitReset = z._1, xRateLimitRemaining = limit - z._2) - } + userAndCallContext._2.map(_.copy(xRateLimitLimit = limit)) + .map(_.copy(xRateLimitReset = z._1)) + .map(_.copy(xRateLimitRemaining = limit - z._2)) } def setXRateLimitsAnonymous(id: String, z: (Long, Long), period: LimitCallPeriod): Option[CallContext] = { val limit = period match { case PER_HOUR => perHourLimitAnonymous case _ => -1 } - userAndCallContext._2.map { cc: CallContext => - cc.copy(xRateLimitLimit = limit, xRateLimitReset = z._1, xRateLimitRemaining = limit - z._2) - } + userAndCallContext._2.map(_.copy(xRateLimitLimit = limit)) + .map(_.copy(xRateLimitReset = z._1)) + .map(_.copy(xRateLimitRemaining = limit - z._2)) } def exceededRateLimit(c: CallLimit, period: LimitCallPeriod): Option[CallContextLight] = { - val remain = ttlWithId(c.rate_limiting_id, c.consumer_id, period) + val remain = ttl(c.consumer_id, period) val limit = period match { case PER_SECOND => c.per_second case PER_MINUTE => c.per_minute @@ -383,9 +220,9 @@ object RateLimitingUtil extends MdcLoggable { case PER_MONTH => c.per_month case PER_YEAR => -1 } - userAndCallContext._2.map { cc: CallContext => - cc.copy(xRateLimitLimit = limit, xRateLimitReset = remain, xRateLimitRemaining = 0).toLight - } + userAndCallContext._2.map(_.copy(xRateLimitLimit = limit)) + .map(_.copy(xRateLimitReset = remain)) + .map(_.copy(xRateLimitRemaining = 0)).map(_.toLight) } def exceededRateLimitAnonymous(id: String, period: LimitCallPeriod): Option[CallContextLight] = { @@ -394,119 +231,64 @@ object RateLimitingUtil extends MdcLoggable { case PER_HOUR => perHourLimitAnonymous case _ => -1 } - userAndCallContext._2.map { cc: CallContext => - cc.copy(xRateLimitLimit = limit, xRateLimitReset = remain, xRateLimitRemaining = 0).toLight - } + userAndCallContext._2.map(_.copy(xRateLimitLimit = limit)) + .map(_.copy(xRateLimitReset = remain)) + .map(_.copy(xRateLimitRemaining = 0)).map(_.toLight) } userAndCallContext._2 match { case Some(cc) => cc.rateLimiting match { case Some(rl) => // Authorized access - // Get all active rate limiting records for this consumer - import code.ratelimiting.RateLimitingDI - import java.util.Date - import scala.concurrent.Await - import scala.concurrent.duration._ - - val consumerId = rl.consumer_id - val allActiveLimits = try { - Await.result( - RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerId, new Date()), - 5.seconds - ) - } catch { - case _: Throwable => List.empty - } - - // Group rate limiting records by period and combine limits - val periodLimits = allActiveLimits.flatMap { rateLimitRecord => - List( - (PER_SECOND, rateLimitRecord.perSecondCallLimit), - (PER_MINUTE, rateLimitRecord.perMinuteCallLimit), - (PER_HOUR, rateLimitRecord.perHourCallLimit), - (PER_DAY, rateLimitRecord.perDayCallLimit), - (PER_WEEK, rateLimitRecord.perWeekCallLimit), - (PER_MONTH, rateLimitRecord.perMonthCallLimit) - ).filter(_._2 > 0).map { case (period, limit) => - (period, rateLimitRecord, limit) - } - }.groupBy(_._1) // Group by period - - // Check combined usage for each period - val combinedLimitChecks = periodLimits.map { case (period, recordsForPeriod) => - val combinedLimit = recordsForPeriod.map(_._3).sum // Sum all limits for this period - val currentUsage = recordsForPeriod.map { case (_, rateLimitRecord, individualLimit) => - val rateLimitingKey = - rateLimitRecord.consumerId + - rateLimitRecord.apiName.getOrElse("") + - rateLimitRecord.apiVersion.getOrElse("") + - rateLimitRecord.bankId.getOrElse("") - - // Get current usage for this specific record - try { - val key = createUniqueKeyWithId(Some(rateLimitRecord.rateLimitingId), rateLimitingKey, period) - Redis.use(JedisMethod.GET, key).map(_.toLong).getOrElse(0L) - } catch { - case _: Throwable => 0L - } - }.sum - - (period, combinedLimit, currentUsage + 1, recordsForPeriod) // +1 for current request - } - - // Check if any combined limit is exceeded - val exceededCombinedLimit = combinedLimitChecks.find { case (_, combinedLimit, totalUsage, _) => - totalUsage > combinedLimit - } - - exceededCombinedLimit match { - case Some((period, combinedLimit, _, recordsForPeriod)) => - // Use the first record for error response - val firstRecord = recordsForPeriod.head._2 - val callLimit = CallLimit( - Some(firstRecord.rateLimitingId), - firstRecord.consumerId, - firstRecord.apiName, - firstRecord.apiVersion, - firstRecord.bankId, - firstRecord.perSecondCallLimit, - firstRecord.perMinuteCallLimit, - firstRecord.perHourCallLimit, - firstRecord.perDayCallLimit, - firstRecord.perWeekCallLimit, - firstRecord.perMonthCallLimit + val rateLimitingKey = + rl.consumer_id + + rl.api_name.getOrElse("") + + rl.api_version.getOrElse("") + + rl.bank_id.getOrElse("") + val checkLimits = List( + underConsumerLimits(rateLimitingKey, PER_SECOND, rl.per_second), + underConsumerLimits(rateLimitingKey, PER_MINUTE, rl.per_minute), + underConsumerLimits(rateLimitingKey, PER_HOUR, rl.per_hour), + underConsumerLimits(rateLimitingKey, PER_DAY, rl.per_day), + underConsumerLimits(rateLimitingKey, PER_WEEK, rl.per_week), + underConsumerLimits(rateLimitingKey, PER_MONTH, rl.per_month) + ) + checkLimits match { + case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x1 == false => + (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_SECOND, rl.per_second), 429, exceededRateLimit(rl, PER_SECOND))), userAndCallContext._2) + case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x2 == false => + (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_MINUTE, rl.per_minute), 429, exceededRateLimit(rl, PER_MINUTE))), userAndCallContext._2) + case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x3 == false => + (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_HOUR, rl.per_hour), 429, exceededRateLimit(rl, PER_HOUR))), userAndCallContext._2) + case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x4 == false => + (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_DAY, rl.per_day), 429, exceededRateLimit(rl, PER_DAY))), userAndCallContext._2) + case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x5 == false => + (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_WEEK, rl.per_week), 429, exceededRateLimit(rl, PER_WEEK))), userAndCallContext._2) + case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x6 == false => + (fullBoxOrException(Empty ~> APIFailureNewStyle(composeMsgAuthorizedAccess(PER_MONTH, rl.per_month), 429, exceededRateLimit(rl, PER_MONTH))), userAndCallContext._2) + case _ => + val incrementCounters = List ( + incrementConsumerCounters(rateLimitingKey, PER_SECOND, rl.per_second), // Responses other than the 429 status code MUST be stored by a cache. + incrementConsumerCounters(rateLimitingKey, PER_MINUTE, rl.per_minute), // Responses other than the 429 status code MUST be stored by a cache. + incrementConsumerCounters(rateLimitingKey, PER_HOUR, rl.per_hour), // Responses other than the 429 status code MUST be stored by a cache. + incrementConsumerCounters(rateLimitingKey, PER_DAY, rl.per_day), // Responses other than the 429 status code MUST be stored by a cache. + incrementConsumerCounters(rateLimitingKey, PER_WEEK, rl.per_week), // Responses other than the 429 status code MUST be stored by a cache. + incrementConsumerCounters(rateLimitingKey, PER_MONTH, rl.per_month) // Responses other than the 429 status code MUST be stored by a cache. ) - val errorMsg = composeMsgAuthorizedAccess(period, combinedLimit) - (fullBoxOrException(Empty ~> APIFailureNewStyle(errorMsg, 429, exceededRateLimit(callLimit, period))), userAndCallContext._2) - case None => - // Increment counters for all active limits - val allIncrementCounters = periodLimits.flatMap { case (period, recordsForPeriod) => - recordsForPeriod.map { case (_, rateLimitRecord, limit) => - val rateLimitingKey = - rateLimitRecord.consumerId + - rateLimitRecord.apiName.getOrElse("") + - rateLimitRecord.apiVersion.getOrElse("") + - rateLimitRecord.bankId.getOrElse("") - (period, incrementConsumerCountersWithId(Some(rateLimitRecord.rateLimitingId), rateLimitingKey, period, limit)) - } - } - - // Find the first active counter to set rate limit headers - allIncrementCounters.find(_._2._1 > 0) match { - case Some((period, counter)) => - // Create a combined limit for headers - val combinedLimitForPeriod = periodLimits.get(period).map(_.map(_._3).sum).getOrElse(0L) - val modifiedRl = rl.copy( - per_second = if (period == PER_SECOND) combinedLimitForPeriod else rl.per_second, - per_minute = if (period == PER_MINUTE) combinedLimitForPeriod else rl.per_minute, - per_hour = if (period == PER_HOUR) combinedLimitForPeriod else rl.per_hour, - per_day = if (period == PER_DAY) combinedLimitForPeriod else rl.per_day, - per_week = if (period == PER_WEEK) combinedLimitForPeriod else rl.per_week, - per_month = if (period == PER_MONTH) combinedLimitForPeriod else rl.per_month - ) - (userAndCallContext._1, setXRateLimits(modifiedRl, counter, period)) - case None => + incrementCounters match { + case first :: _ :: _ :: _ :: _ :: _ :: Nil if first._1 > 0 => + (userAndCallContext._1, setXRateLimits(rl, first, PER_SECOND)) + case _ :: second :: _ :: _ :: _ :: _ :: Nil if second._1 > 0 => + (userAndCallContext._1, setXRateLimits(rl, second, PER_MINUTE)) + case _ :: _ :: third :: _ :: _ :: _ :: Nil if third._1 > 0 => + (userAndCallContext._1, setXRateLimits(rl, third, PER_HOUR)) + case _ :: _ :: _ :: fourth :: _ :: _ :: Nil if fourth._1 > 0 => + (userAndCallContext._1, setXRateLimits(rl, fourth, PER_DAY)) + case _ :: _ :: _ :: _ :: fifth :: _ :: Nil if fifth._1 > 0 => + (userAndCallContext._1, setXRateLimits(rl, fifth, PER_WEEK)) + case _ :: _ :: _ :: _ :: _ :: sixth :: Nil if sixth._1 > 0 => + (userAndCallContext._1, setXRateLimits(rl, sixth, PER_MONTH)) + case _ => (userAndCallContext._1, userAndCallContext._2) } } From 5f0cdf967e2c71b244fe45d52d2b73cefdf79141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 4 Nov 2025 14:27:09 +0100 Subject: [PATCH 12/18] bugfix/fix the rate limiting issue Problem Analysis The issue was in the `checkRateLimiting` method in `AfterApiAuth.scala`. The original code was only retrieving a single rate limiting record per consumer using `getByConsumerId`, but when multiple active rate limiting records exist for the same consumer (as shown in your table), they should be aggregated together. I modified the `checkRateLimiting` method to: 1. **Retrieve all active rate limiting records** for a consumer using `getActiveCallLimitsByConsumerIdAtDate()` instead of just one record 2. **Aggregate the limits properly** by summing up positive values for each time period (per second, per minute, per hour, per day, per week, per month) 3. **Handle edge cases** where some limits are -1 (unlimited) by using -1 if any record has -1 for that period, otherwise summing the positive values --- .../scala/code/api/util/AfterApiAuth.scala | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala b/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala index 0fc70591c1..13eae4fc40 100644 --- a/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala +++ b/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala @@ -96,38 +96,47 @@ object AfterApiAuth extends MdcLoggable{ * Please note that first source is the table RateLimiting and second is the table Consumer */ def checkRateLimiting(userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = { - def getRateLimiting(consumerId: String, version: String, name: String): Future[Box[RateLimiting]] = { + def getActiveRateLimitings(consumerId: String): Future[List[RateLimiting]] = { RateLimitingUtil.useConsumerLimits match { - case true => RateLimitingDI.rateLimiting.vend.getByConsumerId(consumerId, version, name, Some(new Date())) - case false => Future(Empty) + case true => RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerId, new Date()) + case false => Future(List.empty) } } + + def aggregateLimits(limits: List[RateLimiting], consumerId: String): CallLimit = { + def sumLimits(values: List[Long]): Long = { + val positiveValues = values.filter(_ > 0) + if (positiveValues.isEmpty) -1 else positiveValues.sum + } + + if (limits.nonEmpty) { + CallLimit( + consumerId, + limits.find(_.apiName.isDefined).flatMap(_.apiName), + limits.find(_.apiVersion.isDefined).flatMap(_.apiVersion), + limits.find(_.bankId.isDefined).flatMap(_.bankId), + sumLimits(limits.map(_.perSecondCallLimit)), + sumLimits(limits.map(_.perMinuteCallLimit)), + sumLimits(limits.map(_.perHourCallLimit)), + sumLimits(limits.map(_.perDayCallLimit)), + sumLimits(limits.map(_.perWeekCallLimit)), + sumLimits(limits.map(_.perMonthCallLimit)) + ) + } else { + CallLimit(consumerId, None, None, None, -1, -1, -1, -1, -1, -1) + } + } + for { (user, cc) <- userIsLockedOrDeleted consumer = cc.flatMap(_.consumer) - version = cc.map(_.implementedInVersion).getOrElse("None") // Calculate apiVersion in case of Rate Limiting - operationId = cc.flatMap(_.operationId) // Unique Identifier of Dynamic Endpoints - // Calculate apiName in case of Rate Limiting - name = cc.flatMap(_.resourceDocument.map(_.partialFunctionName)) // 1st try: function name at resource doc - .orElse(operationId) // 2nd try: In case of Dynamic Endpoint we can only use operationId - .getOrElse("None") // Not found any unique identifier - rateLimiting <- getRateLimiting(consumer.map(_.consumerId.get).getOrElse(""), version, name) + consumerId = consumer.map(_.consumerId.get).getOrElse("") + rateLimitings <- getActiveRateLimitings(consumerId) } yield { - val limit: Option[CallLimit] = rateLimiting match { - case Full(rl) => Some(CallLimit( - rl.consumerId, - rl.apiName, - rl.apiVersion, - rl.bankId, - rl.perSecondCallLimit, - rl.perMinuteCallLimit, - rl.perHourCallLimit, - rl.perDayCallLimit, - rl.perWeekCallLimit, - rl.perMonthCallLimit)) - case Empty => + val limit: Option[CallLimit] = rateLimitings match { + case Nil => // No rate limiting records found, use consumer defaults Some(CallLimit( - consumer.map(_.consumerId.get).getOrElse(""), + consumerId, None, None, None, @@ -138,7 +147,8 @@ object AfterApiAuth extends MdcLoggable{ consumer.map(_.perWeekCallLimit.get).getOrElse(-1), consumer.map(_.perMonthCallLimit.get).getOrElse(-1) )) - case _ => None + case activeLimits => // Aggregate multiple rate limiting records + Some(aggregateLimits(activeLimits, consumerId)) } (user, cc.map(_.copy(rateLimiting = limit))) } From 1a241d118238bd29e9d4496fcca8934762d1c3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 5 Nov 2025 07:58:32 +0100 Subject: [PATCH 13/18] test/Fix rate limiting v4.0.0 tests --- .../scala/code/api/v4_0_0/RateLimitingTest.scala | 14 +++++++------- .../scala/code/api/v4_0_0/V400ServerSetup.scala | 9 ++++++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/RateLimitingTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/RateLimitingTest.scala index f268a8b7d3..d0a76181c5 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/RateLimitingTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/RateLimitingTest.scala @@ -137,7 +137,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { response03.code should equal(429) // Revert to initial state - val response04 = setRateLimiting(user1, callLimitJsonInitial) + val response04 = setRateLimiting2(user1, callLimitJsonInitial) Then("We should get a 200") response04.code should equal(200) } @@ -159,7 +159,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { response03.code should equal(429) // Revert to initial state - val response04 = setRateLimiting(user1, callLimitJsonInitial) + val response04 = setRateLimiting2(user1, callLimitJsonInitial) Then("We should get a 200") response04.code should equal(200) } @@ -181,7 +181,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { response03.code should equal(429) // Revert to initial state - val response04 = setRateLimiting(user1, callLimitJsonInitial) + val response04 = setRateLimiting2(user1, callLimitJsonInitial) Then("We should get a 200") response04.code should equal(200) } @@ -203,7 +203,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { response03.code should equal(429) // Revert to initial state - val response04 = setRateLimiting(user1, callLimitJsonInitial) + val response04 = setRateLimiting2(user1, callLimitJsonInitial) Then("We should get a 200") response04.code should equal(200) } @@ -225,7 +225,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { response03.code should equal(429) // Revert to initial state - val response04 = setRateLimiting(user1, callLimitJsonInitial) + val response04 = setRateLimiting2(user1, callLimitJsonInitial) Then("We should get a 200") response04.code should equal(200) } @@ -248,7 +248,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { val operationId = "dynamicEndpoint_GET_accounts_ACCOUNT_ID" val apiName = "dynamicEndpoint_GET_accounts_ACCOUNT_ID" val apiVersion = ApiVersion.`dynamic-endpoint`.toString() - val response01 = setRateLimiting(user1, callLimitJsonHour.copy(api_name = Some(apiName), api_version = Some(apiVersion))) + val response01 = setRateLimiting2(user1, callLimitJsonHour.copy(api_name = Some(apiName), api_version = Some(apiVersion))) Then("We should get a 200") response01.code should equal(200) @@ -263,7 +263,7 @@ class RateLimitingTest extends V400ServerSetup with PropsReset { makeGetRequest(requestDynamicEndpoint.GET <@(user1)).code should equal(429) // Revert Rate Limiting to initial state in case of a Dynamic Endpoint - val response02 = setRateLimiting(user1, callLimitJsonInitial.copy(api_name = Some(apiName), api_version = Some(apiVersion))) + val response02 = setRateLimiting2(user1, callLimitJsonInitial.copy(api_name = Some(apiName), api_version = Some(apiVersion))) Then("We should get a 200") response02.code should equal(200) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala index aa07a85466..c96b5fb422 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala @@ -107,7 +107,14 @@ trait V400ServerSetup extends ServerSetupWithTestData with DefaultUsers { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(consumerAndToken) makePutRequest(request400, write(putJson)) - } + } + def setRateLimiting2(consumerAndToken: Option[(Consumer, Token)], putJson: CallLimitPostJsonV400): APIResponse = { + val Some((c, _)) = consumerAndToken + val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") + Entitlement.entitlement.vend.addEntitlement("", resourceUser2.userId, ApiRole.CanUpdateRateLimits.toString) + val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@ user2 + makePutRequest(request400, write(putJson)) + } def setRateLimitingWithoutRole(consumerAndToken: Option[(Consumer, Token)], putJson: CallLimitPostJsonV400): APIResponse = { val Some((c, _)) = consumerAndToken val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") From 46028185cce4b322547f5b368f686eaf5a27b83c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 5 Nov 2025 10:58:17 +0100 Subject: [PATCH 14/18] feature/check NIST messages at startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **🔹 Without NVD API Key** (Default Development): ```OBP-API/pom.xml#L1-2 export MAVEN_OPTS="-Xss128m" && mvn install -pl .,obp-commons ``` **🔹 With Valid NVD API Key** (Production/Security Scanning): ```OBP-API/pom.xml#L1-3 export NVD_API_KEY=your_real_api_key export MAVEN_OPTS="-Xss128m" && mvn install -pl .,obp-commons ``` You can also manually control it: ```OBP-API/pom.xml#L1-5 mvn install -Pdependency-check mvn install -P '!dependency-check' ``` ✅ **Zero 403 Errors**: Plugin only loads when API key is available ✅ **Clean Development**: No network calls or security scanning during normal dev work ✅ **CI/CD Friendly**: Easy to enable/disable via environment variables ✅ **No Build Failures**: Development builds never fail due to network issues ✅ **Production Ready**: Full vulnerability scanning when API key is provided ```OBP-API/pom.xml#L1-2 export MAVEN_OPTS="-Xss128m" && mvn install -pl .,obp-commons && mvn jetty:run -pl obp-api ``` This will run **without any 403 errors** and complete successfully for development work! When you're ready for production security scanning, just get a free NVD API key from https://nvd.nist.gov/developers/request-an-api-key and set it as an environment variable. --- obp-commons/pom.xml | 82 ++++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/obp-commons/pom.xml b/obp-commons/pom.xml index b41909faf1..eac3d3709e 100644 --- a/obp-commons/pom.xml +++ b/obp-commons/pom.xml @@ -91,30 +91,7 @@ - - org.owasp - dependency-check-maven - 7.1.1 - - notifier-dependency-check - HTML - 10 - false - true - - true - true - false - pom - - - - - aggregate - - - - + org.apache.maven.plugins maven-surefire-plugin @@ -178,4 +155,61 @@ + + + + dependency-check + + + + env.NVD_API_KEY + + + + + + + org.owasp + dependency-check-maven + 8.4.3 + + + ${env.NVD_API_KEY} + true + false + true + + notifier-dependency-check + HTML + ${project.build.directory}/dependency-check-report + 10 + false + true + true + true + pom + + + false + false + false + false + false + false + + + + + dependency-check + verify + + check + + + + + + + + From c8bfe81c79868398e55d3b9544e3938fd4d68f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 5 Nov 2025 13:17:10 +0100 Subject: [PATCH 15/18] feature/Revert rate limiting v3.1.0 and v4.0.0 paths --- .../scala/code/api/v3_1_0/APIMethods310.scala | 8 +++---- .../scala/code/api/v4_0_0/APIMethods400.scala | 4 ++-- .../scala/code/api/v3_1_0/RateLimitTest.scala | 24 +++++++++---------- .../code/api/v4_0_0/V400ServerSetup.scala | 8 +++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index ed3490de02..abc3f17f47 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -508,7 +508,7 @@ trait APIMethods310 { implementedInApiVersion, nameOf(callsLimit), "PUT", - "/management/consumers/CONSUMER_ID/consumer/rate-limits", + "/management/consumers/CONSUMER_ID/consumer/call-limits", "Set Rate Limits (call limits) per Consumer", s""" |Set the API rate limiting (call limits) per Consumer: @@ -540,7 +540,7 @@ trait APIMethods310 { Some(List(canUpdateRateLimits))) lazy val callsLimit : OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: Nil JsonPut json -> _ => { + case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonPut json -> _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { (Full(u), callContext) <- authenticatedAccess(cc) @@ -580,7 +580,7 @@ trait APIMethods310 { "/management/consumers/CONSUMER_ID/consumer/call-limits", "Get Rate Limits for a Consumer", s""" - |Get Calls limits per Consumer. + |Get Rate Limits per Consumer. |${userAuthenticationMessage(true)} | |""".stripMargin, @@ -601,7 +601,7 @@ trait APIMethods310 { lazy val getCallsLimit : OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: Nil JsonGet _ => { + case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonGet _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { (Full(u), callContext) <- authenticatedAccess(cc) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 6aa2f024a3..3f5b94eec0 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -173,7 +173,7 @@ trait APIMethods400 extends MdcLoggable { implementedInApiVersion, nameOf(callsLimit), "PUT", - "/management/consumers/CONSUMER_ID/consumer/rate-limits", + "/management/consumers/CONSUMER_ID/consumer/call-limits", "Set Rate Limits / Call Limits per Consumer", s""" |Set the API rate limits / call limits for a Consumer: @@ -205,7 +205,7 @@ trait APIMethods400 extends MdcLoggable { Some(List(canUpdateRateLimits))) lazy val callsLimit : OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: Nil JsonPut json -> _ => { + case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonPut json -> _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { (Full(u), callContext) <- authenticatedAccess(cc) diff --git a/obp-api/src/test/scala/code/api/v3_1_0/RateLimitTest.scala b/obp-api/src/test/scala/code/api/v3_1_0/RateLimitTest.scala index a781d376af..6aeca45e0e 100644 --- a/obp-api/src/test/scala/code/api/v3_1_0/RateLimitTest.scala +++ b/obp-api/src/test/scala/code/api/v3_1_0/RateLimitTest.scala @@ -148,7 +148,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { When("We make a request v3.1.0") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT val response310 = makePutRequest(request310, write(callLimitJson1)) Then("We should get a 401") response310.code should equal(401) @@ -159,7 +159,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { When("We make a request v3.1.0 without a Role " + ApiRole.canUpdateRateLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) val response310 = makePutRequest(request310, write(callLimitJson1)) Then("We should get a 403") response310.code should equal(403) @@ -171,7 +171,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) val response310 = makePutRequest(request310, write(callLimitJson1)) Then("We should get a 200") response310.code should equal(200) @@ -184,7 +184,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitSecondJson)) Then("We should get a 200") response01.code should equal(200) @@ -209,7 +209,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitMinuteJson)) Then("We should get a 200") response01.code should equal(200) @@ -234,7 +234,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitHourJson)) Then("We should get a 200") response01.code should equal(200) @@ -259,7 +259,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitDayJson)) Then("We should get a 200") response01.code should equal(200) @@ -284,7 +284,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitWeekJson)) Then("We should get a 200") response01.code should equal(200) @@ -309,7 +309,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") val id: Long = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.id.get).getOrElse(0) Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(user1) val response01 = makePutRequest(request310, write(callLimitMonthJson)) Then("We should get a 200") response01.code should equal(200) @@ -335,7 +335,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { When("We make a request v3.1.0") val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").GET + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET val response310 = makeGetRequest(request310) Then("We should get a 401") response310.code should equal(401) @@ -346,7 +346,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { When("We make a request v3.1.0 without a Role " + ApiRole.canReadCallLimits) val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").GET <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET <@(user1) val response310 = makeGetRequest(request310) Then("We should get a 403") response310.code should equal(403) @@ -358,7 +358,7 @@ class RateLimitTest extends V310ServerSetup with PropsReset { val Some((c, _)) = user1 val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanReadCallLimits.toString) - val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").GET <@(user1) + val request310 = (v3_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").GET <@(user1) val response310 = makeGetRequest(request310) Then("We should get a 200") response310.code should equal(200) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala index c96b5fb422..dbd09abf61 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala @@ -105,24 +105,24 @@ trait V400ServerSetup extends ServerSetupWithTestData with DefaultUsers { val Some((c, _)) = consumerAndToken val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) - val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(consumerAndToken) + val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(consumerAndToken) makePutRequest(request400, write(putJson)) } def setRateLimiting2(consumerAndToken: Option[(Consumer, Token)], putJson: CallLimitPostJsonV400): APIResponse = { val Some((c, _)) = consumerAndToken val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") Entitlement.entitlement.vend.addEntitlement("", resourceUser2.userId, ApiRole.CanUpdateRateLimits.toString) - val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@ user2 + val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@ user2 makePutRequest(request400, write(putJson)) } def setRateLimitingWithoutRole(consumerAndToken: Option[(Consumer, Token)], putJson: CallLimitPostJsonV400): APIResponse = { val Some((c, _)) = consumerAndToken val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") - val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@(consumerAndToken) + val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@(consumerAndToken) makePutRequest(request400, write(putJson)) } def setRateLimitingAnonymousAccess(putJson: CallLimitPostJsonV400): APIResponse = { - val request400 = (v4_0_0_Request / "management" / "consumers" / "some_consumer_id" / "consumer" / "rate-limits").PUT + val request400 = (v4_0_0_Request / "management" / "consumers" / "some_consumer_id" / "consumer" / "call-limits").PUT makePutRequest(request400, write(putJson)) } From c1585913ed6f197ba5a6a75b69e8101a066be06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 5 Nov 2025 14:09:48 +0100 Subject: [PATCH 16/18] feature/Revert rate limiting v3.1.0 and v4.0.0 paths --- obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala b/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala index 7ac569a749..cbea88008e 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala @@ -38,7 +38,7 @@ trait V510ServerSetup extends ServerSetupWithTestData with DefaultUsers { val Some((c, _)) = consumerAndToken val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanUpdateRateLimits.toString) - val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").PUT <@ (consumerAndToken) + val request400 = (v4_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "call-limits").PUT <@ (consumerAndToken) makePutRequest(request400, write(putJson)) } From 747ac37882a5c45052422bc244e3d7f0f389a688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 5 Nov 2025 15:05:30 +0100 Subject: [PATCH 17/18] refactor/Removed 9 redundant patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Removed 9 redundant patterns** by combining spaced and non-spaced arrow operators into single flexible patterns: - **client_secret**: 3 patterns → 2 patterns (removed the `client_secret->` pattern) - **access_token**: 3 patterns → 2 patterns (removed the `access_token->` pattern) - **refresh_token**: 3 patterns → 2 patterns (removed the `refresh_token->` pattern) - **id_token**: 3 patterns → 2 patterns (removed the `id_token->` pattern) - **token**: 3 patterns → 2 patterns (removed the `token->` pattern) - **password**: 3 patterns → 2 patterns (removed the `password->` pattern) - **api_key**: 3 patterns → 2 patterns (removed the `api_key->` pattern) - **key**: 3 patterns → 2 patterns (removed the `key->` pattern) - **private_key**: 3 patterns → 2 patterns (removed the `private_key->` pattern) --- .../main/scala/code/util/SecureLogging.scala | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/obp-api/src/main/scala/code/util/SecureLogging.scala b/obp-api/src/main/scala/code/util/SecureLogging.scala index 00f6257325..1c3b28b0f4 100644 --- a/obp-api/src/main/scala/code/util/SecureLogging.scala +++ b/obp-api/src/main/scala/code/util/SecureLogging.scala @@ -40,9 +40,6 @@ object SecureLogging { conditionalPattern("securelogging_mask_client_secret") { (Pattern.compile("(?i)(client_secret\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") }, - conditionalPattern("securelogging_mask_client_secret") { - (Pattern.compile("(?i)(client_secret->)([^,\\s&\\)]+)"), "$1***") - }, // Authorization / Tokens conditionalPattern("securelogging_mask_authorization") { @@ -54,36 +51,24 @@ object SecureLogging { conditionalPattern("securelogging_mask_access_token") { (Pattern.compile("(?i)(access_token\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") }, - conditionalPattern("securelogging_mask_access_token") { - (Pattern.compile("(?i)(access_token->)([^,\\s&\\)]+)"), "$1***") - }, conditionalPattern("securelogging_mask_refresh_token") { (Pattern.compile("(?i)(refresh_token[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, conditionalPattern("securelogging_mask_refresh_token") { (Pattern.compile("(?i)(refresh_token\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") }, - conditionalPattern("securelogging_mask_refresh_token") { - (Pattern.compile("(?i)(refresh_token->)([^,\\s&\\)]+)"), "$1***") - }, conditionalPattern("securelogging_mask_id_token") { (Pattern.compile("(?i)(id_token[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, conditionalPattern("securelogging_mask_id_token") { (Pattern.compile("(?i)(id_token\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") }, - conditionalPattern("securelogging_mask_id_token") { - (Pattern.compile("(?i)(id_token->)([^,\\s&\\)]+)"), "$1***") - }, conditionalPattern("securelogging_mask_token") { (Pattern.compile("(?i)(token[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, conditionalPattern("securelogging_mask_token") { (Pattern.compile("(?i)(token\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") }, - conditionalPattern("securelogging_mask_token") { - (Pattern.compile("(?i)(token->)([^,\\s&\\)]+)"), "$1***") - }, // Passwords conditionalPattern("securelogging_mask_password") { @@ -92,9 +77,6 @@ object SecureLogging { conditionalPattern("securelogging_mask_password") { (Pattern.compile("(?i)(password\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") }, - conditionalPattern("securelogging_mask_password") { - (Pattern.compile("(?i)(password->)([^,\\s&\\)]+)"), "$1***") - }, // API keys conditionalPattern("securelogging_mask_api_key") { @@ -103,27 +85,18 @@ object SecureLogging { conditionalPattern("securelogging_mask_api_key") { (Pattern.compile("(?i)(api_key\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") }, - conditionalPattern("securelogging_mask_api_key") { - (Pattern.compile("(?i)(api_key->)([^,\\s&\\)]+)"), "$1***") - }, conditionalPattern("securelogging_mask_key") { (Pattern.compile("(?i)(key[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, conditionalPattern("securelogging_mask_key") { (Pattern.compile("(?i)(key\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") }, - conditionalPattern("securelogging_mask_key") { - (Pattern.compile("(?i)(key->)([^,\\s&\\)]+)"), "$1***") - }, conditionalPattern("securelogging_mask_private_key") { (Pattern.compile("(?i)(private_key[\"']?\\s*[:=]\\s*[\"']?)([^\"',\\s&]+)"), "$1***") }, conditionalPattern("securelogging_mask_private_key") { (Pattern.compile("(?i)(private_key\\s*->\\s*)([^,\\s&\\)]+)"), "$1***") }, - conditionalPattern("securelogging_mask_private_key") { - (Pattern.compile("(?i)(private_key->)([^,\\s&\\)]+)"), "$1***") - }, // Database conditionalPattern("securelogging_mask_jdbc") { From c2e43a2c242be8f062125f42706668c498df7847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 5 Nov 2025 15:08:39 +0100 Subject: [PATCH 18/18] docfix/Remove outdated comments --- .../src/main/scala/code/ratelimiting/MappedRateLimiting.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala b/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala index 23bd41ba50..844e38f5d4 100644 --- a/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala +++ b/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala @@ -153,7 +153,6 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait { c.ApiVersion(apiVersion.orNull) c.ConsumerId(consumerId) - // 👇 bump timestamp for last-write-wins c.updatedAt(new Date()) c.saveMe() @@ -192,7 +191,6 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait { c.ApiVersion(apiVersion.orNull) c.ConsumerId(consumerId) - // 👇 bump timestamp for last-write-wins c.updatedAt(new Date()) c.saveMe()