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")