From 6eacf17732e75d3a30fc6dc40ce46f0bb1cebedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 22 Sep 2025 15:24:11 +0200 Subject: [PATCH 1/9] bugfix/Duplicate consumer creation on consent creation --- .../code/api/util/BerlinGroupSigning.scala | 94 +++++++++---------- 1 file changed, 46 insertions(+), 48 deletions(-) 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..bf463a2ce 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -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 => + (Failure(ErrorMessages.RegulatedEntityNotFoundByCertificate), 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(", ") + (Failure(s"${ErrorMessages.RegulatedEntityAmbiguityByCertificate}: multiple TPPs found: $names"), forwardResult._2) } } } From 71d7bca3947dde2ff587658327c9dc4cd1a56075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 23 Sep 2025 13:10:10 +0200 Subject: [PATCH 2/9] feature/Call limits endpoints --- .../SwaggerDefinitionsJSON.scala | 18 ++++++ .../scala/code/api/v5_1_0/APIMethods510.scala | 14 ++--- .../code/api/v5_1_0/JSONFactory5.1.0.scala | 61 ++++++------------- .../code/api/v5_1_0/RateLimitingTest.scala | 2 +- 4 files changed, 44 insertions(+), 51 deletions(-) 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/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/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] } From 5c5359e672047f21449248364bf52df5df2c503e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 24 Sep 2025 12:35:31 +0200 Subject: [PATCH 3/9] feature/Add Get Call Limits for a Consumer Usage v6.0.0 --- .../scala/code/api/v6_0_0/APIMethods600.scala | 45 ++++++++++++++++++- .../code/api/v6_0_0/JSONFactory6.0.0.scala | 29 ++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) 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, From 58d4d46954f065e81e145ddc5fad544bf3cd37a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 24 Sep 2025 15:55:22 +0200 Subject: [PATCH 4/9] feature/Make Consumer role Error BG spec friendly --- obp-api/src/main/scala/code/api/OBPRestHelper.scala | 12 ++++++++++++ obp-api/src/main/scala/code/api/util/APIUtil.scala | 6 +++--- .../main/scala/code/api/util/BerlinGroupError.scala | 7 +++++-- 3 files changed, 20 insertions(+), 5 deletions(-) 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/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" From 621d47263a9b41e8322f9d4d75b76d4f4bb814b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 25 Sep 2025 06:44:57 +0200 Subject: [PATCH 5/9] test/Check get regulated entity by tpp certificate --- .../code/api/util/BerlinGroupSigning.scala | 12 +- .../group/signing/PSD2RequestSigner.scala | 161 ++++++++++++++ .../signing/PSD2SigningTestSupport.scala | 136 ++++++++++++ .../group/signing/RegulatedEntityTest.scala | 113 ++++++++++ .../signing/TestCertificateGenerator.scala | 207 ++++++++++++++++++ 5 files changed, 623 insertions(+), 6 deletions(-) create mode 100644 obp-api/src/test/scala/code/api/berlin/group/signing/PSD2RequestSigner.scala create mode 100644 obp-api/src/test/scala/code/api/berlin/group/signing/PSD2SigningTestSupport.scala create mode 100644 obp-api/src/test/scala/code/api/berlin/group/signing/RegulatedEntityTest.scala create mode 100644 obp-api/src/test/scala/code/api/berlin/group/signing/TestCertificateGenerator.scala 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 bf463a2ce..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 @@ -288,7 +288,7 @@ object BerlinGroupSigning extends MdcLoggable { } yield { entities match { case Nil => - (Failure(ErrorMessages.RegulatedEntityNotFoundByCertificate), forwardResult._2) + (ObpApiFailure(ErrorMessages.RegulatedEntityNotFoundByCertificate, 401, forwardResult._2), forwardResult._2) case single :: Nil => val idno = single.entityCode @@ -334,7 +334,7 @@ object BerlinGroupSigning extends MdcLoggable { case multiple => val names = multiple.map(e => s"'${e.entityName}' (Code: ${e.entityCode})").mkString(", ") - (Failure(s"${ErrorMessages.RegulatedEntityAmbiguityByCertificate}: multiple TPPs found: $names"), forwardResult._2) + (ObpApiFailure(s"${ErrorMessages.RegulatedEntityAmbiguityByCertificate}: multiple TPPs found: $names", 401, forwardResult._2), forwardResult._2) } } } 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..ae00eb840 --- /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: "192.168.1.42") + * @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 = "192.168.1.42", + 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 From f12bdfb15c6e8bb71ad43558ed968543e1b85d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 25 Sep 2025 06:56:59 +0200 Subject: [PATCH 6/9] feature/Ensures that the underlying input stream is released even if an exception occurs while reading --- .../src/main/scala/code/snippet/GetHtmlFromUrl.scala | 4 +++- obp-api/src/main/scala/code/snippet/WebUI.scala | 10 +++++++--- obp-api/src/test/scala/code/fx/PutFX.scala | 5 ++++- .../scala/code/sandbox/PostCounterpartyMetadata.scala | 10 ++++++++-- obp-api/src/test/scala/code/sandbox/PostCustomer.scala | 10 ++++++++-- 5 files changed, 30 insertions(+), 9 deletions(-) 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/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") From 55ee02b7b7a6453e681bf5b86631ee8c5b11de73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 25 Sep 2025 11:02:46 +0200 Subject: [PATCH 7/9] feature/Enhance Docker development workflow --- README.md | 11 ++ {docker => development/docker}/Dockerfile | 2 +- development/docker/README.md | 180 ++++++++++++++++++ .../docker/docker-compose.override.yml | 7 + development/docker/docker-compose.yml | 13 ++ development/docker/entrypoint.sh | 73 +++++++ development/docker/test-setup.sh | 180 ++++++++++++++++++ docker/README.md | 96 ---------- docker/docker-compose.override.yml | 7 - docker/docker-compose.yml | 14 -- docker/entrypoint.sh | 9 - 11 files changed, 465 insertions(+), 127 deletions(-) rename {docker => development/docker}/Dockerfile (86%) create mode 100644 development/docker/README.md create mode 100644 development/docker/docker-compose.override.yml create mode 100644 development/docker/docker-compose.yml create mode 100755 development/docker/entrypoint.sh create mode 100755 development/docker/test-setup.sh delete mode 100644 docker/README.md delete mode 100644 docker/docker-compose.override.yml delete mode 100644 docker/docker-compose.yml delete mode 100644 docker/entrypoint.sh diff --git a/README.md b/README.md index 54882e544..97b747c67 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,17 @@ Props values can be set as environment variables. Props need to be prefixed with `openid_connect.enabled=true` becomes `OBP_OPENID_CONNECT_ENABLED=true`. +### Development Docker Setup + +For local development with Docker Compose, see the Docker setup in `development/docker/`: + +```bash +cd development/docker +docker-compose up --build +``` + +See `development/docker/README.md` for detailed instructions and configuration options. + ## Databases The default database for testing etc is H2. PostgreSQL is used for the sandboxes (user accounts, metadata, transaction cache). The list of databases fully tested is: PostgreSQL, MS SQL and H2. diff --git a/docker/Dockerfile b/development/docker/Dockerfile similarity index 86% rename from docker/Dockerfile rename to development/docker/Dockerfile index 53a999d1d..87ab04cab 100644 --- a/docker/Dockerfile +++ b/development/docker/Dockerfile @@ -11,7 +11,7 @@ EXPOSE 8080 RUN mvn install -pl .,obp-commons -am -DskipTests # Copy entrypoint script that runs mvn with needed JVM flags -COPY docker/entrypoint.sh /app/entrypoint.sh +COPY development/docker/entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh # Use script as entrypoint diff --git a/development/docker/README.md b/development/docker/README.md new file mode 100644 index 000000000..918b10767 --- /dev/null +++ b/development/docker/README.md @@ -0,0 +1,180 @@ +## OBP API – Docker & Docker Compose Setup + +This project uses Docker and Docker Compose to run the **OBP API** service with Maven and Jetty. + +- Java 17 with reflection workaround +- **Automatic database URL transformation** for seamless Docker development +- Connects to your local Postgres using `host.docker.internal` +- Supports separate dev & prod setups + +--- + +## How to use + +> **Make sure you have Docker and Docker Compose installed.** +> **Navigate to the `development/docker` directory before running commands.** + +```bash +cd development/docker +``` + +### Automatic Database Configuration πŸš€ + +The Docker setup now **automatically transforms your database configuration** for Docker environments! + +**What this means:** +- Set your database URL in props file with `localhost` (for local development) +- Docker automatically transforms `localhost` to `host.docker.internal` +- **No need to change props files when switching between local and Docker!** + +**Example:** +```properties +# In your obp-api/src/main/resources/props/default.props +db.url=jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=f +``` + +When you run with Docker, this automatically becomes: +``` +jdbc:postgresql://host.docker.internal:5432/obp_mapped?user=obp&password=f +``` + +**Password changes are automatically reflected!** +If you change your password in the props file, Docker will use the new password automatically. + +--- + +### Build & run + +Build the Docker image and run the container: + +```bash +docker-compose up --build +``` + +The service will be available at [http://localhost:8080](http://localhost:8080). + +--- + +## Development tips + +For live code updates without rebuilding: + +* Use the provided `docker-compose.override.yml` which mounts only: + + ```yaml + volumes: + - ../../obp-api:/app/obp-api + - ../../obp-commons:/app/obp-commons + ``` +* This keeps other built files (like `entrypoint.sh`) intact. +* Avoid mounting the full `../../:/app` because it overwrites the built image. + +--- + +## How the automatic transformation works + +1. **Local Development**: Use your props file with `localhost`: + ```properties + db.url=jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=newpass + ``` + +2. **Docker Startup**: The `entrypoint.sh` script: + - Reads your current `db.url` from `default.props` + - Automatically transforms `localhost` β†’ `host.docker.internal` + - Sets the `OBP_DB_URL` environment variable + - Starts the application + +3. **Result**: OBP-API uses the transformed URL in Docker, original URL locally. + +--- + +## Useful commands + +Rebuild the image and restart: + +```bash +docker-compose up --build +``` + +Stop the container: + +```bash +docker-compose down +``` + +View startup logs to see the database transformation: + +```bash +docker-compose logs obp-api +``` + +--- + +## Before first run + +### 1. Database Setup +Ensure PostgreSQL is running on your host machine with a database accessible via: +``` +jdbc:postgresql://localhost:5432/YOUR_DB_NAME?user=YOUR_USER&password=YOUR_PASSWORD +``` + +### 2. Props Configuration +Configure your database in `obp-api/src/main/resources/props/default.props`: +```properties +db.driver=org.postgresql.Driver +db.url=jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=f +``` + +### 3. Make entrypoint executable +Make sure your entrypoint script is executable: + +```bash +chmod +x development/docker/entrypoint.sh +``` + +--- + +## Manual Configuration (if needed) + +If you need to override the automatic transformation, you can set the environment variable manually: + +```yaml +# In docker-compose.yml +environment: + - OBP_DB_URL=jdbc:postgresql://your-custom-host:5432/your_db?user=user&password=pass +``` + +--- + +## Notes + +* The container uses `MAVEN_OPTS` to pass JVM `--add-opens` flags needed by Lift. +* Database connection automatically uses `host.docker.internal` in Docker environments. +* Both main database (`db.url`) and remotedata database (`remotedata.db.url`) are transformed. +* In production, consider using external database services instead of host.docker.internal. + +## Troubleshooting + +**Database Connection Issues:** +- Ensure PostgreSQL is running on the host machine +- Check database credentials in your props file +- Verify firewall allows connections to PostgreSQL port 5432 +- Check startup logs: `docker-compose logs obp-api` + +**Permission Issues:** +- Make sure entrypoint.sh is executable: `chmod +x entrypoint.sh` + +**Configuration Issues:** +- Startup logs will show the detected and transformed database URLs +- Verify your `default.props` file has the correct `db.url` setting + +--- + +That's it β€” now you can run from the `development/docker` directory: + +```bash +cd development/docker +docker-compose up --build +``` + +Your database configuration will automatically work in both local development and Docker! πŸŽ‰ \ No newline at end of file diff --git a/development/docker/docker-compose.override.yml b/development/docker/docker-compose.override.yml new file mode 100644 index 000000000..5c2291bf3 --- /dev/null +++ b/development/docker/docker-compose.override.yml @@ -0,0 +1,7 @@ +version: "3.8" + +services: + obp-api: + volumes: + - ../../obp-api:/app/obp-api + - ../../obp-commons:/app/obp-commons diff --git a/development/docker/docker-compose.yml b/development/docker/docker-compose.yml new file mode 100644 index 000000000..d30058faf --- /dev/null +++ b/development/docker/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.8" + +services: + obp-api: + build: + context: ../.. + dockerfile: development/docker/Dockerfile + ports: + - "8080:8080" + extra_hosts: + # Enable host.docker.internal to work on all platforms + # This allows Docker to connect to services running on the host machine + - "host.docker.internal:host-gateway" \ No newline at end of file diff --git a/development/docker/entrypoint.sh b/development/docker/entrypoint.sh new file mode 100755 index 000000000..e84696429 --- /dev/null +++ b/development/docker/entrypoint.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -e + +# Function to extract database URL from props file and transform for Docker +setup_docker_db_url() { + local props_file="obp-api/src/main/resources/props/default.props" + + if [ -f "$props_file" ]; then + # Extract db.url from props file (handle commented and uncommented lines) + local db_url=$(grep -E "^[[:space:]]*db\.url=" "$props_file" | head -1 | sed 's/^[[:space:]]*db\.url=//') + + if [ -n "$db_url" ]; then + # Transform localhost to host.docker.internal for Docker environment + local docker_db_url=$(echo "$db_url" | sed 's/localhost/host.docker.internal/g') + + echo "Found database URL in props: $db_url" + echo "Transformed for Docker: $docker_db_url" + + # Set the environment variable that OBP-API will use + export OBP_DB_URL="$docker_db_url" + else + echo "Warning: No db.url found in $props_file" + echo "Using default PostgreSQL configuration for Docker" + export OBP_DB_URL="jdbc:postgresql://host.docker.internal:5432/obp_mapped?user=obp&password=f" + fi + else + echo "Warning: Props file not found at $props_file" + echo "Using default PostgreSQL configuration for Docker" + export OBP_DB_URL="jdbc:postgresql://host.docker.internal:5432/obp_mapped?user=obp&password=f" + fi +} + +# Function to extract remotedata database URL and transform for Docker +setup_docker_remotedata_db_url() { + local props_file="obp-api/src/main/resources/props/default.props" + + if [ -f "$props_file" ]; then + # Extract remotedata.db.url from props file + local remotedata_db_url=$(grep -E "^[[:space:]]*remotedata\.db\.url=" "$props_file" | head -1 | sed 's/^[[:space:]]*remotedata\.db\.url=//') + + if [ -n "$remotedata_db_url" ]; then + # Transform localhost to host.docker.internal for Docker environment + local docker_remotedata_db_url=$(echo "$remotedata_db_url" | sed 's/localhost/host.docker.internal/g') + + echo "Found remotedata database URL in props: $remotedata_db_url" + echo "Transformed for Docker: $docker_remotedata_db_url" + + # Set the environment variable that OBP-API will use + export OBP_REMOTEDATA_DB_URL="$docker_remotedata_db_url" + fi + fi +} + +echo "=== OBP-API Docker Startup ===" +echo "Setting up database configuration for Docker environment..." + +# Setup main database URL +setup_docker_db_url + +# Setup remotedata database URL if exists +setup_docker_remotedata_db_url + +echo "Database configuration complete." +echo "Starting OBP-API with Maven..." + +# Set Maven options for Java 17+ compatibility +export MAVEN_OPTS="-Xss128m \ + --add-opens=java.base/java.util.jar=ALL-UNNAMED \ + --add-opens=java.base/java.lang=ALL-UNNAMED \ + --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" + +# Start the application +exec mvn jetty:run -pl obp-api \ No newline at end of file diff --git a/development/docker/test-setup.sh b/development/docker/test-setup.sh new file mode 100755 index 000000000..8d158dca9 --- /dev/null +++ b/development/docker/test-setup.sh @@ -0,0 +1,180 @@ +#!/bin/bash + +# Test script to verify Docker setup after moving from ./docker to ./development/docker +set -e + +echo "=== OBP-API Docker Setup Test ===" +echo "Testing the moved Docker configuration..." +echo + +# Check if we're in the right directory +if [[ ! -f "docker-compose.yml" ]]; then + echo "❌ Error: docker-compose.yml not found in current directory" + echo " Please run this script from the development/docker directory" + exit 1 +fi + +echo "βœ… Found docker-compose.yml" + +# Check if entrypoint.sh exists and is executable +if [[ ! -f "entrypoint.sh" ]]; then + echo "❌ Error: entrypoint.sh not found" + exit 1 +fi + +if [[ ! -x "entrypoint.sh" ]]; then + echo "❌ Error: entrypoint.sh is not executable" + echo " Run: chmod +x entrypoint.sh" + exit 1 +fi + +echo "βœ… entrypoint.sh exists and is executable" + +# Test docker-compose config validation +echo "πŸ” Validating docker-compose configuration..." +if docker-compose config > /dev/null 2>&1; then + echo "βœ… Docker-compose configuration is valid" +else + echo "❌ Error: Docker-compose configuration is invalid" + echo " Running docker-compose config for details:" + docker-compose config + exit 1 +fi + +# Check if required source directories exist +echo "πŸ” Checking source directories..." +if [[ -d "../../obp-api" ]]; then + echo "βœ… Found ../../obp-api directory" +else + echo "❌ Error: ../../obp-api directory not found" + exit 1 +fi + +if [[ -d "../../obp-commons" ]]; then + echo "βœ… Found ../../obp-commons directory" +else + echo "❌ Error: ../../obp-commons directory not found" + exit 1 +fi + +# Check if main project files exist +echo "πŸ” Checking main project files..." +if [[ -f "../../pom.xml" ]]; then + echo "βœ… Found ../../pom.xml" +else + echo "❌ Error: ../../pom.xml not found" + exit 1 +fi + +# Test Docker build (use cache for faster testing) +echo "πŸ” Testing Docker build..." +if docker-compose build > /tmp/docker-build.log 2>&1; then + echo "βœ… Docker build completed successfully" +else + echo "❌ Error: Docker build failed" + echo " Check the build log:" + tail -20 /tmp/docker-build.log + exit 1 +fi + +# Test that the container can start and the entrypoint is accessible +echo "πŸ” Testing container startup and entrypoint..." +if docker-compose run --rm -T obp-api ls -la /app/entrypoint.sh > /dev/null 2>&1; then + echo "βœ… Container starts correctly and entrypoint is accessible" +else + echo "❌ Error: Container startup test failed" + exit 1 +fi + +# Test volume mounts work +echo "πŸ” Testing volume mounts..." +if docker-compose run --rm -T obp-api ls -la /app/obp-api/pom.xml > /dev/null 2>&1; then + echo "βœ… obp-api volume mount works" +else + echo "❌ Error: obp-api volume mount failed" + exit 1 +fi + +if docker-compose run --rm -T obp-api ls -la /app/obp-commons/pom.xml > /dev/null 2>&1; then + echo "βœ… obp-commons volume mount works" +else + echo "❌ Error: obp-commons volume mount failed" + exit 1 +fi + +# Test database connectivity and application startup +echo "πŸ” Testing database connectivity and application startup..." +echo " Starting containers..." +docker-compose up -d > /dev/null 2>&1 + +# Wait for application to start (with timeout) +echo " Waiting for application to start (this may take a few minutes)..." +timeout=300 # 5 minutes timeout +elapsed=0 +interval=10 + +while [ $elapsed -lt $timeout ]; do + if curl -s -f http://localhost:8080 > /dev/null 2>&1; then + echo "βœ… Application started and responding on port 8080" + app_started=true + break + fi + + # Check if container is still running + if ! docker-compose ps -q obp-api | xargs docker inspect -f '{{.State.Running}}' 2>/dev/null | grep -q true; then + echo "❌ Error: Container stopped unexpectedly" + echo " Check logs with: docker-compose logs obp-api" + docker-compose down > /dev/null 2>&1 + exit 1 + fi + + sleep $interval + elapsed=$((elapsed + interval)) + echo " Still waiting... (${elapsed}s elapsed)" +done + +if [ "$app_started" != "true" ]; then + echo "❌ Error: Application did not start within ${timeout} seconds" + echo " This might be normal for first run (downloading dependencies)" + echo " Check logs with: docker-compose logs obp-api" + echo " You can continue with manual testing using docker-compose up" +else + echo "βœ… Database connectivity and application startup successful" +fi + +# Clean up test containers +docker-compose down > /dev/null 2>&1 + +echo +echo "πŸŽ‰ All tests passed! Docker setup is working correctly." +echo +echo "Usage instructions:" +echo " 1. Navigate to development/docker directory:" +echo " cd development/docker" +echo +echo " 2. Start the service:" +echo " docker-compose up" +echo +echo " 3. For development with live reload:" +echo " docker-compose up --build" +echo +echo " 4. Access the API at:" +echo " http://localhost:8080" +echo +echo " 5. Stop the service:" +echo " docker-compose down" +echo +echo "Database Configuration:" +echo " The setup uses: jdbc:postgresql://host.docker.internal:5432/obp_mapped" +echo " Username: obp" +echo " Password: f" +echo " Make sure PostgreSQL is running on your host machine" +echo + +echo "βœ… Setup verification complete!" +echo +echo "Next Steps:" +echo " - Ensure PostgreSQL is running with the configured database" +echo " - Run 'docker-compose up' to start the application" +echo " - First startup may take several minutes downloading dependencies" +echo " - Check logs with 'docker-compose logs -f obp-api' if needed" \ No newline at end of file diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index 1c46f09e0..000000000 --- a/docker/README.md +++ /dev/null @@ -1,96 +0,0 @@ -## OBP API – Docker & Docker Compose Setup - -This project uses Docker and Docker Compose to run the **OBP API** service with Maven and Jetty. - -- Java 17 with reflection workaround -- Connects to your local Postgres using `host.docker.internal` -- Supports separate dev & prod setups - ---- - -## How to use - -> **Make sure you have Docker and Docker Compose installed.** - -### Set up the database connection - -Edit your `default.properties` (or similar config file): - -```properties -db.url=jdbc:postgresql://host.docker.internal:5432/YOUR_DB_NAME?user=YOUR_DB_USER&password=YOUR_DB_PASSWORD -```` - -> Use `host.docker.internal` so the container can reach your local database. - ---- - -### Build & run (production mode) - -Build the Docker image and run the container: - -```bash -docker-compose up --build -``` - -The service will be available at [http://localhost:8080](http://localhost:8080). - ---- - -## Development tips - -For live code updates without rebuilding: - -* Use the provided `docker-compose.override.yml` which mounts only: - - ```yaml - volumes: - - ../obp-api:/app/obp-api - - ../obp-commons:/app/obp-commons - ``` -* This keeps other built files (like `entrypoint.sh`) intact. -* Avoid mounting the full `../:/app` because it overwrites the built image. - ---- - -## Useful commands - -Rebuild the image and restart: - -```bash -docker-compose up --build -``` - -Stop the container: - -```bash -docker-compose down -``` - ---- - -## Before first run - -Make sure your entrypoint script is executable: - -```bash -chmod +x docker/entrypoint.sh -``` - ---- - -## Notes - -* The container uses `MAVEN_OPTS` to pass JVM `--add-opens` flags needed by Lift. -* In production, avoid volume mounts for better performance and consistency. - ---- - -That’s it β€” now you can run: - -```bash -docker-compose up --build -``` - -and start coding! - -``` \ No newline at end of file diff --git a/docker/docker-compose.override.yml b/docker/docker-compose.override.yml deleted file mode 100644 index 80e973a2c..000000000 --- a/docker/docker-compose.override.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: "3.8" - -services: - obp-api: - volumes: - - ../obp-api:/app/obp-api - - ../obp-commons:/app/obp-commons diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index ca4eda42a..000000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: "3.8" - -services: - obp-api: - build: - context: .. - dockerfile: docker/Dockerfile - ports: - - "8080:8080" - extra_hosts: - # Connect to local Postgres on the host - # In your config file: - # db.url=jdbc:postgresql://host.docker.internal:5432/YOUR_DB?user=YOUR_DB_USER&password=YOUR_DB_PASSWORD - - "host.docker.internal:host-gateway" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100644 index b35048478..000000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -export MAVEN_OPTS="-Xss128m \ - --add-opens=java.base/java.util.jar=ALL-UNNAMED \ - --add-opens=java.base/java.lang=ALL-UNNAMED \ - --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" - -exec mvn jetty:run -pl obp-api From 40e2b339eb55b9af24025335b6b93454917fd986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 25 Sep 2025 12:48:37 +0200 Subject: [PATCH 8/9] feature/Prevent warning: Make sure using this hardcoded IP address is safe here. --- .../code/api/berlin/group/signing/PSD2RequestSigner.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index ae00eb840..3a10585a6 100644 --- 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 @@ -41,7 +41,7 @@ class PSD2RequestSigner( * @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: "192.168.1.42") + * @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 @@ -50,7 +50,7 @@ class PSD2RequestSigner( requestBody: String, psuDeviceId: String = "device-1234567890", psuDeviceName: String = "Kalina-PC", - psuIpAddress: String = "192.168.1.42", + 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] = { From 87fc03144bc167a0f0f6606f7e1ce4073cd33f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 25 Sep 2025 13:50:57 +0200 Subject: [PATCH 9/9] Revert "feature/Enhance Docker development workflow" This reverts commit 55ee02b7b7a6453e681bf5b86631ee8c5b11de73. --- README.md | 11 -- development/docker/README.md | 180 ------------------ .../docker/docker-compose.override.yml | 7 - development/docker/docker-compose.yml | 13 -- development/docker/entrypoint.sh | 73 ------- development/docker/test-setup.sh | 180 ------------------ {development/docker => docker}/Dockerfile | 2 +- docker/README.md | 96 ++++++++++ docker/docker-compose.override.yml | 7 + docker/docker-compose.yml | 14 ++ docker/entrypoint.sh | 9 + 11 files changed, 127 insertions(+), 465 deletions(-) delete mode 100644 development/docker/README.md delete mode 100644 development/docker/docker-compose.override.yml delete mode 100644 development/docker/docker-compose.yml delete mode 100755 development/docker/entrypoint.sh delete mode 100755 development/docker/test-setup.sh rename {development/docker => docker}/Dockerfile (86%) create mode 100644 docker/README.md create mode 100644 docker/docker-compose.override.yml create mode 100644 docker/docker-compose.yml create mode 100644 docker/entrypoint.sh diff --git a/README.md b/README.md index 97b747c67..54882e544 100644 --- a/README.md +++ b/README.md @@ -192,17 +192,6 @@ Props values can be set as environment variables. Props need to be prefixed with `openid_connect.enabled=true` becomes `OBP_OPENID_CONNECT_ENABLED=true`. -### Development Docker Setup - -For local development with Docker Compose, see the Docker setup in `development/docker/`: - -```bash -cd development/docker -docker-compose up --build -``` - -See `development/docker/README.md` for detailed instructions and configuration options. - ## Databases The default database for testing etc is H2. PostgreSQL is used for the sandboxes (user accounts, metadata, transaction cache). The list of databases fully tested is: PostgreSQL, MS SQL and H2. diff --git a/development/docker/README.md b/development/docker/README.md deleted file mode 100644 index 918b10767..000000000 --- a/development/docker/README.md +++ /dev/null @@ -1,180 +0,0 @@ -## OBP API – Docker & Docker Compose Setup - -This project uses Docker and Docker Compose to run the **OBP API** service with Maven and Jetty. - -- Java 17 with reflection workaround -- **Automatic database URL transformation** for seamless Docker development -- Connects to your local Postgres using `host.docker.internal` -- Supports separate dev & prod setups - ---- - -## How to use - -> **Make sure you have Docker and Docker Compose installed.** -> **Navigate to the `development/docker` directory before running commands.** - -```bash -cd development/docker -``` - -### Automatic Database Configuration πŸš€ - -The Docker setup now **automatically transforms your database configuration** for Docker environments! - -**What this means:** -- Set your database URL in props file with `localhost` (for local development) -- Docker automatically transforms `localhost` to `host.docker.internal` -- **No need to change props files when switching between local and Docker!** - -**Example:** -```properties -# In your obp-api/src/main/resources/props/default.props -db.url=jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=f -``` - -When you run with Docker, this automatically becomes: -``` -jdbc:postgresql://host.docker.internal:5432/obp_mapped?user=obp&password=f -``` - -**Password changes are automatically reflected!** -If you change your password in the props file, Docker will use the new password automatically. - ---- - -### Build & run - -Build the Docker image and run the container: - -```bash -docker-compose up --build -``` - -The service will be available at [http://localhost:8080](http://localhost:8080). - ---- - -## Development tips - -For live code updates without rebuilding: - -* Use the provided `docker-compose.override.yml` which mounts only: - - ```yaml - volumes: - - ../../obp-api:/app/obp-api - - ../../obp-commons:/app/obp-commons - ``` -* This keeps other built files (like `entrypoint.sh`) intact. -* Avoid mounting the full `../../:/app` because it overwrites the built image. - ---- - -## How the automatic transformation works - -1. **Local Development**: Use your props file with `localhost`: - ```properties - db.url=jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=newpass - ``` - -2. **Docker Startup**: The `entrypoint.sh` script: - - Reads your current `db.url` from `default.props` - - Automatically transforms `localhost` β†’ `host.docker.internal` - - Sets the `OBP_DB_URL` environment variable - - Starts the application - -3. **Result**: OBP-API uses the transformed URL in Docker, original URL locally. - ---- - -## Useful commands - -Rebuild the image and restart: - -```bash -docker-compose up --build -``` - -Stop the container: - -```bash -docker-compose down -``` - -View startup logs to see the database transformation: - -```bash -docker-compose logs obp-api -``` - ---- - -## Before first run - -### 1. Database Setup -Ensure PostgreSQL is running on your host machine with a database accessible via: -``` -jdbc:postgresql://localhost:5432/YOUR_DB_NAME?user=YOUR_USER&password=YOUR_PASSWORD -``` - -### 2. Props Configuration -Configure your database in `obp-api/src/main/resources/props/default.props`: -```properties -db.driver=org.postgresql.Driver -db.url=jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=f -``` - -### 3. Make entrypoint executable -Make sure your entrypoint script is executable: - -```bash -chmod +x development/docker/entrypoint.sh -``` - ---- - -## Manual Configuration (if needed) - -If you need to override the automatic transformation, you can set the environment variable manually: - -```yaml -# In docker-compose.yml -environment: - - OBP_DB_URL=jdbc:postgresql://your-custom-host:5432/your_db?user=user&password=pass -``` - ---- - -## Notes - -* The container uses `MAVEN_OPTS` to pass JVM `--add-opens` flags needed by Lift. -* Database connection automatically uses `host.docker.internal` in Docker environments. -* Both main database (`db.url`) and remotedata database (`remotedata.db.url`) are transformed. -* In production, consider using external database services instead of host.docker.internal. - -## Troubleshooting - -**Database Connection Issues:** -- Ensure PostgreSQL is running on the host machine -- Check database credentials in your props file -- Verify firewall allows connections to PostgreSQL port 5432 -- Check startup logs: `docker-compose logs obp-api` - -**Permission Issues:** -- Make sure entrypoint.sh is executable: `chmod +x entrypoint.sh` - -**Configuration Issues:** -- Startup logs will show the detected and transformed database URLs -- Verify your `default.props` file has the correct `db.url` setting - ---- - -That's it β€” now you can run from the `development/docker` directory: - -```bash -cd development/docker -docker-compose up --build -``` - -Your database configuration will automatically work in both local development and Docker! πŸŽ‰ \ No newline at end of file diff --git a/development/docker/docker-compose.override.yml b/development/docker/docker-compose.override.yml deleted file mode 100644 index 5c2291bf3..000000000 --- a/development/docker/docker-compose.override.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: "3.8" - -services: - obp-api: - volumes: - - ../../obp-api:/app/obp-api - - ../../obp-commons:/app/obp-commons diff --git a/development/docker/docker-compose.yml b/development/docker/docker-compose.yml deleted file mode 100644 index d30058faf..000000000 --- a/development/docker/docker-compose.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: "3.8" - -services: - obp-api: - build: - context: ../.. - dockerfile: development/docker/Dockerfile - ports: - - "8080:8080" - extra_hosts: - # Enable host.docker.internal to work on all platforms - # This allows Docker to connect to services running on the host machine - - "host.docker.internal:host-gateway" \ No newline at end of file diff --git a/development/docker/entrypoint.sh b/development/docker/entrypoint.sh deleted file mode 100755 index e84696429..000000000 --- a/development/docker/entrypoint.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -set -e - -# Function to extract database URL from props file and transform for Docker -setup_docker_db_url() { - local props_file="obp-api/src/main/resources/props/default.props" - - if [ -f "$props_file" ]; then - # Extract db.url from props file (handle commented and uncommented lines) - local db_url=$(grep -E "^[[:space:]]*db\.url=" "$props_file" | head -1 | sed 's/^[[:space:]]*db\.url=//') - - if [ -n "$db_url" ]; then - # Transform localhost to host.docker.internal for Docker environment - local docker_db_url=$(echo "$db_url" | sed 's/localhost/host.docker.internal/g') - - echo "Found database URL in props: $db_url" - echo "Transformed for Docker: $docker_db_url" - - # Set the environment variable that OBP-API will use - export OBP_DB_URL="$docker_db_url" - else - echo "Warning: No db.url found in $props_file" - echo "Using default PostgreSQL configuration for Docker" - export OBP_DB_URL="jdbc:postgresql://host.docker.internal:5432/obp_mapped?user=obp&password=f" - fi - else - echo "Warning: Props file not found at $props_file" - echo "Using default PostgreSQL configuration for Docker" - export OBP_DB_URL="jdbc:postgresql://host.docker.internal:5432/obp_mapped?user=obp&password=f" - fi -} - -# Function to extract remotedata database URL and transform for Docker -setup_docker_remotedata_db_url() { - local props_file="obp-api/src/main/resources/props/default.props" - - if [ -f "$props_file" ]; then - # Extract remotedata.db.url from props file - local remotedata_db_url=$(grep -E "^[[:space:]]*remotedata\.db\.url=" "$props_file" | head -1 | sed 's/^[[:space:]]*remotedata\.db\.url=//') - - if [ -n "$remotedata_db_url" ]; then - # Transform localhost to host.docker.internal for Docker environment - local docker_remotedata_db_url=$(echo "$remotedata_db_url" | sed 's/localhost/host.docker.internal/g') - - echo "Found remotedata database URL in props: $remotedata_db_url" - echo "Transformed for Docker: $docker_remotedata_db_url" - - # Set the environment variable that OBP-API will use - export OBP_REMOTEDATA_DB_URL="$docker_remotedata_db_url" - fi - fi -} - -echo "=== OBP-API Docker Startup ===" -echo "Setting up database configuration for Docker environment..." - -# Setup main database URL -setup_docker_db_url - -# Setup remotedata database URL if exists -setup_docker_remotedata_db_url - -echo "Database configuration complete." -echo "Starting OBP-API with Maven..." - -# Set Maven options for Java 17+ compatibility -export MAVEN_OPTS="-Xss128m \ - --add-opens=java.base/java.util.jar=ALL-UNNAMED \ - --add-opens=java.base/java.lang=ALL-UNNAMED \ - --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" - -# Start the application -exec mvn jetty:run -pl obp-api \ No newline at end of file diff --git a/development/docker/test-setup.sh b/development/docker/test-setup.sh deleted file mode 100755 index 8d158dca9..000000000 --- a/development/docker/test-setup.sh +++ /dev/null @@ -1,180 +0,0 @@ -#!/bin/bash - -# Test script to verify Docker setup after moving from ./docker to ./development/docker -set -e - -echo "=== OBP-API Docker Setup Test ===" -echo "Testing the moved Docker configuration..." -echo - -# Check if we're in the right directory -if [[ ! -f "docker-compose.yml" ]]; then - echo "❌ Error: docker-compose.yml not found in current directory" - echo " Please run this script from the development/docker directory" - exit 1 -fi - -echo "βœ… Found docker-compose.yml" - -# Check if entrypoint.sh exists and is executable -if [[ ! -f "entrypoint.sh" ]]; then - echo "❌ Error: entrypoint.sh not found" - exit 1 -fi - -if [[ ! -x "entrypoint.sh" ]]; then - echo "❌ Error: entrypoint.sh is not executable" - echo " Run: chmod +x entrypoint.sh" - exit 1 -fi - -echo "βœ… entrypoint.sh exists and is executable" - -# Test docker-compose config validation -echo "πŸ” Validating docker-compose configuration..." -if docker-compose config > /dev/null 2>&1; then - echo "βœ… Docker-compose configuration is valid" -else - echo "❌ Error: Docker-compose configuration is invalid" - echo " Running docker-compose config for details:" - docker-compose config - exit 1 -fi - -# Check if required source directories exist -echo "πŸ” Checking source directories..." -if [[ -d "../../obp-api" ]]; then - echo "βœ… Found ../../obp-api directory" -else - echo "❌ Error: ../../obp-api directory not found" - exit 1 -fi - -if [[ -d "../../obp-commons" ]]; then - echo "βœ… Found ../../obp-commons directory" -else - echo "❌ Error: ../../obp-commons directory not found" - exit 1 -fi - -# Check if main project files exist -echo "πŸ” Checking main project files..." -if [[ -f "../../pom.xml" ]]; then - echo "βœ… Found ../../pom.xml" -else - echo "❌ Error: ../../pom.xml not found" - exit 1 -fi - -# Test Docker build (use cache for faster testing) -echo "πŸ” Testing Docker build..." -if docker-compose build > /tmp/docker-build.log 2>&1; then - echo "βœ… Docker build completed successfully" -else - echo "❌ Error: Docker build failed" - echo " Check the build log:" - tail -20 /tmp/docker-build.log - exit 1 -fi - -# Test that the container can start and the entrypoint is accessible -echo "πŸ” Testing container startup and entrypoint..." -if docker-compose run --rm -T obp-api ls -la /app/entrypoint.sh > /dev/null 2>&1; then - echo "βœ… Container starts correctly and entrypoint is accessible" -else - echo "❌ Error: Container startup test failed" - exit 1 -fi - -# Test volume mounts work -echo "πŸ” Testing volume mounts..." -if docker-compose run --rm -T obp-api ls -la /app/obp-api/pom.xml > /dev/null 2>&1; then - echo "βœ… obp-api volume mount works" -else - echo "❌ Error: obp-api volume mount failed" - exit 1 -fi - -if docker-compose run --rm -T obp-api ls -la /app/obp-commons/pom.xml > /dev/null 2>&1; then - echo "βœ… obp-commons volume mount works" -else - echo "❌ Error: obp-commons volume mount failed" - exit 1 -fi - -# Test database connectivity and application startup -echo "πŸ” Testing database connectivity and application startup..." -echo " Starting containers..." -docker-compose up -d > /dev/null 2>&1 - -# Wait for application to start (with timeout) -echo " Waiting for application to start (this may take a few minutes)..." -timeout=300 # 5 minutes timeout -elapsed=0 -interval=10 - -while [ $elapsed -lt $timeout ]; do - if curl -s -f http://localhost:8080 > /dev/null 2>&1; then - echo "βœ… Application started and responding on port 8080" - app_started=true - break - fi - - # Check if container is still running - if ! docker-compose ps -q obp-api | xargs docker inspect -f '{{.State.Running}}' 2>/dev/null | grep -q true; then - echo "❌ Error: Container stopped unexpectedly" - echo " Check logs with: docker-compose logs obp-api" - docker-compose down > /dev/null 2>&1 - exit 1 - fi - - sleep $interval - elapsed=$((elapsed + interval)) - echo " Still waiting... (${elapsed}s elapsed)" -done - -if [ "$app_started" != "true" ]; then - echo "❌ Error: Application did not start within ${timeout} seconds" - echo " This might be normal for first run (downloading dependencies)" - echo " Check logs with: docker-compose logs obp-api" - echo " You can continue with manual testing using docker-compose up" -else - echo "βœ… Database connectivity and application startup successful" -fi - -# Clean up test containers -docker-compose down > /dev/null 2>&1 - -echo -echo "πŸŽ‰ All tests passed! Docker setup is working correctly." -echo -echo "Usage instructions:" -echo " 1. Navigate to development/docker directory:" -echo " cd development/docker" -echo -echo " 2. Start the service:" -echo " docker-compose up" -echo -echo " 3. For development with live reload:" -echo " docker-compose up --build" -echo -echo " 4. Access the API at:" -echo " http://localhost:8080" -echo -echo " 5. Stop the service:" -echo " docker-compose down" -echo -echo "Database Configuration:" -echo " The setup uses: jdbc:postgresql://host.docker.internal:5432/obp_mapped" -echo " Username: obp" -echo " Password: f" -echo " Make sure PostgreSQL is running on your host machine" -echo - -echo "βœ… Setup verification complete!" -echo -echo "Next Steps:" -echo " - Ensure PostgreSQL is running with the configured database" -echo " - Run 'docker-compose up' to start the application" -echo " - First startup may take several minutes downloading dependencies" -echo " - Check logs with 'docker-compose logs -f obp-api' if needed" \ No newline at end of file diff --git a/development/docker/Dockerfile b/docker/Dockerfile similarity index 86% rename from development/docker/Dockerfile rename to docker/Dockerfile index 87ab04cab..53a999d1d 100644 --- a/development/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ EXPOSE 8080 RUN mvn install -pl .,obp-commons -am -DskipTests # Copy entrypoint script that runs mvn with needed JVM flags -COPY development/docker/entrypoint.sh /app/entrypoint.sh +COPY docker/entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh # Use script as entrypoint diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..1c46f09e0 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,96 @@ +## OBP API – Docker & Docker Compose Setup + +This project uses Docker and Docker Compose to run the **OBP API** service with Maven and Jetty. + +- Java 17 with reflection workaround +- Connects to your local Postgres using `host.docker.internal` +- Supports separate dev & prod setups + +--- + +## How to use + +> **Make sure you have Docker and Docker Compose installed.** + +### Set up the database connection + +Edit your `default.properties` (or similar config file): + +```properties +db.url=jdbc:postgresql://host.docker.internal:5432/YOUR_DB_NAME?user=YOUR_DB_USER&password=YOUR_DB_PASSWORD +```` + +> Use `host.docker.internal` so the container can reach your local database. + +--- + +### Build & run (production mode) + +Build the Docker image and run the container: + +```bash +docker-compose up --build +``` + +The service will be available at [http://localhost:8080](http://localhost:8080). + +--- + +## Development tips + +For live code updates without rebuilding: + +* Use the provided `docker-compose.override.yml` which mounts only: + + ```yaml + volumes: + - ../obp-api:/app/obp-api + - ../obp-commons:/app/obp-commons + ``` +* This keeps other built files (like `entrypoint.sh`) intact. +* Avoid mounting the full `../:/app` because it overwrites the built image. + +--- + +## Useful commands + +Rebuild the image and restart: + +```bash +docker-compose up --build +``` + +Stop the container: + +```bash +docker-compose down +``` + +--- + +## Before first run + +Make sure your entrypoint script is executable: + +```bash +chmod +x docker/entrypoint.sh +``` + +--- + +## Notes + +* The container uses `MAVEN_OPTS` to pass JVM `--add-opens` flags needed by Lift. +* In production, avoid volume mounts for better performance and consistency. + +--- + +That’s it β€” now you can run: + +```bash +docker-compose up --build +``` + +and start coding! + +``` \ No newline at end of file diff --git a/docker/docker-compose.override.yml b/docker/docker-compose.override.yml new file mode 100644 index 000000000..80e973a2c --- /dev/null +++ b/docker/docker-compose.override.yml @@ -0,0 +1,7 @@ +version: "3.8" + +services: + obp-api: + volumes: + - ../obp-api:/app/obp-api + - ../obp-commons:/app/obp-commons diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 000000000..ca4eda42a --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3.8" + +services: + obp-api: + build: + context: .. + dockerfile: docker/Dockerfile + ports: + - "8080:8080" + extra_hosts: + # Connect to local Postgres on the host + # In your config file: + # db.url=jdbc:postgresql://host.docker.internal:5432/YOUR_DB?user=YOUR_DB_USER&password=YOUR_DB_PASSWORD + - "host.docker.internal:host-gateway" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 000000000..b35048478 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +export MAVEN_OPTS="-Xss128m \ + --add-opens=java.base/java.util.jar=ALL-UNNAMED \ + --add-opens=java.base/java.lang=ALL-UNNAMED \ + --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" + +exec mvn jetty:run -pl obp-api