mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 15:06:50 +00:00
Merge pull request #2619 from constantine2nd/develop
Duplicate consumer creation on consent creation
This commit is contained in:
commit
4a5e652cd8
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 "" => <script></script>
|
||||
|
||||
@ -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 => "<h1>SDK Showcases is wrong, please check the props `webui_featured_sdks_external_link` </h1>"
|
||||
}
|
||||
@ -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 => "<h1>FAQs is wrong, please check the props `webui_main_faq_external_link` </h1>"
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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]
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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]()
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user