Skip to content

Commit 92870ea

Browse files
authored
Merge pull request #2608 from constantine2nd/develop
Rate Limiting
2 parents 72d3626 + c39c133 commit 92870ea

File tree

15 files changed

+497
-137
lines changed

15 files changed

+497
-137
lines changed

obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ object BgSpecValidation {
3636

3737
if (date.isBefore(today)) {
3838
Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) cannot be in the past!")
39-
} else if (date.isEqual(MaxValidDays) || date.isAfter(MaxValidDays)) {
39+
} else if (date.isAfter(MaxValidDays)) {
4040
Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) exceeds the maximum allowed period of 180 days (until $MaxValidDays).")
4141
} else {
42-
Right(date) // Valid date
42+
Right(date) // Valid date (inclusive of 180 days)
4343
}
4444
} catch {
4545
case _: DateTimeParseException =>
@@ -55,23 +55,4 @@ object BgSpecValidation {
5555
}
5656
}
5757

58-
// Example usage
59-
def main(args: Array[String]): Unit = {
60-
val testDates = Seq(
61-
"2025-05-10", // More than 180 days ahead
62-
"9999-12-31", // Exceeds max allowed
63-
"2015-01-01", // In the past
64-
"invalid-date", // Invalid format
65-
LocalDate.now().plusDays(90).toString, // Valid (within 180 days)
66-
LocalDate.now().plusDays(180).toString, // Valid (exactly 180 days)
67-
LocalDate.now().plusDays(181).toString // More than 180 days
68-
)
69-
70-
testDates.foreach { date =>
71-
validateValidUntil(date) match {
72-
case Right(validDate) => println(s"Valid date: $validDate")
73-
case Left(error) => println(s"Error: $error")
74-
}
75-
}
76-
}
7758
}

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

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3209,8 +3209,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
32093209

32103210
// COMMON POST AUTHENTICATION CODE GOES BELOW
32113211

3212+
// Check is it Consumer disabled
3213+
val consumerIsDisabled: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkConsumerIsDisabled(res)
32123214
// Check is it a user deleted or locked
3213-
val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(res)
3215+
val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(consumerIsDisabled)
32143216
// Check Rate Limiting
32153217
val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkRateLimiting(userIsLockedOrDeleted)
32163218
// User init actions
@@ -4012,17 +4014,22 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
40124014
val consumerName = cc.flatMap(_.consumer.map(_.name.get)).getOrElse("")
40134015
val certificate = getCertificateFromTppSignatureCertificate(requestHeaders)
40144016
for {
4015-
tpp <- BerlinGroupSigning.getTppByCertificate(certificate, cc)
4017+
tpps <- BerlinGroupSigning.getRegulatedEntityByCertificate(certificate, cc)
40164018
} yield {
4017-
if (tpp.nonEmpty) {
4018-
val hasRole = tpp.exists(_.services.contains(serviceProvider))
4019-
if (hasRole) {
4020-
Full(true)
4021-
} else {
4022-
Failure(X509ActionIsNotAllowed)
4023-
}
4024-
} else {
4025-
Failure("No valid Tpp")
4019+
tpps match {
4020+
case Nil =>
4021+
Failure(RegulatedEntityNotFoundByCertificate)
4022+
case single :: Nil =>
4023+
// Only one match, proceed to role check
4024+
if (single.services.contains(serviceProvider)) {
4025+
Full(true)
4026+
} else {
4027+
Failure(X509ActionIsNotAllowed)
4028+
}
4029+
case multiple =>
4030+
// Ambiguity detected: more than one TPP matches the certificate
4031+
val names = multiple.map(e => s"'${e.entityName}' (Code: ${e.entityCode})").mkString(", ")
4032+
Failure(s"$RegulatedEntityAmbiguityByCertificate: multiple TPPs found: $names")
40264033
}
40274034
}
40284035
case value if value.toUpperCase == "CERTIFICATE" => Future {

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import code.accountholders.AccountHolders
66
import code.api.Constant
77
import code.api.util.APIUtil.getPropsAsBoolValue
88
import code.api.util.ApiRole.{CanCreateAccount, CanCreateHistoricalTransactionAtBank}
9-
import code.api.util.ErrorMessages.{UserIsDeleted, UsernameHasBeenLocked}
9+
import code.api.util.ErrorMessages.{ConsumerIsDisabled, UserIsDeleted, UsernameHasBeenLocked}
1010
import code.api.util.RateLimitingJson.CallLimit
1111
import code.bankconnectors.{Connector, LocalMappedConnectorInternal}
1212
import code.entitlement.Entitlement
@@ -78,6 +78,18 @@ object AfterApiAuth extends MdcLoggable{
7878
}
7979
}
8080
}
81+
def checkConsumerIsDisabled(res: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = {
82+
for {
83+
(user: Box[User], cc) <- res
84+
} yield {
85+
cc.map(_.consumer) match {
86+
case Some(Full(consumer)) if !consumer.isActive.get => // There is a consumer. Check it.
87+
(Failure(ConsumerIsDisabled), cc) // The Consumer is DISABLED.
88+
case _ => // There is no Consumer. Just forward the result.
89+
(user, cc)
90+
}
91+
}
92+
}
8193

8294
/**
8395
* This block of code needs to update Call Context with Rate Limiting

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

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import code.consumer.Consumers
88
import code.model.Consumer
99
import code.util.Helper.MdcLoggable
1010
import com.openbankproject.commons.ExecutionContext.Implicits.global
11-
import com.openbankproject.commons.model.{RegulatedEntityTrait, User}
11+
import com.openbankproject.commons.model.{RegulatedEntityAttributeSimple, RegulatedEntityTrait, User}
1212
import net.liftweb.common.{Box, Empty, Failure, Full}
1313
import net.liftweb.http.provider.HTTPParam
1414
import net.liftweb.util.Helpers
@@ -111,29 +111,40 @@ object BerlinGroupSigning extends MdcLoggable {
111111
certificate
112112
}
113113

114-
def getTppByCertificate(certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = {
115-
// Use the regular expression to find the value of CN
116-
val extractedCN = cnPattern.findFirstMatchIn(certificate.getIssuerDN.getName) match {
117-
case Some(m) => m.group(1) // Extract the value of CN
118-
case None => "CN not found"
119-
}
120-
val issuerCommonName = extractedCN // Certificate.caCert
114+
def getRegulatedEntityByCertificate(certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = {
115+
val issuerCN = cnPattern.findFirstMatchIn(certificate.getIssuerDN.getName)
116+
.map(_.group(1).trim)
117+
.getOrElse("CN not found")
118+
121119
val serialNumber = certificate.getSerialNumber.toString
122-
val regulatedEntities: Future[List[RegulatedEntityTrait]] = for {
120+
121+
for {
123122
(entities, _) <- getRegulatedEntitiesNewStyle(callContext)
124123
} yield {
125-
logger.debug("Regulated Entities: " + entities)
126124
entities.filter { entity =>
127-
val hasSerialNumber = entity.attributes.exists(_.exists(a =>
128-
a.name == "CERTIFICATE_SERIAL_NUMBER" && a.value == serialNumber
129-
))
130-
val hasCaName = entity.attributes.exists(_.exists(a =>
131-
a.name == "CERTIFICATE_CA_NAME" && a.value == issuerCommonName
132-
))
133-
hasSerialNumber && hasCaName
125+
val attrs = entity.attributes.getOrElse(Nil)
126+
127+
// Extract serial number and CA name from attributes
128+
val serialOpt = attrs.collectFirst { case a if a.name.equalsIgnoreCase("CERTIFICATE_SERIAL_NUMBER") => a.value.trim }
129+
val caNameOpt = attrs.collectFirst { case a if a.name.equalsIgnoreCase("CERTIFICATE_CA_NAME") => a.value.trim }
130+
131+
val serialMatches = serialOpt.contains(serialNumber)
132+
val caNameMatches = caNameOpt.exists(_.equalsIgnoreCase(issuerCN))
133+
134+
val isMatch = serialMatches && caNameMatches
135+
136+
// Log everything for debugging
137+
val serialLog = serialOpt.getOrElse("N/A")
138+
val caNameLog = caNameOpt.getOrElse("N/A")
139+
val allAttrsLog = attrs.map(a => s"${a.name}='${a.value}'").mkString(", ")
140+
141+
if (isMatch)
142+
logger.debug(s"[MATCH] Entity '${entity.entityName}' (Code: ${entity.entityCode}) matches CN='$issuerCN', Serial='$serialNumber' " +
143+
s"(Attributes found: Serial='$serialLog', CA Name='$caNameLog', All Attributes: [$allAttrsLog])")
144+
145+
isMatch
134146
}
135147
}
136-
regulatedEntities
137148
}
138149

139150

@@ -280,7 +291,7 @@ object BerlinGroupSigning extends MdcLoggable {
280291
}
281292

282293
for {
283-
entities <- getTppByCertificate(certificate, forwardResult._2) // Find TPP via certificate
294+
entities <- getRegulatedEntityByCertificate(certificate, forwardResult._2) // Find Regulated Entity via certificate
284295
} yield {
285296
// Certificate can be changed but this value is permanent per Regulated entity
286297
val idno = entities.map(_.entityCode).headOption.getOrElse("")

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,7 @@ object ErrorMessages {
580580
val RegulatedEntityNotFound = "OBP-34100: Regulated Entity not found. Please specify a valid value for REGULATED_ENTITY_ID."
581581
val RegulatedEntityNotDeleted = "OBP-34101: Regulated Entity cannot be deleted. Please specify a valid value for REGULATED_ENTITY_ID."
582582
val RegulatedEntityNotFoundByCertificate = "OBP-34102: Regulated Entity cannot be found by provided certificate."
583+
val RegulatedEntityAmbiguityByCertificate = "OBP-34103: More than 1 Regulated Entity found by provided certificate."
583584
val PostJsonIsNotSigned = "OBP-34110: JWT at the post json cannot be verified."
584585

585586
// Consents

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,21 +153,22 @@ object RateLimitingUtil extends MdcLoggable {
153153

154154
def getInfo(consumerKey: String, period: LimitCallPeriod): ((Option[Long], Option[Long]), LimitCallPeriod) = {
155155
val key = createUniqueKey(consumerKey, period)
156-
val ttl = Redis.use(JedisMethod.TTL, key).get.toLong
157-
ttl match {
158-
case -2 =>
159-
((None, None), period)
160-
case _ =>
161-
((Redis.use(JedisMethod.TTL, key).map(_.toLong), Some(ttl)), period)
162-
}
156+
157+
// get TTL
158+
val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong)
159+
160+
// get value (assuming string storage)
161+
val valueOpt: Option[Long] = Redis.use(JedisMethod.GET, key).map(_.toLong)
162+
163+
((valueOpt, ttlOpt), period)
163164
}
164165

165166
getInfo(consumerKey, RateLimitingPeriod.PER_SECOND) ::
166167
getInfo(consumerKey, RateLimitingPeriod.PER_MINUTE) ::
167168
getInfo(consumerKey, RateLimitingPeriod.PER_HOUR) ::
168169
getInfo(consumerKey, RateLimitingPeriod.PER_DAY) ::
169170
getInfo(consumerKey, RateLimitingPeriod.PER_WEEK) ::
170-
getInfo(consumerKey, RateLimitingPeriod.PER_MONTH) ::
171+
getInfo(consumerKey, RateLimitingPeriod.PER_MONTH) ::
171172
Nil
172173
}
173174

obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import code.api.v3_1_0._
2929
import code.api.v4_0_0.JSONFactory400.{createAccountBalancesJson, createBalancesJson, createNewCoreBankAccountJson}
3030
import code.api.v4_0_0._
3131
import code.api.v5_0_0.JSONFactory500
32-
import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson}
32+
import code.api.v5_1_0.JSONFactory510.{createCallLimitJson, createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson}
3333
import code.atmattribute.AtmAttribute
3434
import code.bankconnectors.Connector
3535
import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent}
@@ -39,6 +39,7 @@ import code.loginattempts.LoginAttempt
3939
import code.metrics.APIMetrics
4040
import code.model.dataAccess.{AuthUser, MappedBankAccount}
4141
import code.model.{AppType, Consumer}
42+
import code.ratelimiting.{RateLimiting, RateLimitingDI}
4243
import code.regulatedentities.MappedRegulatedEntityProvider
4344
import code.userlocks.UserLocksProvider
4445
import code.users.Users
@@ -3290,6 +3291,50 @@ trait APIMethods510 {
32903291
}
32913292

32923293

3294+
staticResourceDocs += ResourceDoc(
3295+
getCallsLimit,
3296+
implementedInApiVersion,
3297+
nameOf(getCallsLimit),
3298+
"GET",
3299+
"/management/consumers/CONSUMER_ID/consumer/call-limits",
3300+
"Get Call Limits for a Consumer",
3301+
s"""
3302+
|Get Calls limits per Consumer.
3303+
|${userAuthenticationMessage(true)}
3304+
|
3305+
|""".stripMargin,
3306+
EmptyBody,
3307+
callLimitJson,
3308+
List(
3309+
$UserNotLoggedIn,
3310+
InvalidJsonFormat,
3311+
InvalidConsumerId,
3312+
ConsumerNotFoundByConsumerId,
3313+
UserHasMissingRoles,
3314+
UpdateConsumerError,
3315+
UnknownError
3316+
),
3317+
List(apiTagConsumer),
3318+
Some(List(canReadCallLimits)))
3319+
3320+
3321+
lazy val getCallsLimit: OBPEndpoint = {
3322+
case "management" :: "consumers" :: consumerId :: "consumer" :: "call-limits" :: Nil JsonGet _ => {
3323+
cc =>
3324+
implicit val ec = EndpointContext(Some(cc))
3325+
for {
3326+
// (Full(u), callContext) <- authenticatedAccess(cc)
3327+
// _ <- NewStyle.function.hasEntitlement("", cc.userId, canReadCallLimits, callContext)
3328+
consumer <- NewStyle.function.getConsumerByConsumerId(consumerId, cc.callContext)
3329+
rateLimiting: Option[RateLimiting] <- RateLimitingDI.rateLimiting.vend.findMostRecentRateLimit(consumerId, None, None, None)
3330+
rateLimit <- Future(RateLimitingUtil.consumerRateLimitState(consumer.consumerId.get).toList)
3331+
} yield {
3332+
(createCallLimitJson(consumer, rateLimiting, rateLimit), HttpCode.`200`(cc.callContext))
3333+
}
3334+
}
3335+
}
3336+
3337+
32933338
staticResourceDocs += ResourceDoc(
32943339
updateConsumerRedirectURL,
32953340
implementedInApiVersion,

0 commit comments

Comments
 (0)