diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala
index d78be8924..ea6adbdbb 100644
--- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala
+++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala
@@ -159,6 +159,18 @@ case class APIFailureNewStyle(failMsg: String,
}
}
+object ObpApiFailure {
+ def apply(failMsg: String, failCode: Int = 400, cc: Option[CallContext] = None) = {
+ fullBoxOrException(Empty ~> APIFailureNewStyle(failMsg, failCode, cc.map(_.toLight)))
+ }
+
+ // overload for plain CallContext
+ def apply(failMsg: String, failCode: Int, cc: CallContext) = {
+ fullBoxOrException(Empty ~> APIFailureNewStyle(failMsg, failCode, Some(cc.toLight)))
+ }
+}
+
+
//if you change this, think about backwards compatibility! All existing
//versions of the API return this failure message, so if you change it, make sure
//that all stable versions retain the same behavior
diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala
index c7f56c3c6..f0b2b05d7 100644
--- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala
+++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala
@@ -3947,6 +3947,24 @@ object SwaggerDefinitionsJSON {
Some(redisCallLimitJson)
)
+ lazy val callLimitsJson510Example: CallLimitsJson510 = CallLimitsJson510(
+ limits = List(
+ CallLimitJson510(
+ rate_limiting_id = "80e1e0b2-d8bf-4f85-a579-e69ef36e3305",
+ from_date = DateWithDayExampleObject,
+ to_date = DateWithDayExampleObject,
+ per_second_call_limit = "100",
+ per_minute_call_limit = "100",
+ per_hour_call_limit = "-1",
+ per_day_call_limit = "-1",
+ per_week_call_limit = "-1",
+ per_month_call_limit = "-1",
+ created_at = DateWithDayExampleObject,
+ updated_at = DateWithDayExampleObject
+ )
+ )
+ )
+
lazy val accountWebhookPostJson = AccountWebhookPostJson(
account_id =accountIdExample.value,
trigger_name = ApiTrigger.onBalanceChange.toString(),
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 af187aacc..5134f935f 100644
--- a/obp-api/src/main/scala/code/api/util/APIUtil.scala
+++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala
@@ -4021,7 +4021,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
} yield {
tpps match {
case Nil =>
- Failure(RegulatedEntityNotFoundByCertificate)
+ ObpApiFailure(RegulatedEntityNotFoundByCertificate, 401, cc)
case single :: Nil =>
logger.debug(s"Regulated entity by certificate: $single")
// Only one match, proceed to role check
@@ -4029,12 +4029,12 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
logger.debug(s"Regulated entity by certificate (single.services: ${single.services}, serviceProvider: $serviceProvider): ")
Full(true)
} else {
- Failure(X509ActionIsNotAllowed)
+ ObpApiFailure(X509ActionIsNotAllowed, 403, cc)
}
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")
+ ObpApiFailure(s"$RegulatedEntityAmbiguityByCertificate: multiple TPPs found: $names", 401, cc)
}
}
case value if value.toUpperCase == "CERTIFICATE" => Future {
diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala
index 7e286003b..c3de4f1fc 100644
--- a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala
+++ b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala
@@ -73,11 +73,14 @@ object BerlinGroupError {
case "401" if message.contains("OBP-35005") => "CONSENT_INVALID"
case "401" if message.contains("OBP-20300") => "CERTIFICATE_BLOCKED"
+ case "401" if message.contains("OBP-34102") => "CERTIFICATE_BLOCKED"
+ case "401" if message.contains("OBP-34103") => "CERTIFICATE_BLOCKED"
+
case "401" if message.contains("OBP-20312") => "CERTIFICATE_INVALID"
- case "401" if message.contains("OBP-20300") => "CERTIFICATE_INVALID"
case "401" if message.contains("OBP-20310") => "SIGNATURE_INVALID"
- case "401" if message.contains("OBP-20060") => "ROLE_INVALID"
+ case "403" if message.contains("OBP-20307") => "ROLE_INVALID"
+ case "403" if message.contains("OBP-20060") => "ROLE_INVALID"
case "400" if message.contains("OBP-10034") => "PARAMETER_NOT_CONSISTENT"
diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala
index cb186a439..6029defbb 100644
--- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala
+++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala
@@ -1,15 +1,15 @@
package code.api.util
-import code.api.{APIFailureNewStyle, RequestHeader}
-import code.api.util.APIUtil.{OBPReturnType, fullBoxOrException}
+import code.api.util.APIUtil.OBPReturnType
import code.api.util.ErrorUtil.apiFailure
import code.api.util.newstyle.RegulatedEntityNewStyle.getRegulatedEntitiesNewStyle
+import code.api.{ObpApiFailure, RequestHeader}
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.{RegulatedEntityAttributeSimple, RegulatedEntityTrait, User}
-import net.liftweb.common.{Box, Empty, Failure, Full}
+import com.openbankproject.commons.model.{RegulatedEntityTrait, User}
+import net.liftweb.common.{Box, Failure, Full}
import net.liftweb.http.provider.HTTPParam
import net.liftweb.util.Helpers
@@ -277,66 +277,64 @@ object BerlinGroupSigning extends MdcLoggable {
val tppSignatureCert: String = APIUtil.getRequestHeader(RequestHeader.`TPP-Signature-Certificate`, requestHeaders)
if (tppSignatureCert.isEmpty) {
Future(forwardResult)
- } else { // Dynamic consumer creation/update works in case that RequestHeader.`TPP-Signature-Certificate is present in the current call
+ } else { // Dynamic consumer creation/update works in case that RequestHeader.`TPP-Signature-Certificate` is present
val certificate = getCertificateFromTppSignatureCertificate(requestHeaders)
- // Use the regular expression to find the value of EMAILADDRESS
- val extractedEmail = emailPattern.findFirstMatchIn(certificate.getSubjectDN.getName) match {
- case Some(m) => Some(m.group(1)) // Extract the value of EMAILADDRESS
- case None => None
- }
- // Use the regular expression to find the value of Organisation
- val extractOrganisation = organisationlPattern.findFirstMatchIn(certificate.getSubjectDN.getName) match {
- case Some(m) => Some(m.group(1)) // Extract the value of Organisation
- case None => None
- }
+
+ val extractedEmail = emailPattern.findFirstMatchIn(certificate.getSubjectDN.getName).map(_.group(1))
+ val extractOrganisation = organisationlPattern.findFirstMatchIn(certificate.getSubjectDN.getName).map(_.group(1))
for {
- entities <- getRegulatedEntityByCertificate(certificate, forwardResult._2) // Find Regulated Entity via certificate
+ entities <- getRegulatedEntityByCertificate(certificate, forwardResult._2)
} yield {
- // Certificate can be changed but this value is permanent per Regulated entity
- val idno = entities.map(_.entityCode).headOption.getOrElse("")
+ entities match {
+ case Nil =>
+ (ObpApiFailure(ErrorMessages.RegulatedEntityNotFoundByCertificate, 401, forwardResult._2), forwardResult._2)
- val entityName = entities.map(_.entityName).headOption
+ case single :: Nil =>
+ val idno = single.entityCode
+ val entityName = Option(single.entityName)
- // Get or create consumer by the unique key (azp, iss)
- val consumer: Box[Consumer] = Consumers.consumers.vend.getOrCreateConsumer(
- consumerId = None,
- key = Some(Helpers.randomString(40).toLowerCase),
- secret = Some(Helpers.randomString(40).toLowerCase),
- aud = None,
- azp = Some(idno), // The pair (azp, iss) is a unique key in case of Client of an Identity Provider
- iss = Some(RequestHeader.`TPP-Signature-Certificate`),
- sub = None,
- Some(true),
- name = entityName,
- appType = None,
- description = Some(s"Certificate serial number:${certificate.getSerialNumber}"),
- developerEmail = extractedEmail,
- redirectURL = None,
- createdByUserId = None,
- certificate = None,
- logoUrl = code.api.Constant.consumerDefaultLogoUrl
- )
-
- // Set or update certificate
- consumer match {
- case Full(consumer) =>
- val certificateFromHeader = getHeaderValue(RequestHeader.`TPP-Signature-Certificate`, requestHeaders)
- Consumers.consumers.vend.updateConsumer(
- id = consumer.id.get,
+ val consumer: Box[Consumer] = Consumers.consumers.vend.getOrCreateConsumer(
+ consumerId = None,
+ key = Some(Helpers.randomString(40).toLowerCase),
+ secret = Some(Helpers.randomString(40).toLowerCase),
+ aud = None,
+ azp = Some(idno),
+ iss = Some(RequestHeader.`TPP-Signature-Certificate`),
+ sub = None,
+ Some(true),
name = entityName,
- certificate = Some(certificateFromHeader)
- ) match {
+ appType = None,
+ description = Some(s"Certificate serial number:${certificate.getSerialNumber}"),
+ developerEmail = extractedEmail,
+ redirectURL = None,
+ createdByUserId = None,
+ certificate = None,
+ logoUrl = code.api.Constant.consumerDefaultLogoUrl
+ )
+
+ consumer match {
case Full(consumer) =>
- // Update call context with a created consumer
- (forwardResult._1, forwardResult._2.map(_.copy(consumer = Full(consumer))))
+ val certificateFromHeader = getHeaderValue(RequestHeader.`TPP-Signature-Certificate`, requestHeaders)
+ Consumers.consumers.vend.updateConsumer(
+ id = consumer.id.get,
+ name = entityName,
+ certificate = Some(certificateFromHeader)
+ ) match {
+ case Full(updatedConsumer) =>
+ (forwardResult._1, forwardResult._2.map(_.copy(consumer = Full(updatedConsumer))))
+ case error =>
+ logger.debug(error)
+ (Failure(s"${ErrorMessages.CreateConsumerError} Regulated entity: $idno"), forwardResult._2)
+ }
case error =>
logger.debug(error)
(Failure(s"${ErrorMessages.CreateConsumerError} Regulated entity: $idno"), forwardResult._2)
}
- case error =>
- logger.debug(error)
- (Failure(s"${ErrorMessages.CreateConsumerError} Regulated entity: $idno"), forwardResult._2)
+
+ case multiple =>
+ val names = multiple.map(e => s"'${e.entityName}' (Code: ${e.entityCode})").mkString(", ")
+ (ObpApiFailure(s"${ErrorMessages.RegulatedEntityAmbiguityByCertificate}: multiple TPPs found: $names", 401, forwardResult._2), forwardResult._2)
}
}
}
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 0ade38dca..c4ac89761 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
@@ -3304,7 +3304,7 @@ trait APIMethods510 {
|
|""".stripMargin,
EmptyBody,
- callLimitJson,
+ callLimitsJson510Example,
List(
$UserNotLoggedIn,
InvalidJsonFormat,
@@ -3319,19 +3319,15 @@ trait APIMethods510 {
lazy val getCallsLimit: OBPEndpoint = {
- case "management" :: "consumers" :: consumerId :: "consumer" :: "call-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)
- // _ <- 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)
+ _ <- NewStyle.function.getConsumerByConsumerId(consumerId, cc.callContext)
+ rateLimiting <- RateLimitingDI.rateLimiting.vend.getAllByConsumerId(consumerId, None)
} yield {
- (createCallLimitJson(consumer, rateLimiting, rateLimit), HttpCode.`200`(cc.callContext))
+ (createCallLimitJson(rateLimiting), HttpCode.`200`(cc.callContext))
}
- }
}
diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala
index 1c64a09e5..a5f01717b 100644
--- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala
+++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala
@@ -686,6 +686,7 @@ case class ViewPermissionJson(
)
case class CallLimitJson510(
+ rate_limiting_id: String,
from_date: Date,
to_date: Date,
per_second_call_limit : String,
@@ -695,9 +696,9 @@ case class CallLimitJson510(
per_week_call_limit : String,
per_month_call_limit : String,
created_at : Date,
- updated_at : Date,
- current_state: Option[RedisCallLimitJson]
+ updated_at : Date
)
+case class CallLimitsJson510(limits: List[CallLimitJson510])
object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
@@ -1323,48 +1324,26 @@ 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)
- )
+ def createCallLimitJson(rateLimitings: List[RateLimiting]): CallLimitsJson510 = {
+ CallLimitsJson510(
+ rateLimitings.map( i =>
+ CallLimitJson510(
+ rate_limiting_id = i.rateLimitingId,
+ from_date = i.fromDate,
+ to_date = i.toDate,
+ per_second_call_limit = i.perSecondCallLimit.toString,
+ per_minute_call_limit = i.perMinuteCallLimit.toString,
+ per_hour_call_limit = i.perHourCallLimit.toString,
+ per_day_call_limit = i.perDayCallLimit.toString,
+ per_week_call_limit = i.perWeekCallLimit.toString,
+ per_month_call_limit = i.perMonthCallLimit.toString,
+ created_at = i.createdAt.get,
+ updated_at = i.updatedAt.get,
)
- }
-
- 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
+ )
)
+
}
}
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 318bb47d5..4c35facab 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
@@ -2,11 +2,13 @@ package code.api.v6_0_0
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
import code.api.util.APIUtil._
+import code.api.util.ApiRole.canReadCallLimits
import code.api.util.ApiTag._
import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidJsonFormat, UnknownError, _}
import code.api.util.FutureUtil.EndpointContext
-import code.api.util.NewStyle
+import code.api.util.{NewStyle, RateLimitingUtil}
import code.api.util.NewStyle.HttpCode
+import code.api.v6_0_0.JSONFactory600.createCurrentUsageJson
import code.bankconnectors.LocalMappedConnectorInternal
import code.bankconnectors.LocalMappedConnectorInternal._
import code.entitlement.Entitlement
@@ -20,6 +22,7 @@ import com.openbankproject.commons.ExecutionContext.Implicits.global
import scala.collection.immutable.{List, Nil}
import scala.collection.mutable.ArrayBuffer
+import scala.concurrent.Future
trait APIMethods600 {
@@ -38,6 +41,46 @@ trait APIMethods600 {
val codeContext = CodeContext(staticResourceDocs, apiRelations)
+ staticResourceDocs += ResourceDoc(
+ getCurrentCallsLimit,
+ implementedInApiVersion,
+ nameOf(getCurrentCallsLimit),
+ "GET",
+ "/management/consumers/CONSUMER_ID/consumer/current-usage",
+ "Get Call Limits for a Consumer Usage",
+ s"""
+ |Get Call Limits for a Consumer Usage.
+ |${userAuthenticationMessage(true)}
+ |
+ |""".stripMargin,
+ EmptyBody,
+ redisCallLimitJson,
+ List(
+ $UserNotLoggedIn,
+ InvalidJsonFormat,
+ InvalidConsumerId,
+ ConsumerNotFoundByConsumerId,
+ UserHasMissingRoles,
+ UpdateConsumerError,
+ UnknownError
+ ),
+ List(apiTagConsumer),
+ Some(List(canReadCallLimits)))
+
+
+ lazy val getCurrentCallsLimit: OBPEndpoint = {
+ case "management" :: "consumers" :: consumerId :: "consumer" :: "current-usage" :: Nil JsonGet _ =>
+ cc =>
+ implicit val ec = EndpointContext(Some(cc))
+ for {
+ _ <- NewStyle.function.getConsumerByConsumerId(consumerId, cc.callContext)
+ currentUsage <- Future(RateLimitingUtil.consumerRateLimitState(consumerId).toList)
+ } yield {
+ (createCurrentUsageJson(currentUsage), HttpCode.`200`(cc.callContext))
+ }
+ }
+
+
staticResourceDocs += ResourceDoc(
getCurrentUser,
implementedInApiVersion,
diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
index 24acb6b1f..4c339ac46 100644
--- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
+++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
@@ -27,9 +27,11 @@
package code.api.v6_0_0
import code.api.util.APIUtil.stringOrNull
+import code.api.util.RateLimitingPeriod.LimitCallPeriod
import code.api.util._
import code.api.v2_0_0.{EntitlementJSONs, JSONFactory200}
import code.api.v3_0_0.{UserJsonV300, ViewJSON300, ViewsJSON300}
+import code.api.v3_1_0.{RateLimit, RedisCallLimitJson}
import code.entitlement.Entitlement
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model._
@@ -78,6 +80,33 @@ case class UserV600(user: User, entitlements: List[Entitlement], views: Option[P
case class UsersJsonV600(current_user: UserV600, on_behalf_of_user: UserV600)
object JSONFactory600 extends CustomJsonFormats with MdcLoggable{
+
+ def createCurrentUsageJson(rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): Option[RedisCallLimitJson] = {
+ if (rateLimits.isEmpty) None
+ else {
+ val grouped: Map[LimitCallPeriod, (Option[Long], Option[Long])] =
+ rateLimits.map { case (limits, period) => period -> limits }.toMap
+
+ def getInfo(period: RateLimitingPeriod.Value): Option[RateLimit] =
+ grouped.get(period).collect {
+ case (Some(x), Some(y)) => RateLimit(Some(x), Some(y))
+ }
+
+ 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)
+ )
+ )
+ }
+ }
+
+
+
def createUserInfoJSON(current_user: UserV600, onBehalfOfUser: Option[UserV600]): UserJsonV600 = {
UserJsonV600(
user_id = current_user.user.userId,
diff --git a/obp-api/src/main/scala/code/snippet/GetHtmlFromUrl.scala b/obp-api/src/main/scala/code/snippet/GetHtmlFromUrl.scala
index dbe167349..b1016ffa1 100644
--- a/obp-api/src/main/scala/code/snippet/GetHtmlFromUrl.scala
+++ b/obp-api/src/main/scala/code/snippet/GetHtmlFromUrl.scala
@@ -44,7 +44,9 @@ object GetHtmlFromUrl extends MdcLoggable {
def vendorSupportHtml = tryo(scala.io.Source.fromURL(vendorSupportHtmlUrl))
logger.debug("vendorSupportHtml: " + vendorSupportHtml)
- def vendorSupportHtmlScript = vendorSupportHtml.map(_.mkString).getOrElse("")
+ def vendorSupportHtmlScript = vendorSupportHtml.map { source =>
+ try source.mkString finally source.close()
+ }.getOrElse("")
logger.debug("vendorSupportHtmlScript: " + vendorSupportHtmlScript)
val jsVendorSupportHtml: NodeSeq = vendorSupportHtmlScript match {
case "" =>
diff --git a/obp-api/src/main/scala/code/snippet/WebUI.scala b/obp-api/src/main/scala/code/snippet/WebUI.scala
index 8170b84e4..016d1d1f3 100644
--- a/obp-api/src/main/scala/code/snippet/WebUI.scala
+++ b/obp-api/src/main/scala/code/snippet/WebUI.scala
@@ -174,7 +174,8 @@ class WebUI extends MdcLoggable{
val sdksExternalHtmlLink = getWebUiPropsValue("webui_featured_sdks_external_link","")
val sdksExternalHtmlContent = try {
- Source.fromURL(sdksExternalHtmlLink, "UTF-8").mkString
+ val source = Source.fromURL(sdksExternalHtmlLink, "UTF-8")
+ try source.mkString finally source.close()
} catch {
case _ : Throwable => "
SDK Showcases is wrong, please check the props `webui_featured_sdks_external_link`
"
}
@@ -199,7 +200,8 @@ class WebUI extends MdcLoggable{
val mainFaqHtmlLink = getWebUiPropsValue("webui_main_faq_external_link","")
val mainFaqExternalHtmlContent = try {
- Source.fromURL(mainFaqHtmlLink, "UTF-8").mkString
+ val source = Source.fromURL(mainFaqHtmlLink, "UTF-8")
+ try source.mkString finally source.close()
} catch {
case _ : Throwable => "FAQs is wrong, please check the props `webui_main_faq_external_link`
"
}
@@ -618,7 +620,9 @@ class WebUI extends MdcLoggable{
logger.info("htmlTry: " + htmlTry)
// Convert to a string
- val htmlString = htmlTry.map(_.mkString).getOrElse("")
+ val htmlString = htmlTry.map { source =>
+ try source.mkString finally source.close()
+ }.getOrElse("")
logger.info("htmlString: " + htmlString)
// Create an HTML object
diff --git a/obp-api/src/test/scala/code/api/berlin/group/signing/PSD2RequestSigner.scala b/obp-api/src/test/scala/code/api/berlin/group/signing/PSD2RequestSigner.scala
new file mode 100644
index 000000000..3a10585a6
--- /dev/null
+++ b/obp-api/src/test/scala/code/api/berlin/group/signing/PSD2RequestSigner.scala
@@ -0,0 +1,161 @@
+package code.api.berlin.group.signing
+
+import java.nio.charset.StandardCharsets
+import java.security.spec.PKCS8EncodedKeySpec
+import java.security.{KeyFactory, MessageDigest, PrivateKey, Signature}
+import java.time.format.DateTimeFormatter
+import java.time.{ZoneOffset, ZonedDateTime}
+import java.util.{Base64, UUID}
+import scala.util.{Failure, Success, Try}
+
+/**
+ * PSD2 Request Signer for Berlin Group API calls
+ *
+ * This utility provides cryptographic signing for Berlin Group PSD2 API requests.
+ * It follows the HTTP signature standard required by PSD2 regulations.
+ *
+ * Usage:
+ * val signer = new PSD2RequestSigner(privateKeyPem, certificatePem)
+ * val headers = signer.signRequest(requestBody)
+ */
+class PSD2RequestSigner(
+ privateKeyPem: String,
+ certificatePem: String,
+ keyId: String = "SN=1082, CA=CN=MAIB Prisacaru Sergiu (Test), O=MAIB"
+) {
+
+ // Parse private key once during initialization
+ private val privateKey: PrivateKey = parsePrivateKey(privateKeyPem) match {
+ case Success(key) => key
+ case Failure(ex) => throw new IllegalArgumentException(s"Invalid private key: ${ex.getMessage}", ex)
+ }
+
+ // Encode certificate once during initialization
+ private val certificateBase64: String = Base64.getEncoder.encodeToString(
+ certificatePem.getBytes(StandardCharsets.UTF_8)
+ )
+
+ /**
+ * Sign a Berlin Group API request and return headers
+ *
+ * @param requestBody The JSON request body as string
+ * @param psuDeviceId Optional PSU device ID (default: "device-1234567890")
+ * @param psuDeviceName Optional PSU device name (default: "Kalina-PC")
+ * @param psuIpAddress Optional PSU IP address (default: "psu-service.local")
+ * @param tppRedirectUri Optional TPP redirect URI (default: "tppapp://example.com/redirect")
+ * @param tppNokRedirectUri Optional TPP error redirect URI (default: "https://example.com/redirect")
+ * @return Map of HTTP headers for the signed request
+ */
+ def signRequest(
+ requestBody: String,
+ psuDeviceId: String = "device-1234567890",
+ psuDeviceName: String = "Kalina-PC",
+ psuIpAddress: String = "psu-service.local", // Use DNS/hostname instead of raw IP
+ tppRedirectUri: String = "tppapp://example.com/redirect",
+ tppNokRedirectUri: String = "https://example.com/redirect"
+ ): Map[String, String] = {
+
+ // Generate required header values
+ val xRequestId = UUID.randomUUID().toString
+ val dateHeader = ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME)
+ val digestHeader = createDigestHeader(requestBody)
+
+ // Create signature string according to PSD2 specification
+ val dataToSign = s"digest: $digestHeader\ndate: $dateHeader\nx-request-id: $xRequestId"
+ val signature = signData(dataToSign)
+
+ // Create signature header
+ val signatureHeader = s"""keyId="$keyId", algorithm="rsa-sha256", headers="digest date x-request-id", signature="$signature""""
+
+ // Return complete headers map
+ Map(
+ "Content-Type" -> "application/json",
+ "Date" -> dateHeader,
+ "X-Request-ID" -> xRequestId,
+ "Digest" -> digestHeader,
+ "Signature" -> signatureHeader,
+ "TPP-Signature-Certificate" -> certificateBase64,
+ "PSU-Device-ID" -> psuDeviceId,
+ "PSU-Device-Name" -> psuDeviceName,
+ "PSU-IP-Address" -> psuIpAddress,
+ "TPP-Redirect-URI" -> tppRedirectUri,
+ "TPP-Nok-Redirect-URI" -> tppNokRedirectUri
+ )
+ }
+
+ /**
+ * Create SHA-256 digest header for request body
+ */
+ private def createDigestHeader(requestBody: String): String = {
+ val digest = MessageDigest.getInstance("SHA-256")
+ val hashBytes = digest.digest(requestBody.getBytes(StandardCharsets.UTF_8))
+ val base64Hash = Base64.getEncoder.encodeToString(hashBytes)
+ s"SHA-256=$base64Hash"
+ }
+
+ /**
+ * Sign data using RSA-SHA256 algorithm
+ */
+ private def signData(dataToSign: String): String = {
+ val signature = Signature.getInstance("SHA256withRSA")
+ signature.initSign(privateKey)
+ signature.update(dataToSign.getBytes(StandardCharsets.UTF_8))
+ val signatureBytes = signature.sign()
+ Base64.getEncoder.encodeToString(signatureBytes)
+ }
+
+ /**
+ * Parse PEM-formatted private key
+ */
+ private def parsePrivateKey(privateKeyPem: String): Try[PrivateKey] = Try {
+ val cleanedPem = privateKeyPem
+ .replace("-----BEGIN PRIVATE KEY-----", "")
+ .replace("-----END PRIVATE KEY-----", "")
+ .replace("-----BEGIN RSA PRIVATE KEY-----", "")
+ .replace("-----END RSA PRIVATE KEY-----", "")
+ .replaceAll("\\s", "")
+
+ val keyBytes = Base64.getDecoder.decode(cleanedPem)
+ val keySpec = new PKCS8EncodedKeySpec(keyBytes)
+ val keyFactory = KeyFactory.getInstance("RSA")
+ keyFactory.generatePrivate(keySpec)
+ }
+}
+
+
+
+/**
+ * Simple trait for mixing into test classes to provide PSD2 signing capabilities
+ */
+trait PSD2SigningSupport {
+
+ /**
+ * Override these in your test class to provide actual certificate content
+ */
+ def berlinGroupPrivateKey: String = throw new NotImplementedError("berlinGroupPrivateKey must be implemented")
+ def berlinGroupCertificate: String = throw new NotImplementedError("berlinGroupCertificate must be implemented")
+ def berlinGroupKeyId: String = "SN=1082, CA=CN=MAIB Prisacaru Sergiu (Test), O=MAIB"
+
+ private lazy val psd2Signer = new PSD2RequestSigner(berlinGroupPrivateKey, berlinGroupCertificate, berlinGroupKeyId)
+
+ /**
+ * Sign a Berlin Group request and return headers
+ */
+ def signPSD2Request(requestBody: String): Map[String, String] = {
+ psd2Signer.signRequest(requestBody)
+ }
+
+ /**
+ * Sign a Berlin Group request with custom PSU parameters
+ */
+ def signPSD2Request(
+ requestBody: String,
+ psuDeviceId: String,
+ psuDeviceName: String,
+ psuIpAddress: String,
+ tppRedirectUri: String = "tppapp://example.com/redirect",
+ tppNokRedirectUri: String = "https://example.com/redirect"
+ ): Map[String, String] = {
+ psd2Signer.signRequest(requestBody, psuDeviceId, psuDeviceName, psuIpAddress, tppRedirectUri, tppNokRedirectUri)
+ }
+}
\ No newline at end of file
diff --git a/obp-api/src/test/scala/code/api/berlin/group/signing/PSD2SigningTestSupport.scala b/obp-api/src/test/scala/code/api/berlin/group/signing/PSD2SigningTestSupport.scala
new file mode 100644
index 000000000..fe43b02d1
--- /dev/null
+++ b/obp-api/src/test/scala/code/api/berlin/group/signing/PSD2SigningTestSupport.scala
@@ -0,0 +1,136 @@
+package code.api.berlin.group.signing
+
+import code.api.util.APIUtil
+import net.liftweb.common.Box
+import net.liftweb.util.Props
+import org.scalatest.{BeforeAndAfterEach, Suite}
+
+import java.nio.file.Path
+import scala.util.{Failure, Success}
+
+/**
+ * Test support trait that automatically generates and configures PSD2 certificates on the fly
+ * This eliminates the need for external certificate files in tests
+ */
+trait PSD2SigningTestSupport extends BeforeAndAfterEach with PSD2SigningSupport { self: Suite =>
+
+ // Generated certificate data
+ private var _certificateData: Option[TestCertificateGenerator.CertificateData] = None
+ private var _p12Path: Option[Path] = None
+
+ // Test configuration
+ protected def tppSignaturePassword: String = "testpassword123"
+ protected def tppSignatureAlias: String = "test-tpp-alias"
+ protected def tppCommonName: String = "Berlin Group Test TPP"
+ protected def tppOrganization: String = "Test Bank Organization"
+
+ override def beforeEach(): Unit = {
+ super.beforeEach()
+
+ // Generate certificates on the fly
+ TestCertificateGenerator.generateTestCertificateWithTempFiles(
+ commonName = tppCommonName,
+ organizationName = tppOrganization,
+ password = tppSignaturePassword,
+ alias = tppSignatureAlias
+ ) match {
+ case Success((certData, tempP12Path)) =>
+ _certificateData = Some(certData)
+ _p12Path = Some(tempP12Path)
+
+ // Set up properties for the test
+ setPropsValues(
+ "truststore.path.tpp_signature" -> tempP12Path.toString,
+ "truststore.password.tpp_signature" -> tppSignaturePassword,
+ "truststore.alias.tpp_signature" -> tppSignatureAlias,
+ "use_tpp_signature_revocation_list" -> "false"
+ )
+
+ println(s"Generated test certificate for: $tppCommonName")
+ println(s"Created temporary P12 keystore at: $tempP12Path")
+
+ case Failure(exception) =>
+ throw new RuntimeException(s"Failed to generate test certificates: ${exception.getMessage}", exception)
+ }
+ }
+
+ override def afterEach(): Unit = {
+ // Clean up temporary files
+ _p12Path.foreach { path =>
+ try {
+ java.nio.file.Files.deleteIfExists(path)
+ } catch {
+ case _: Exception => // Ignore cleanup errors
+ }
+ }
+ _p12Path = None
+ _certificateData = None
+
+ super.afterEach()
+ }
+
+ // Implementation of PSD2SigningSupport
+ override def berlinGroupPrivateKey: String = {
+ _certificateData
+ .map(_.privateKeyPem)
+ .getOrElse(throw new IllegalStateException("Certificate data not initialized. Make sure beforeEach() is called."))
+ }
+
+ override def berlinGroupCertificate: String = {
+ _certificateData
+ .map(_.certificatePem)
+ .getOrElse(throw new IllegalStateException("Certificate data not initialized. Make sure beforeEach() is called."))
+ }
+
+ override def berlinGroupKeyId: String = {
+ _certificateData match {
+ case Some(certData) =>
+ val serialNumber = certData.serialNumber.toString
+ s"SN=$serialNumber, CA=CN=$tppCommonName, O=$tppOrganization"
+ case None =>
+ throw new IllegalStateException("Certificate data not initialized. Make sure beforeEach() is called.")
+ }
+ }
+
+ /**
+ * This method should be provided by the parent test class that extends PropsReset
+ * We assume it's available from the test setup
+ */
+ protected def setPropsValues(keyValuePairs: (String, String)*): Unit
+
+ /**
+ * Get the generated certificate data for advanced test scenarios
+ */
+ protected def getCertificateData: Option[TestCertificateGenerator.CertificateData] = _certificateData
+
+ /**
+ * Get the temporary P12 file path
+ */
+ protected def getP12Path: Option[Path] = _p12Path
+
+ /**
+ * Validate that all necessary properties are set
+ */
+ protected def validateTestSetup(): Unit = {
+ val requiredProps = List(
+ "truststore.path.tpp_signature",
+ "truststore.password.tpp_signature",
+ "truststore.alias.tpp_signature"
+ )
+
+ requiredProps.foreach { prop =>
+ val value = APIUtil.getPropsValue(prop).getOrElse("")
+ if (value.isEmpty) {
+ throw new IllegalStateException(s"Required property '$prop' is not set")
+ }
+ }
+
+ // Verify certificate data is available
+ if (_certificateData.isEmpty) {
+ throw new IllegalStateException("Certificate data is not initialized")
+ }
+
+ println("Test setup validation passed")
+ }
+}
+
diff --git a/obp-api/src/test/scala/code/api/berlin/group/signing/RegulatedEntityTest.scala b/obp-api/src/test/scala/code/api/berlin/group/signing/RegulatedEntityTest.scala
new file mode 100644
index 000000000..81eab6aa9
--- /dev/null
+++ b/obp-api/src/test/scala/code/api/berlin/group/signing/RegulatedEntityTest.scala
@@ -0,0 +1,113 @@
+package code.api.berlin.group.signing
+
+import code.api.berlin.group.v1_3.BerlinGroupServerSetupV1_3
+import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.ErrorMessagesBG
+
+class RegulatedEntityTest extends BerlinGroupServerSetupV1_3 with PSD2SigningTestSupport {
+
+ override def beforeEach(): Unit = {
+ super.beforeEach()
+
+ // Additional test-specific properties
+ setPropsValues(
+ "use_consumer_limits" -> "false",
+ "allow_anonymous_access" -> "true",
+ "berlin_group_psd2_signing_enabled" -> "true"
+ )
+
+ // Validate that everything is set up correctly
+ validateTestSetup()
+ }
+
+ // Override certificate details for this specific test
+ override protected def tppCommonName: String = "Berlin Group Test TPP Certificate"
+ override protected def tppOrganization: String = "Some Test Bank"
+ override protected def tppSignaturePassword: String = "testpassword123"
+ override protected def tppSignatureAlias: String = "bnm test"
+
+ scenario("Create signed consent request with dynamically generated certificates") {
+ Given("A consent request body")
+ val requestBody = """{
+ "access": {
+ "accounts": [],
+ "balances": [],
+ "transactions": []
+ },
+ "recurringIndicator": true,
+ "validUntil": "2024-12-31",
+ "frequencyPerDay": 4
+ }"""
+
+ When("I sign the request using the generated certificates")
+ val headers = signPSD2Request(requestBody)
+
+ Then("The headers should contain the required PSD2 signing elements")
+ headers should contain key "X-Request-ID"
+ headers should contain key "Digest"
+ headers should contain key "TPP-Signature-Certificate"
+ headers should contain key "Signature"
+
+ And("I can use the signed request with OBP's HTTP client")
+ val request = (V1_3_BG / "consents").POST
+ val response = makePostRequestAdditionalHeader(request, requestBody, headers.toList)
+
+ // Since this is a test certificate, we expect authentication to fail but with proper structure
+ response.code should equal(401)
+ response.body.extract[ErrorMessagesBG].tppMessages.head.code should equal("CERTIFICATE_BLOCKED")
+ }
+
+ scenario("Test certificate validation and signing process") {
+ Given("A payment initiation request body")
+ val paymentRequestBody = """{
+ "instructedAmount": {
+ "currency": "EUR",
+ "amount": "123.45"
+ },
+ "debtorAccount": {
+ "iban": "DE02100100109307118603"
+ },
+ "creditorName": "John Doe",
+ "creditorAccount": {
+ "iban": "DE23100120020123456789"
+ },
+ "remittanceInformationUnstructured": "Test payment"
+ }"""
+
+ When("I create a signature for the payment request")
+ val signedHeaders = signPSD2Request(paymentRequestBody)
+
+ Then("The signature should be valid and contain all required headers")
+ signedHeaders should have size (11)
+ signedHeaders("Digest") should startWith("SHA-256=")
+ signedHeaders("Signature") should include("keyId=")
+ signedHeaders("X-Request-ID") should not be empty
+
+ And("The request should be properly formatted for Berlin Group API")
+ val request = (V1_3_BG / "payments" / "sepa-credit-transfers").POST
+ val response = makePostRequestAdditionalHeader(request, paymentRequestBody, signedHeaders.toList)
+
+ // We expect authentication failure with test certificates, but the structure should be valid
+ response.code should (equal(401) or equal(400) or equal(403))
+ }
+
+ scenario("Test custom certificate parameters") {
+ Given("Custom certificate parameters")
+ val customCertData = TestCertificateGenerator.generateTestCertificate(
+ commonName = "Custom Test Certificate",
+ organizationName = "Custom Test Org",
+ validityDays = 30
+ )
+
+ customCertData should be a 'success
+
+ When("I inspect the generated certificate")
+ val certData = customCertData.get
+
+ Then("It should have the correct properties")
+ certData.certificate.getSubjectDN.getName should include("Custom Test Certificate")
+ certData.certificate.getSubjectDN.getName should include("Custom Test Org")
+
+ And("The certificate should be valid")
+ certData.certificate.checkValidity() // Should not throw exception
+ }
+}
\ No newline at end of file
diff --git a/obp-api/src/test/scala/code/api/berlin/group/signing/TestCertificateGenerator.scala b/obp-api/src/test/scala/code/api/berlin/group/signing/TestCertificateGenerator.scala
new file mode 100644
index 000000000..cbebada85
--- /dev/null
+++ b/obp-api/src/test/scala/code/api/berlin/group/signing/TestCertificateGenerator.scala
@@ -0,0 +1,207 @@
+package code.api.berlin.group.signing
+
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.asn1.x509.{BasicConstraints, Extension, KeyUsage, SubjectPublicKeyInfo}
+import org.bouncycastle.cert.jcajce.{JcaX509CertificateConverter, JcaX509v3CertificateBuilder}
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
+
+import java.io.{ByteArrayOutputStream, StringWriter}
+import java.math.BigInteger
+import java.nio.file.{Files, Path}
+import java.security._
+import java.security.cert.X509Certificate
+import java.time.{LocalDateTime, ZoneOffset}
+import java.util.Date
+import scala.util.Try
+
+/**
+ * Utility class for generating test certificates and keystores on the fly
+ * Used for Berlin Group PSD2 testing without relying on external certificate files
+ */
+object TestCertificateGenerator {
+
+ // Add BouncyCastle provider
+ Security.addProvider(new BouncyCastleProvider())
+
+ case class CertificateData(
+ privateKey: PrivateKey,
+ publicKey: PublicKey,
+ certificate: X509Certificate,
+ privateKeyPem: String,
+ certificatePem: String,
+ p12Data: Array[Byte],
+ serialNumber: BigInteger
+ )
+
+ /**
+ * Generate a self-signed test certificate with private key
+ */
+ def generateTestCertificate(
+ commonName: String = "Test TPP Certificate",
+ organizationName: String = "Test Organization",
+ keySize: Int = 2048,
+ validityDays: Int = 365,
+ password: String = "password",
+ alias: String = "test-alias"
+ ): Try[CertificateData] = {
+
+ Try {
+ // Generate key pair
+ val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
+ keyPairGenerator.initialize(keySize)
+ val keyPair = keyPairGenerator.generateKeyPair()
+
+ val privateKey = keyPair.getPrivate
+ val publicKey = keyPair.getPublic
+
+ // Create certificate
+ val now = new Date()
+ val notBefore = now
+ val notAfter = Date.from(LocalDateTime.now().plusDays(validityDays).toInstant(ZoneOffset.UTC))
+
+ val dnName = new X500Name(s"CN=$commonName, O=$organizationName, C=US")
+ val certSerialNumber = BigInteger.valueOf(System.currentTimeMillis())
+
+ val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded)
+
+ val certBuilder = new JcaX509v3CertificateBuilder(
+ dnName, // issuer
+ certSerialNumber,
+ notBefore,
+ notAfter,
+ dnName, // subject (same as issuer for self-signed)
+ publicKey
+ )
+
+ // Add extensions
+ certBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(false))
+ certBuilder.addExtension(Extension.keyUsage, false,
+ new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.nonRepudiation))
+
+ // Sign the certificate
+ val contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey)
+ val certHolder = certBuilder.build(contentSigner)
+
+ val certificateConverter = new JcaX509CertificateConverter()
+ val certificate = certificateConverter.getCertificate(certHolder)
+
+ // Convert to PEM format
+ val privateKeyPem = convertPrivateKeyToPem(privateKey)
+ val certificatePem = convertCertificateToPem(certificate)
+
+ // Create P12 data
+ val p12Data = createP12KeyStore(privateKey, certificate, alias, password)
+
+ CertificateData(
+ privateKey = privateKey,
+ publicKey = publicKey,
+ certificate = certificate,
+ privateKeyPem = privateKeyPem,
+ certificatePem = certificatePem,
+ p12Data = p12Data,
+ serialNumber = certSerialNumber
+ )
+ }
+ }
+
+ /**
+ * Convert private key to PEM format string
+ */
+ private def convertPrivateKeyToPem(privateKey: PrivateKey): String = {
+ val stringWriter = new StringWriter()
+ val pemWriter = new JcaPEMWriter(stringWriter)
+ try {
+ pemWriter.writeObject(privateKey)
+ pemWriter.flush()
+ stringWriter.toString
+ } finally {
+ pemWriter.close()
+ stringWriter.close()
+ }
+ }
+
+ /**
+ * Convert certificate to PEM format string
+ */
+ private def convertCertificateToPem(certificate: X509Certificate): String = {
+ val stringWriter = new StringWriter()
+ val pemWriter = new JcaPEMWriter(stringWriter)
+ try {
+ pemWriter.writeObject(certificate)
+ pemWriter.flush()
+ stringWriter.toString
+ } finally {
+ pemWriter.close()
+ stringWriter.close()
+ }
+ }
+
+ /**
+ * Create a PKCS12 keystore with the private key and certificate
+ * Also adds the certificate as a trusted certificate entry for truststore validation
+ */
+ private def createP12KeyStore(
+ privateKey: PrivateKey,
+ certificate: X509Certificate,
+ alias: String,
+ password: String
+ ): Array[Byte] = {
+ val keyStore = KeyStore.getInstance("PKCS12")
+ keyStore.load(null, null)
+
+ // Add key entry (private key + certificate chain)
+ val certChain = Array[java.security.cert.Certificate](certificate)
+ keyStore.setKeyEntry(alias, privateKey, password.toCharArray, certChain)
+
+ // Add trusted certificate entry for truststore validation
+ keyStore.setCertificateEntry(s"trusted-$alias", certificate)
+
+ val outputStream = new ByteArrayOutputStream()
+ try {
+ keyStore.store(outputStream, password.toCharArray)
+ outputStream.toByteArray
+ } finally {
+ outputStream.close()
+ }
+ }
+
+ /**
+ * Write P12 data to a temporary file and return the path
+ */
+ def writeP12ToTempFile(p12Data: Array[Byte], prefix: String = "test-keystore"): Try[Path] = {
+ Try {
+ val tempFile = Files.createTempFile(prefix, ".p12")
+ Files.write(tempFile, p12Data)
+ // Mark for deletion on exit
+ tempFile.toFile.deleteOnExit()
+ tempFile
+ }
+ }
+
+ /**
+ * Generate a complete test certificate setup with temporary files
+ */
+ def generateTestCertificateWithTempFiles(
+ commonName: String = "Test Berlin Group TPP",
+ organizationName: String = "Test Bank",
+ password: String = "testpassword123",
+ alias: String = "test-tpp-alias"
+ ): Try[(CertificateData, Path)] = {
+ for {
+ certData <- generateTestCertificate(commonName, organizationName, password = password, alias = alias)
+ tempP12Path <- writeP12ToTempFile(certData.p12Data, "berlin-group-test")
+ } yield (certData, tempP12Path)
+ }
+
+ /**
+ * Default certificate data for Berlin Group tests
+ */
+ lazy val defaultBerlinGroupTestCertificate: Try[CertificateData] = {
+ generateTestCertificate(
+ commonName = "Berlin Group Test TPP Certificate",
+ organizationName = "MAIB Test Bank"
+ )
+ }
+}
\ No newline at end of file
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 4b9b4f527..9991aeba5 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
@@ -124,7 +124,7 @@ class RateLimitingTest extends V510ServerSetup with PropsReset {
val response510 = makeGetRequest(request510)
Then("We should get a 200")
response510.code should equal(200)
- response510.body.extract[CallLimitJson510]
+ response510.body.extract[CallLimitsJson510]
}
diff --git a/obp-api/src/test/scala/code/fx/PutFX.scala b/obp-api/src/test/scala/code/fx/PutFX.scala
index 142c44e03..3d7bc0408 100644
--- a/obp-api/src/test/scala/code/fx/PutFX.scala
+++ b/obp-api/src/test/scala/code/fx/PutFX.scala
@@ -139,7 +139,10 @@ object PutFX extends SendServerRequests {
println(s"fxDataPath is $fxDataPath")
// This contains a list of fx rates.
- val fxListData = JsonParser.parse(Source.fromFile(fxDataPath.getOrElse("ERROR")).mkString)
+ val fxListData = {
+ val source = Source.fromFile(fxDataPath.getOrElse("ERROR"))
+ try JsonParser.parse(source.mkString) finally source.close()
+ }
var fxrates = ListBuffer[FxJson]()
diff --git a/obp-api/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala b/obp-api/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala
index 6d317913e..7f3b48e4f 100644
--- a/obp-api/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala
+++ b/obp-api/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala
@@ -83,7 +83,10 @@ object PostCounterpartyMetadata extends SendServerRequests {
// This contains a list of counterparty lists. one list for each region
- val counerpartyListData = JsonParser.parse(Source.fromFile(counterpartyDataPath).mkString)
+ val counerpartyListData = {
+ val source = Source.fromFile(counterpartyDataPath)
+ try JsonParser.parse(source.mkString) finally source.close()
+ }
var counterparties = ListBuffer[CounterpartyJSONRecord]()
// Loop through the lists
@@ -122,7 +125,10 @@ object PostCounterpartyMetadata extends SendServerRequests {
val mainDataPath = "/Users/simonredfern/Documents/OpenBankProject/DATA/May_2018_ABN_Netherlands_extra/loaded_01/OBP_sandbox_pretty.json"
- val mainData = JsonParser.parse(Source.fromFile(mainDataPath).mkString)
+ val mainData = {
+ val source = Source.fromFile(mainDataPath)
+ try JsonParser.parse(source.mkString) finally source.close()
+ }
val users = (mainData \ "users").children
println("got " + users.length + " users")
diff --git a/obp-api/src/test/scala/code/sandbox/PostCustomer.scala b/obp-api/src/test/scala/code/sandbox/PostCustomer.scala
index 426b7ff9c..5f2274bb2 100644
--- a/obp-api/src/test/scala/code/sandbox/PostCustomer.scala
+++ b/obp-api/src/test/scala/code/sandbox/PostCustomer.scala
@@ -103,7 +103,10 @@ object PostCustomer extends SendServerRequests {
println(s"customerDataPath is $customerDataPath")
// This contains a list of customers.
- val customerListData = JsonParser.parse(Source.fromFile(customerDataPath.getOrElse("ERROR")).mkString)
+ val customerListData = {
+ val source = Source.fromFile(customerDataPath.getOrElse("ERROR"))
+ try JsonParser.parse(source.mkString) finally source.close()
+ }
var customers = ListBuffer[CustomerFullJson]()
@@ -127,7 +130,10 @@ object PostCustomer extends SendServerRequests {
println(s"mainDataPath is $mainDataPath")
- val mainData = JsonParser.parse(Source.fromFile(mainDataPath.getOrElse("ERROR")).mkString)
+ val mainData = {
+ val source = Source.fromFile(mainDataPath.getOrElse("ERROR"))
+ try JsonParser.parse(source.mkString) finally source.close()
+ }
val users = (mainData \ "users").children
println("got " + users.length + " users")