feature/Add function getCallsLimit v5.1.0

This commit is contained in:
Marko Milić 2025-09-05 08:31:47 +02:00
parent f2a1eacaec
commit d1e3e819a5
7 changed files with 305 additions and 58 deletions

View File

@ -29,7 +29,7 @@ import code.api.v3_1_0._
import code.api.v4_0_0.JSONFactory400.{createAccountBalancesJson, createBalancesJson, createNewCoreBankAccountJson}
import code.api.v4_0_0._
import code.api.v5_0_0.JSONFactory500
import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson}
import code.api.v5_1_0.JSONFactory510.{createCallLimitJson, createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson}
import code.atmattribute.AtmAttribute
import code.bankconnectors.Connector
import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent}
@ -39,6 +39,7 @@ import code.loginattempts.LoginAttempt
import code.metrics.APIMetrics
import code.model.dataAccess.{AuthUser, MappedBankAccount}
import code.model.{AppType, Consumer}
import code.ratelimiting.{RateLimiting, RateLimitingDI}
import code.regulatedentities.MappedRegulatedEntityProvider
import code.userlocks.UserLocksProvider
import code.users.Users
@ -3290,6 +3291,50 @@ trait APIMethods510 {
}
staticResourceDocs += ResourceDoc(
getCallsLimit,
implementedInApiVersion,
nameOf(getCallsLimit),
"GET",
"/management/consumers/CONSUMER_ID/consumer/call-limits",
"Get Call Limits for a Consumer",
s"""
|Get Calls limits per Consumer.
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
EmptyBody,
callLimitJson,
List(
$UserNotLoggedIn,
InvalidJsonFormat,
InvalidConsumerId,
ConsumerNotFoundByConsumerId,
UserHasMissingRoles,
UpdateConsumerError,
UnknownError
),
List(apiTagConsumer),
Some(List(canReadCallLimits)))
lazy val getCallsLimit: OBPEndpoint = {
case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonGet _ => {
cc =>
implicit val ec = EndpointContext(Some(cc))
for {
// (Full(u), callContext) <- authenticatedAccess(cc)
// _ <- NewStyle.function.hasEntitlement("", cc.userId, canReadCallLimits, callContext)
consumer <- NewStyle.function.getConsumerByConsumerId(consumerId, cc.callContext)
rateLimiting: Option[RateLimiting] <- RateLimitingDI.rateLimiting.vend.findMostRecentRateLimit(consumerId, None, None, None)
rateLimit <- Future(RateLimitingUtil.consumerRateLimitState(consumer.consumerId.get).toList)
} yield {
(createCallLimitJson(consumer, rateLimiting, rateLimit), HttpCode.`200`(cc.callContext))
}
}
}
staticResourceDocs += ResourceDoc(
updateConsumerRedirectURL,
implementedInApiVersion,

View File

@ -31,6 +31,7 @@ import code.api.berlin.group.ConstantsBG
import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.ConsentAccessJson
import code.api.util.APIUtil.{DateWithDay, DateWithSeconds, gitCommit, stringOrNull}
import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet
import code.api.util.RateLimitingPeriod.LimitCallPeriod
import code.api.util._
import code.api.v1_2_1.BankRoutingJsonV121
import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeJsonV140, LocationJsonV140, MetaJsonV140, TransactionRequestAccountJsonV140, transformToLocationFromV140, transformToMetaFromV140}
@ -38,6 +39,7 @@ import code.api.v2_0_0.TransactionRequestChargeJsonV200
import code.api.v2_1_0.ResourceUserJSON
import code.api.v3_0_0.JSONFactory300.{createLocationJson, createMetaJson, transformToAddressFromV300}
import code.api.v3_0_0.{AddressJsonV300, OpeningTimesV300}
import code.api.v3_1_0.{CallLimitJson, RateLimit, RedisCallLimitJson}
import code.api.v4_0_0.{EnergySource400, HostedAt400, HostedBy400}
import code.api.v5_0_0.PostConsentRequestJsonV500
import code.atmattribute.AtmAttribute
@ -45,6 +47,7 @@ import code.atms.Atms.Atm
import code.consent.MappedConsent
import code.metrics.APIMetric
import code.model.Consumer
import code.ratelimiting.RateLimiting
import code.users.{UserAttribute, Users}
import code.util.Helper.MdcLoggable
import code.views.system.{AccountAccess, ViewDefinition, ViewPermission}
@ -147,8 +150,8 @@ case class CheckSystemIntegrityJsonV510(
debug_info: Option[String] = None
)
case class ConsentJsonV510(consent_id: String,
jwt: String,
case class ConsentJsonV510(consent_id: String,
jwt: String,
status: String,
consent_request_id: Option[String],
scopes: Option[List[Role]],
@ -466,7 +469,7 @@ case class ConsumerJsonV510(consumer_id: String,
certificate_info: Option[CertificateInfoJsonV510],
created_by_user: ResourceUserJSON,
enabled: Boolean,
created: Date,
created: Date,
logo_url: Option[String]
)
case class MyConsumerJsonV510(consumer_id: String,
@ -482,7 +485,7 @@ case class MyConsumerJsonV510(consumer_id: String,
certificate_info: Option[CertificateInfoJsonV510],
created_by_user: ResourceUserJSON,
enabled: Boolean,
created: Date,
created: Date,
logo_url: Option[String]
)
case class ConsumerJsonOnlyForPostResponseV510(consumer_id: String,
@ -682,6 +685,20 @@ case class ViewPermissionJson(
extra_data: Option[List[String]]
)
case class CallLimitJson510(
from_date: Date,
to_date: Date,
per_second_call_limit : String,
per_minute_call_limit : String,
per_hour_call_limit : String,
per_day_call_limit : String,
per_week_call_limit : String,
per_month_call_limit : String,
created_at : Date,
updated_at : Date,
current_state: Option[RedisCallLimitJson]
)
object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
def createTransactionRequestJson(tr : TransactionRequest, transactionRequestAttributes: List[TransactionRequestAttributeTrait] ) : TransactionRequestJsonV510 = {
@ -718,7 +735,7 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
def createTransactionRequestJSONs(transactionRequests : List[TransactionRequest], transactionRequestAttributes: List[TransactionRequestAttributeTrait]) : TransactionRequestsJsonV510 = {
TransactionRequestsJsonV510(
transactionRequests.map(
transactionRequest =>
transactionRequest =>
createTransactionRequestJson(transactionRequest, transactionRequestAttributes)
))
}
@ -1259,13 +1276,13 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
if(value == null || value.isEmpty) None else Some(value.split(",").toList)
)
}
def createMinimalAgentsJson(agents: List[Agent]): MinimalAgentsJsonV510 = {
MinimalAgentsJsonV510(
agents
.filter(_.isConfirmedAgent == true)
.map(agent => MinimalAgentJsonV510(
agent_id = agent.agentId,
agent_id = agent.agentId,
legal_name = agent.legalName,
agent_number = agent.number
)))
@ -1306,4 +1323,48 @@ object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
)
}
def createCallLimitJson(consumer: Consumer, rateLimiting: Option[RateLimiting], rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): CallLimitJson510 = {
val redisRateLimit = rateLimits match {
case Nil => None
case _ =>
def getInfo(period: RateLimitingPeriod.Value): Option[RateLimit] = {
rateLimits.filter(_._2 == period) match {
case x :: Nil =>
x._1 match {
case (Some(x), Some(y)) => Some(RateLimit(Some(x), Some(y)))
case _ => None
}
case _ => None
}
}
Some(
RedisCallLimitJson(
getInfo(RateLimitingPeriod.PER_SECOND),
getInfo(RateLimitingPeriod.PER_MINUTE),
getInfo(RateLimitingPeriod.PER_HOUR),
getInfo(RateLimitingPeriod.PER_DAY),
getInfo(RateLimitingPeriod.PER_WEEK),
getInfo(RateLimitingPeriod.PER_MONTH)
)
)
}
CallLimitJson510(
from_date = rateLimiting.map(_.fromDate).orNull,
to_date = rateLimiting.map(_.toDate).orNull,
per_second_call_limit = rateLimiting.map(_.perSecondCallLimit.toString).getOrElse("-1"),
per_minute_call_limit = rateLimiting.map(_.perMinuteCallLimit.toString).getOrElse("-1"),
per_hour_call_limit = rateLimiting.map(_.perHourCallLimit.toString).getOrElse("-1"),
per_day_call_limit = rateLimiting.map(_.perDayCallLimit.toString).getOrElse("-1"),
per_week_call_limit = rateLimiting.map(_.perWeekCallLimit.toString).getOrElse("-1"),
per_month_call_limit = rateLimiting.map(_.perMonthCallLimit.toString).getOrElse("-1"),
created_at = rateLimiting.map(_.createdAt.get).orNull,
updated_at = rateLimiting.map(_.updatedAt.get).orNull,
redisRateLimit
)
}
}

View File

@ -101,6 +101,28 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait {
}
result
}
def findMostRecentRateLimit(consumerId: String,
bankId: Option[String],
apiVersion: Option[String],
apiName: Option[String]): Future[Option[RateLimiting]] = Future {
findMostRecentRateLimitCommon(consumerId, bankId, apiVersion, apiName)
}
def findMostRecentRateLimitCommon(consumerId: String,
bankId: Option[String],
apiVersion: Option[String],
apiName: Option[String]): Option[RateLimiting] = {
val byConsumerParam = By(RateLimiting.ConsumerId, consumerId)
val byBankParam = bankId.map(v => By(RateLimiting.BankId, v)).getOrElse(NullRef(RateLimiting.BankId))
val byApiVersionParam = apiVersion.map(v => By(RateLimiting.ApiVersion, v)).getOrElse(NullRef(RateLimiting.ApiVersion))
val byApiNameParam = apiName.map(v => By(RateLimiting.ApiName, v)).getOrElse(NullRef(RateLimiting.ApiName))
RateLimiting.findAll(
byConsumerParam, byBankParam, byApiVersionParam, byApiNameParam,
OrderBy(RateLimiting.updatedAt, Descending)
).headOption
}
def createOrUpdateConsumerCallLimits(consumerId: String,
fromDate: Date,
toDate: Date,
@ -113,64 +135,40 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait {
perDay: Option[String],
perWeek: Option[String],
perMonth: Option[String]): Future[Box[RateLimiting]] = Future {
def createRateLimit(c: RateLimiting): Box[RateLimiting] = {
def createOrUpdateRateLimit(c: RateLimiting): Box[RateLimiting] = {
tryo {
c.FromDate(fromDate)
c.ToDate(toDate)
perSecond match {
case Some(v) => c.PerSecondCallLimit(v.toLong)
case None =>
}
perMinute match {
case Some(v) => c.PerMinuteCallLimit(v.toLong)
case None =>
}
perHour match {
case Some(v) => c.PerHourCallLimit(v.toLong)
case None =>
}
perDay match {
case Some(v) => c.PerDayCallLimit(v.toLong)
case None =>
}
perWeek match {
case Some(v) => c.PerWeekCallLimit(v.toLong)
case None =>
}
perMonth match {
case Some(v) => c.PerMonthCallLimit(v.toLong)
case None =>
}
bankId match {
case Some(v) => c.BankId(v)
case None => c.BankId(null)
}
apiName match {
case Some(v) => c.ApiName(v)
case None => c.ApiName(null)
}
apiVersion match {
case Some(v) => c.ApiVersion(v)
case None => c.ApiVersion(null)
}
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 byConsumerParam = By(RateLimiting.ConsumerId, consumerId)
val byBankParam = if(bankId.isDefined) By(RateLimiting.BankId, bankId.get) else NullRef(RateLimiting.BankId)
val byApiVersionParam = if(apiVersion.isDefined) By(RateLimiting.ApiVersion, apiVersion.get) else NullRef(RateLimiting.ApiVersion)
val byApiNameParam = if(apiName.isDefined) By(RateLimiting.ApiName, apiName.get) else NullRef(RateLimiting.ApiName)
val rateLimit = RateLimiting.find(byConsumerParam, byBankParam, byApiVersionParam, byApiNameParam)
val result = rateLimit match {
case Full(limit) => createRateLimit(limit)
case _ => createRateLimit(RateLimiting.create)
val result = findMostRecentRateLimitCommon(consumerId, bankId, apiVersion, apiName) match {
case Some(limit) => createOrUpdateRateLimit(limit)
case None => createOrUpdateRateLimit(RateLimiting.create)
}
result
}
}
class RateLimiting extends RateLimitingTrait with LongKeyedMapper[RateLimiting] with IdPK with CreatedUpdated {

View File

@ -17,6 +17,7 @@ trait RateLimitingProviderTrait {
def getAll(): Future[List[RateLimiting]]
def getAllByConsumerId(consumerId: String, date: Option[Date] = None): Future[List[RateLimiting]]
def getByConsumerId(consumerId: String, apiVersion: String, apiName: String, date: Option[Date] = None): Future[Box[RateLimiting]]
def findMostRecentRateLimit(consumerId: String, bankId: Option[String], apiVersion: Option[String], apiName: Option[String]): Future[Option[RateLimiting]]
def createOrUpdateConsumerCallLimits(consumerId: String,
fromDate: Date,
toDate: Date,

View File

@ -0,0 +1,132 @@
/**
Open Bank Project - API
Copyright (C) 2011-2019, TESOBE GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Email: contact@tesobe.com
TESOBE GmbH
Osloerstrasse 16/17
Berlin 13359, Germany
This product includes software developed at
TESOBE (http://www.tesobe.com/)
*/
package code.api.v5_1_0
import code.api.util.APIUtil.OAuth._
import code.api.util.ApiRole
import code.api.util.ApiRole.CanReadCallLimits
import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn}
import code.api.v4_0_0.CallLimitPostJsonV400
import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0
import code.consumer.Consumers
import code.entitlement.Entitlement
import code.setup.PropsReset
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model.ErrorMessage
import com.openbankproject.commons.util.ApiVersion
import org.scalatest.Tag
import java.time.format.DateTimeFormatter
import java.time.{ZoneId, ZonedDateTime}
import java.util.Date
class RateLimitingTest extends V510ServerSetup with PropsReset {
/**
* Test tags
* Example: To run tests with tag "getPermissions":
* mvn test -D tagsToInclude
*
* This is made possible by the scalatest maven plugin
*/
object ApiVersion400 extends Tag(ApiVersion.v4_0_0.toString)
object ApiVersion510 extends Tag(ApiVersion.v5_1_0.toString)
object ApiCallsLimit extends Tag(nameOf(Implementations5_1_0.getCallsLimit))
override def beforeEach() = {
super.beforeEach()
setPropsValues("use_consumer_limits"->"true")
setPropsValues("user_consumer_limit_anonymous_access"->"6000")
}
val yesterday = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1)
val tomorrow = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(10)
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'")
val fromDate = Date.from(yesterday.toInstant())
val toDate = Date.from(tomorrow.toInstant())
val callLimitJsonInitial = CallLimitPostJsonV400(
from_date = fromDate,
to_date = toDate,
api_version = None,
api_name = None,
bank_id = None,
per_second_call_limit = "-1",
per_minute_call_limit = "-1",
per_hour_call_limit = "-1",
per_day_call_limit ="-1",
per_week_call_limit = "-1",
per_month_call_limit = "-1"
)
val callLimitJsonMonth: CallLimitPostJsonV400 = callLimitJsonInitial.copy(per_month_call_limit = "100")
feature("Rate Limit - " + ApiCallsLimit + " - " + ApiVersion400) {
scenario("We will try to get calls limit per minute for a Consumer - unauthorized access", ApiCallsLimit, ApiVersion510) {
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 response510 = makeGetRequest(request510)
Then("We should get a 401")
response510.code should equal(401)
And("error should be " + UserNotLoggedIn)
response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
scenario("We will try to get calls limit per minute without a proper Role " + ApiRole.canReadCallLimits, ApiCallsLimit, ApiVersion510) {
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 response510 = makeGetRequest(request510)
Then("We should get a 403")
response510.code should equal(403)
And("error should be " + UserHasMissingRoles + CanReadCallLimits)
response510.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanReadCallLimits)
}
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)
val response01 = setRateLimiting(user1, callLimitJsonMonth)
Then("We should get a 200")
response01.code should equal(200)
When(s"We make a request v$ApiVersion510 with a Role " + ApiRole.canReadCallLimits)
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 response510 = makeGetRequest(request510)
Then("We should get a 200")
response510.code should equal(200)
response510.body.extract[CallLimitJson510]
}
}
}

View File

@ -11,8 +11,9 @@ import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140
import code.api.v2_0_0.{BasicAccountsJSON, TransactionRequestBodyJsonV200}
import code.api.v3_0_0.ViewJsonV300
import code.api.v3_1_0.{CreateAccountRequestJsonV310, CreateAccountResponseJsonV310, CustomerJsonV310}
import code.api.v4_0_0.{AtmJsonV400, BanksJson400, PostAccountAccessJsonV400, PostViewJsonV400, TransactionRequestWithChargeJSON400}
import code.api.v4_0_0.{AtmJsonV400, BanksJson400, CallLimitPostJsonV400, PostAccountAccessJsonV400, PostViewJsonV400, TransactionRequestWithChargeJSON400}
import code.api.v5_0_0.PostCustomerJsonV500
import code.consumer.Consumers
import code.entitlement.Entitlement
import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData}
import com.openbankproject.commons.model.{AccountRoutingJsonV121, AmountOfMoneyJsonV121, CreateViewJson}
@ -32,6 +33,15 @@ trait V510ServerSetup extends ServerSetupWithTestData with DefaultUsers {
def dynamicEndpoint_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString
def dynamicEntity_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString
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)
makePutRequest(request400, write(putJson))
}
def randomBankId : String = {
def getBanksInfo : APIResponse = {
val request = v5_1_0_Request / "banks"

View File

@ -4,8 +4,8 @@
"command": "mvn",
"args": ["jetty:run", "-pl", "obp-api"],
"env": {
"MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED"
},
"MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED --add-opens=java.base/java.util.stream=ALL-UNNAMED --add-opens=java.base/java.util.regex=ALL-UNNAMED"
}
"use_new_terminal": true,
"allow_concurrent_runs": false,
"reveal": "always",