Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ object BgSpecValidation {

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

// Example usage
def main(args: Array[String]): Unit = {
val testDates = Seq(
"2025-05-10", // More than 180 days ahead
"9999-12-31", // Exceeds max allowed
"2015-01-01", // In the past
"invalid-date", // Invalid format
LocalDate.now().plusDays(90).toString, // Valid (within 180 days)
LocalDate.now().plusDays(180).toString, // Valid (exactly 180 days)
LocalDate.now().plusDays(181).toString // More than 180 days
)

testDates.foreach { date =>
validateValidUntil(date) match {
case Right(validDate) => println(s"Valid date: $validDate")
case Left(error) => println(s"Error: $error")
}
}
}
}
29 changes: 18 additions & 11 deletions obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3196,8 +3196,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{

// COMMON POST AUTHENTICATION CODE GOES BELOW

// Check is it Consumer disabled
val consumerIsDisabled: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkConsumerIsDisabled(res)
// Check is it a user deleted or locked
val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(res)
val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(consumerIsDisabled)
// Check Rate Limiting
val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkRateLimiting(userIsLockedOrDeleted)
// User init actions
Expand Down Expand Up @@ -3999,17 +4001,22 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
val consumerName = cc.flatMap(_.consumer.map(_.name.get)).getOrElse("")
val certificate = getCertificateFromTppSignatureCertificate(requestHeaders)
for {
tpp <- BerlinGroupSigning.getTppByCertificate(certificate, cc)
tpps <- BerlinGroupSigning.getRegulatedEntityByCertificate(certificate, cc)
} yield {
if (tpp.nonEmpty) {
val hasRole = tpp.exists(_.services.contains(serviceProvider))
if (hasRole) {
Full(true)
} else {
Failure(X509ActionIsNotAllowed)
}
} else {
Failure("No valid Tpp")
tpps match {
case Nil =>
Failure(RegulatedEntityNotFoundByCertificate)
case single :: Nil =>
// Only one match, proceed to role check
if (single.services.contains(serviceProvider)) {
Full(true)
} else {
Failure(X509ActionIsNotAllowed)
}
case multiple =>
// Ambiguity detected: more than one TPP matches the certificate
val names = multiple.map(e => s"'${e.entityName}' (Code: ${e.entityCode})").mkString(", ")
Failure(s"$RegulatedEntityAmbiguityByCertificate: multiple TPPs found: $names")
}
}
case value if value.toUpperCase == "CERTIFICATE" => Future {
Expand Down
14 changes: 13 additions & 1 deletion obp-api/src/main/scala/code/api/util/AfterApiAuth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import code.accountholders.AccountHolders
import code.api.Constant
import code.api.util.APIUtil.getPropsAsBoolValue
import code.api.util.ApiRole.{CanCreateAccount, CanCreateHistoricalTransactionAtBank}
import code.api.util.ErrorMessages.{UserIsDeleted, UsernameHasBeenLocked}
import code.api.util.ErrorMessages.{ConsumerIsDisabled, UserIsDeleted, UsernameHasBeenLocked}
import code.api.util.RateLimitingJson.CallLimit
import code.bankconnectors.{Connector, LocalMappedConnectorInternal}
import code.entitlement.Entitlement
Expand Down Expand Up @@ -78,6 +78,18 @@ object AfterApiAuth extends MdcLoggable{
}
}
}
def checkConsumerIsDisabled(res: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = {
for {
(user: Box[User], cc) <- res
} yield {
cc.map(_.consumer) match {
case Some(Full(consumer)) if !consumer.isActive.get => // There is a consumer. Check it.
(Failure(ConsumerIsDisabled), cc) // The Consumer is DISABLED.
case _ => // There is no Consumer. Just forward the result.
(user, cc)
}
}
}

/**
* This block of code needs to update Call Context with Rate Limiting
Expand Down
49 changes: 30 additions & 19 deletions obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import code.consumer.Consumers
import code.model.Consumer
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model.{RegulatedEntityTrait, User}
import com.openbankproject.commons.model.{RegulatedEntityAttributeSimple, RegulatedEntityTrait, User}
import net.liftweb.common.{Box, Empty, Failure, Full}
import net.liftweb.http.provider.HTTPParam
import net.liftweb.util.Helpers
Expand Down Expand Up @@ -111,29 +111,40 @@ object BerlinGroupSigning extends MdcLoggable {
certificate
}

def getTppByCertificate(certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = {
// Use the regular expression to find the value of CN
val extractedCN = cnPattern.findFirstMatchIn(certificate.getIssuerDN.getName) match {
case Some(m) => m.group(1) // Extract the value of CN
case None => "CN not found"
}
val issuerCommonName = extractedCN // Certificate.caCert
def getRegulatedEntityByCertificate(certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = {
val issuerCN = cnPattern.findFirstMatchIn(certificate.getIssuerDN.getName)
.map(_.group(1).trim)
.getOrElse("CN not found")

val serialNumber = certificate.getSerialNumber.toString
val regulatedEntities: Future[List[RegulatedEntityTrait]] = for {

for {
(entities, _) <- getRegulatedEntitiesNewStyle(callContext)
} yield {
logger.debug("Regulated Entities: " + entities)
entities.filter { entity =>
val hasSerialNumber = entity.attributes.exists(_.exists(a =>
a.name == "CERTIFICATE_SERIAL_NUMBER" && a.value == serialNumber
))
val hasCaName = entity.attributes.exists(_.exists(a =>
a.name == "CERTIFICATE_CA_NAME" && a.value == issuerCommonName
))
hasSerialNumber && hasCaName
val attrs = entity.attributes.getOrElse(Nil)

// Extract serial number and CA name from attributes
val serialOpt = attrs.collectFirst { case a if a.name.equalsIgnoreCase("CERTIFICATE_SERIAL_NUMBER") => a.value.trim }
val caNameOpt = attrs.collectFirst { case a if a.name.equalsIgnoreCase("CERTIFICATE_CA_NAME") => a.value.trim }

val serialMatches = serialOpt.contains(serialNumber)
val caNameMatches = caNameOpt.exists(_.equalsIgnoreCase(issuerCN))

val isMatch = serialMatches && caNameMatches

// Log everything for debugging
val serialLog = serialOpt.getOrElse("N/A")
val caNameLog = caNameOpt.getOrElse("N/A")
val allAttrsLog = attrs.map(a => s"${a.name}='${a.value}'").mkString(", ")

if (isMatch)
logger.debug(s"[MATCH] Entity '${entity.entityName}' (Code: ${entity.entityCode}) matches CN='$issuerCN', Serial='$serialNumber' " +
s"(Attributes found: Serial='$serialLog', CA Name='$caNameLog', All Attributes: [$allAttrsLog])")

isMatch
}
}
regulatedEntities
}


Expand Down Expand Up @@ -280,7 +291,7 @@ object BerlinGroupSigning extends MdcLoggable {
}

for {
entities <- getTppByCertificate(certificate, forwardResult._2) // Find TPP via certificate
entities <- getRegulatedEntityByCertificate(certificate, forwardResult._2) // Find Regulated Entity via certificate
} yield {
// Certificate can be changed but this value is permanent per Regulated entity
val idno = entities.map(_.entityCode).headOption.getOrElse("")
Expand Down
1 change: 1 addition & 0 deletions obp-api/src/main/scala/code/api/util/ErrorMessages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ object ErrorMessages {
val RegulatedEntityNotFound = "OBP-34100: Regulated Entity not found. Please specify a valid value for REGULATED_ENTITY_ID."
val RegulatedEntityNotDeleted = "OBP-34101: Regulated Entity cannot be deleted. Please specify a valid value for REGULATED_ENTITY_ID."
val RegulatedEntityNotFoundByCertificate = "OBP-34102: Regulated Entity cannot be found by provided certificate."
val RegulatedEntityAmbiguityByCertificate = "OBP-34103: More than 1 Regulated Entity found by provided certificate."
val PostJsonIsNotSigned = "OBP-34110: JWT at the post json cannot be verified."

// Consents
Expand Down
17 changes: 9 additions & 8 deletions obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -153,21 +153,22 @@ object RateLimitingUtil extends MdcLoggable {

def getInfo(consumerKey: String, period: LimitCallPeriod): ((Option[Long], Option[Long]), LimitCallPeriod) = {
val key = createUniqueKey(consumerKey, period)
val ttl = Redis.use(JedisMethod.TTL, key).get.toLong
ttl match {
case -2 =>
((None, None), period)
case _ =>
((Redis.use(JedisMethod.TTL, key).map(_.toLong), Some(ttl)), period)
}

// get TTL
val ttlOpt: Option[Long] = Redis.use(JedisMethod.TTL, key).map(_.toLong)

// get value (assuming string storage)
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) ::
getInfo(consumerKey, RateLimitingPeriod.PER_MONTH) ::
Nil
}

Expand Down
47 changes: 46 additions & 1 deletion obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading