Skip to content

Commit 626505f

Browse files
committed
Merge branch 'develop' of github.com:OpenBankProject/OBP-API into develop
2 parents fc0672b + 52754db commit 626505f

25 files changed

+658
-158
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
.settings
99
.metals
1010
.vscode
11+
*.code-workspace
1112
.zed
13+
.cursor
1214
.classpath
1315
.project
1416
.cache
@@ -35,5 +37,4 @@ marketing_diagram_generation/outputs/*
3537
.bsp
3638
.specstory
3739
project/project
38-
coursier
39-
*.code-workspace
40+
coursier

.sdkmanrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Enable auto-env through the sdkman_auto_env config
2+
# Add key=value pairs of SDKs to use below
3+
java=11.0.28-tem

README.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ This project is dual licensed under the AGPL V3 (see NOTICE) and commercial lice
4343

4444
## Setup
4545

46+
### Installing JDK
47+
#### With sdkman
48+
49+
A good way to manage JDK versions and install the correct version for OBP is [sdkman](https://sdkman.io/). If you have this installed then you can install the correct JDK easily using:
50+
```
51+
sdk env install
52+
```
53+
54+
#### Manually
55+
56+
- OracleJDK: 1.8, 13
57+
- OpenJdk: 11
58+
59+
OpenJDK 11 is available for download here: [https://jdk.java.net/archive/](https://jdk.java.net/archive/).
60+
4661
The project uses Maven 3 as its build tool.
4762

4863
To compile and run Jetty, install Maven 3, create your configuration in `obp-api/src/main/resources/props/default.props` and execute:
@@ -753,13 +768,6 @@ The same as `Frozen APIs`, if a related unit test fails, make sure whether the m
753768

754769
- A good book on Lift: "Lift in Action" by Timothy Perrett published by Manning.
755770

756-
## Supported JDK Versions
757-
758-
- OracleJDK: 1.8, 13
759-
- OpenJdk: 11
760-
761-
OpenJDK 11 is available for download here: [https://jdk.java.net/archive/](https://jdk.java.net/archive/).
762-
763771
## Endpoint Request and Response Example
764772

765773
```log

obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ object BgSpecValidation {
3636

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

58-
// Example usage
59-
def main(args: Array[String]): Unit = {
60-
val testDates = Seq(
61-
"2025-05-10", // More than 180 days ahead
62-
"9999-12-31", // Exceeds max allowed
63-
"2015-01-01", // In the past
64-
"invalid-date", // Invalid format
65-
LocalDate.now().plusDays(90).toString, // Valid (within 180 days)
66-
LocalDate.now().plusDays(180).toString, // Valid (exactly 180 days)
67-
LocalDate.now().plusDays(181).toString // More than 180 days
68-
)
69-
70-
testDates.foreach { date =>
71-
validateValidUntil(date) match {
72-
case Right(validDate) => println(s"Valid date: $validDate")
73-
case Left(error) => println(s"Error: $error")
74-
}
75-
}
76-
}
7758
}

obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
346346
Some(balances.filter(_.accountId.equals(x.accountId)).flatMap(balance => (List(CoreAccountBalanceJson(
347347
balanceAmount = AmountOfMoneyV13(x.currency, balance.balanceAmount.toString()),
348348
balanceType = balance.balanceType,
349-
lastChangeDateTime = balance.lastChangeDateTime.map(APIUtil.DateWithMsAndTimeZoneOffset.format(_))
349+
lastChangeDateTime = balance.lastChangeDateTime.map(APIUtil.DateWithMsFormat.format(_))
350350
)))))
351351
}else{
352352
None
@@ -432,7 +432,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
432432
Some(balances.filter(_.accountId.equals(bankAccount.accountId)).flatMap(balance => (List(CoreAccountBalanceJson(
433433
balanceAmount = AmountOfMoneyV13(bankAccount.currency, balance.balanceAmount.toString()),
434434
balanceType = balance.balanceType,
435-
lastChangeDateTime = balance.lastChangeDateTime.map(APIUtil.DateWithMsAndTimeZoneOffset.format(_))
435+
lastChangeDateTime = balance.lastChangeDateTime.map(APIUtil.DateWithMsFormat.format(_))
436436
)))))
437437
} else {
438438
None
@@ -477,7 +477,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
477477
`balances` = accountBalances.map(accountBalance => AccountBalance(
478478
balanceAmount = AmountOfMoneyV13(bankAccount.currency, accountBalance.balanceAmount.toString()),
479479
balanceType = accountBalance.balanceType,
480-
lastChangeDateTime = accountBalance.lastChangeDateTime.map(APIUtil.DateWithMsAndTimeZoneOffset.format(_)),
480+
lastChangeDateTime = accountBalance.lastChangeDateTime.map(APIUtil.DateWithMsFormat.format(_)),
481481
referenceDate = accountBalance.referenceDate,
482482
)
483483
))
@@ -614,8 +614,8 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
614614
else
615615
transaction.amount.get.toString()
616616
),
617-
bookingDate = transaction.startDate.map(APIUtil.DateWithMsAndTimeZoneOffset.format(_)).getOrElse(""),
618-
valueDate = transaction.finishDate.map(APIUtil.DateWithMsAndTimeZoneOffset.format(_)).getOrElse(""),
617+
bookingDate = transaction.startDate.map(APIUtil.DateWithMsFormat.format(_)).getOrElse(""),
618+
valueDate = transaction.finishDate.map(APIUtil.DateWithMsFormat.format(_)).getOrElse(""),
619619
remittanceInformationUnstructured = transaction.description.getOrElse(""),
620620
bankTransactionCode ="",
621621
)

obp-api/src/main/scala/code/api/util/APIUtil.scala

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,11 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
130130
val DateWithMonthFormat = new SimpleDateFormat(DateWithMonth)
131131
val DateWithDayFormat = new SimpleDateFormat(DateWithDay)
132132
val DateWithSecondsFormat = new SimpleDateFormat(DateWithSeconds)
133-
val DateWithMsFormat = new SimpleDateFormat(DateWithMs)
133+
// If you need UTC Z format, please continue to use DateWithMsFormat. eg: 2025-01-01T01:01:01.000Z
134+
val DateWithMsFormat = new SimpleDateFormat(DateWithMs)
135+
// If you need a format with timezone offset (+0000), please use DateWithMsRollbackFormat, eg: 2025-01-01T01:01:01.000+0000
134136
val DateWithMsRollbackFormat = new SimpleDateFormat(DateWithMsAndTimeZoneOffset)
137+
135138
val rfc7231Date = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH)
136139

137140
val DateWithYearExampleString: String = "1100"
@@ -967,7 +970,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
967970
if(date == null)
968971
None
969972
else
970-
Some(APIUtil.DateWithMsAndTimeZoneOffset.format(date))
973+
Some(APIUtil.DateWithMsRollbackFormat.format(date))
971974

972975
def stringOrNull(text : String) =
973976
if(text == null || text.isEmpty)
@@ -3209,8 +3212,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
32093212

32103213
// COMMON POST AUTHENTICATION CODE GOES BELOW
32113214

3215+
// Check is it Consumer disabled
3216+
val consumerIsDisabled: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkConsumerIsDisabled(res)
32123217
// Check is it a user deleted or locked
3213-
val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(res)
3218+
val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(consumerIsDisabled)
32143219
// Check Rate Limiting
32153220
val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkRateLimiting(userIsLockedOrDeleted)
32163221
// User init actions
@@ -4012,17 +4017,22 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
40124017
val consumerName = cc.flatMap(_.consumer.map(_.name.get)).getOrElse("")
40134018
val certificate = getCertificateFromTppSignatureCertificate(requestHeaders)
40144019
for {
4015-
tpp <- BerlinGroupSigning.getTppByCertificate(certificate, cc)
4020+
tpps <- BerlinGroupSigning.getRegulatedEntityByCertificate(certificate, cc)
40164021
} yield {
4017-
if (tpp.nonEmpty) {
4018-
val hasRole = tpp.exists(_.services.contains(serviceProvider))
4019-
if (hasRole) {
4020-
Full(true)
4021-
} else {
4022-
Failure(X509ActionIsNotAllowed)
4023-
}
4024-
} else {
4025-
Failure("No valid Tpp")
4022+
tpps match {
4023+
case Nil =>
4024+
Failure(RegulatedEntityNotFoundByCertificate)
4025+
case single :: Nil =>
4026+
// Only one match, proceed to role check
4027+
if (single.services.contains(serviceProvider)) {
4028+
Full(true)
4029+
} else {
4030+
Failure(X509ActionIsNotAllowed)
4031+
}
4032+
case multiple =>
4033+
// Ambiguity detected: more than one TPP matches the certificate
4034+
val names = multiple.map(e => s"'${e.entityName}' (Code: ${e.entityCode})").mkString(", ")
4035+
Failure(s"$RegulatedEntityAmbiguityByCertificate: multiple TPPs found: $names")
40264036
}
40274037
}
40284038
case value if value.toUpperCase == "CERTIFICATE" => Future {

obp-api/src/main/scala/code/api/util/AfterApiAuth.scala

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import code.accountholders.AccountHolders
66
import code.api.Constant
77
import code.api.util.APIUtil.getPropsAsBoolValue
88
import code.api.util.ApiRole.{CanCreateAccount, CanCreateHistoricalTransactionAtBank}
9-
import code.api.util.ErrorMessages.{UserIsDeleted, UsernameHasBeenLocked}
9+
import code.api.util.ErrorMessages.{ConsumerIsDisabled, UserIsDeleted, UsernameHasBeenLocked}
1010
import code.api.util.RateLimitingJson.CallLimit
1111
import code.bankconnectors.{Connector, LocalMappedConnectorInternal}
1212
import code.entitlement.Entitlement
@@ -78,6 +78,18 @@ object AfterApiAuth extends MdcLoggable{
7878
}
7979
}
8080
}
81+
def checkConsumerIsDisabled(res: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = {
82+
for {
83+
(user: Box[User], cc) <- res
84+
} yield {
85+
cc.map(_.consumer) match {
86+
case Some(Full(consumer)) if !consumer.isActive.get => // There is a consumer. Check it.
87+
(Failure(ConsumerIsDisabled), cc) // The Consumer is DISABLED.
88+
case _ => // There is no Consumer. Just forward the result.
89+
(user, cc)
90+
}
91+
}
92+
}
8193

8294
/**
8395
* This block of code needs to update Call Context with Rate Limiting

obp-api/src/main/scala/code/api/util/ApiSession.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ case class CallContext(
3030
dauthResponseHeader: Option[String] = None,
3131
spelling: Option[String] = None,
3232
user: Box[User] = Empty,
33+
onBehalfOfUser: Option[User] = None,
3334
consenter: Box[User] = Empty,
3435
consumer: Box[Consumer] = Empty,
3536
ipAddress: String = "",

obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import code.consumer.Consumers
88
import code.model.Consumer
99
import code.util.Helper.MdcLoggable
1010
import com.openbankproject.commons.ExecutionContext.Implicits.global
11-
import com.openbankproject.commons.model.{RegulatedEntityTrait, User}
11+
import com.openbankproject.commons.model.{RegulatedEntityAttributeSimple, RegulatedEntityTrait, User}
1212
import net.liftweb.common.{Box, Empty, Failure, Full}
1313
import net.liftweb.http.provider.HTTPParam
1414
import net.liftweb.util.Helpers
@@ -111,29 +111,40 @@ object BerlinGroupSigning extends MdcLoggable {
111111
certificate
112112
}
113113

114-
def getTppByCertificate(certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = {
115-
// Use the regular expression to find the value of CN
116-
val extractedCN = cnPattern.findFirstMatchIn(certificate.getIssuerDN.getName) match {
117-
case Some(m) => m.group(1) // Extract the value of CN
118-
case None => "CN not found"
119-
}
120-
val issuerCommonName = extractedCN // Certificate.caCert
114+
def getRegulatedEntityByCertificate(certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = {
115+
val issuerCN = cnPattern.findFirstMatchIn(certificate.getIssuerDN.getName)
116+
.map(_.group(1).trim)
117+
.getOrElse("CN not found")
118+
121119
val serialNumber = certificate.getSerialNumber.toString
122-
val regulatedEntities: Future[List[RegulatedEntityTrait]] = for {
120+
121+
for {
123122
(entities, _) <- getRegulatedEntitiesNewStyle(callContext)
124123
} yield {
125-
logger.debug("Regulated Entities: " + entities)
126124
entities.filter { entity =>
127-
val hasSerialNumber = entity.attributes.exists(_.exists(a =>
128-
a.name == "CERTIFICATE_SERIAL_NUMBER" && a.value == serialNumber
129-
))
130-
val hasCaName = entity.attributes.exists(_.exists(a =>
131-
a.name == "CERTIFICATE_CA_NAME" && a.value == issuerCommonName
132-
))
133-
hasSerialNumber && hasCaName
125+
val attrs = entity.attributes.getOrElse(Nil)
126+
127+
// Extract serial number and CA name from attributes
128+
val serialOpt = attrs.collectFirst { case a if a.name.equalsIgnoreCase("CERTIFICATE_SERIAL_NUMBER") => a.value.trim }
129+
val caNameOpt = attrs.collectFirst { case a if a.name.equalsIgnoreCase("CERTIFICATE_CA_NAME") => a.value.trim }
130+
131+
val serialMatches = serialOpt.contains(serialNumber)
132+
val caNameMatches = caNameOpt.exists(_.equalsIgnoreCase(issuerCN))
133+
134+
val isMatch = serialMatches && caNameMatches
135+
136+
// Log everything for debugging
137+
val serialLog = serialOpt.getOrElse("N/A")
138+
val caNameLog = caNameOpt.getOrElse("N/A")
139+
val allAttrsLog = attrs.map(a => s"${a.name}='${a.value}'").mkString(", ")
140+
141+
if (isMatch)
142+
logger.debug(s"[MATCH] Entity '${entity.entityName}' (Code: ${entity.entityCode}) matches CN='$issuerCN', Serial='$serialNumber' " +
143+
s"(Attributes found: Serial='$serialLog', CA Name='$caNameLog', All Attributes: [$allAttrsLog])")
144+
145+
isMatch
134146
}
135147
}
136-
regulatedEntities
137148
}
138149

139150

@@ -280,7 +291,7 @@ object BerlinGroupSigning extends MdcLoggable {
280291
}
281292

282293
for {
283-
entities <- getTppByCertificate(certificate, forwardResult._2) // Find TPP via certificate
294+
entities <- getRegulatedEntityByCertificate(certificate, forwardResult._2) // Find Regulated Entity via certificate
284295
} yield {
285296
// Certificate can be changed but this value is permanent per Regulated entity
286297
val idno = entities.map(_.entityCode).headOption.getOrElse("")

obp-api/src/main/scala/code/api/util/ConsentUtil.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,10 @@ object Consent extends MdcLoggable {
431431

432432
def applyConsentRules(consent: ConsentJWT): Future[(Box[User], Option[CallContext])] = {
433433
val cc = callContext
434+
if(consent.createdByUserId.nonEmpty) {
435+
val onBehalfOfUser = Users.users.vend.getUserByUserId(consent.createdByUserId)
436+
cc.copy(onBehalfOfUser = onBehalfOfUser.toOption)
437+
}
434438
// 1. Get or Create a User
435439
getOrCreateUser(consent.sub, consent.iss, Some(consent.jti), None, None) map {
436440
case (Full(user), newUser) =>

0 commit comments

Comments
 (0)