mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
Merge remote-tracking branch 'Hongwei/develop' into feature/refactorViewPermission
# Conflicts: # obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala # obp-api/src/main/scala/code/model/View.scala
This commit is contained in:
commit
dc8e9e02e2
@ -365,6 +365,8 @@ BankMockKey=change_me
|
||||
#####################################################################################
|
||||
## Web interface configuration
|
||||
|
||||
## do not put sensitive information in any webui props, as these can be retrieved by a public endpoint.
|
||||
|
||||
## IMPLEMENTING BANK SPECIFIC BRANDING ON ONE OBP INSTANCE ########################
|
||||
# Note, you can specify bank specific branding by appending _FOR_BRAND_<BANK_ID> to the standard props names
|
||||
# e.g.
|
||||
|
||||
@ -39,6 +39,7 @@ import code.api.ResourceDocs1_4_0.ResourceDocs300.{ResourceDocs310, ResourceDocs
|
||||
import code.api.ResourceDocs1_4_0._
|
||||
import code.api._
|
||||
import code.api.attributedefinition.AttributeDefinition
|
||||
import code.api.berlin.group.ConstantsBG
|
||||
import code.api.cache.Redis
|
||||
import code.api.util.APIUtil.{enableVersionIfAllowed, errorJsonResponse, getPropsValue}
|
||||
import code.api.util.ApiRole.CanCreateEntitlementAtAnyBank
|
||||
@ -722,8 +723,14 @@ class Boot extends MdcLoggable {
|
||||
}
|
||||
|
||||
LiftRules.uriNotFound.prepend{
|
||||
case (r, _) if r.uri.contains(ConstantsBG.berlinGroupVersion1.urlPrefix) => NotFoundAsResponse(errorJsonResponse(
|
||||
s"${ErrorMessages.InvalidUri}Current Url is (${r.uri.toString}), Current Content-Type Header is (${r.headers.find(_._1.equals("Content-Type")).map(_._2).getOrElse("")})",
|
||||
405,
|
||||
Some(CallContextLight(url = r.uri))
|
||||
)
|
||||
)
|
||||
case (r, _) => NotFoundAsResponse(errorJsonResponse(
|
||||
s"${ErrorMessages.InvalidUri}Current Url is (${r.uri.toString}), Current Content-Type Header is (${r.headers.find(_._1.equals("Content-Type")).map(_._2).getOrElse("")})",
|
||||
s"${ErrorMessages.InvalidUri}Current Url is (${r.uri.toString}), Current Content-Type Header is (${r.headers.find(_._1.equals("Content-Type")).map(_._2).getOrElse("")})",
|
||||
404)
|
||||
)
|
||||
}
|
||||
|
||||
@ -230,7 +230,7 @@ object MessageDocsSwaggerDefinitions
|
||||
currency = currencyExample.value,
|
||||
description = Some(transactionDescriptionExample.value),
|
||||
startDate = DateWithDayExampleObject,
|
||||
finishDate = DateWithDayExampleObject,
|
||||
finishDate = Some(DateWithDayExampleObject),
|
||||
balance = BigDecimal(balanceAmountExample.value),
|
||||
status = transactionStatusExample.value,
|
||||
)
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
package code.api.berlin.group.v1_3
|
||||
|
||||
import code.api.util.APIUtil.DateWithDayFormat
|
||||
import code.api.util.APIUtil.rfc7231Date
|
||||
import code.api.util.ErrorMessages.InvalidDateFormat
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.format.{DateTimeFormatter, DateTimeParseException}
|
||||
import java.time.{LocalDate, ZoneId}
|
||||
import java.util.Date
|
||||
import java.util.{Date, Locale}
|
||||
|
||||
object BgSpecValidation {
|
||||
|
||||
|
||||
@ -385,7 +385,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
|
||||
val canReadTransactions = canReadTransactionsAccounts.map(_.accountId.value).contains(x.accountId.value)
|
||||
|
||||
val cashAccountType = x.attributes.getOrElse(Nil).filter(_.name== "cashAccountType").map(_.value).headOption.getOrElse("")
|
||||
|
||||
|
||||
CoreAccountJsonV13(
|
||||
resourceId = x.accountId.value,
|
||||
iban = iBan,
|
||||
@ -423,13 +423,13 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
|
||||
val transactionRef = LinkHrefJson(s"/$commonPath/transactions")
|
||||
val canReadTransactions = canReadTransactionsAccounts.map(_.accountId.value).contains(bankAccount.accountId.value)
|
||||
val cashAccountType = bankAccount.attributes.getOrElse(Nil).filter(_.name== "cashAccountType").map(_.value).headOption.getOrElse("")
|
||||
|
||||
|
||||
|
||||
|
||||
val account = AccountJsonV13(
|
||||
resourceId = bankAccount.accountId.value,
|
||||
iban = iBan,
|
||||
currency = bankAccount.currency,
|
||||
name = if(bankAccount.name.isBlank) None else Some(bankAccount.name),
|
||||
name = if(APIUtil.getPropsAsBoolValue("BG_v1312_show_account_name", defaultValue = true)) Some(bankAccount.name) else None,
|
||||
cashAccountType = cashAccountType,
|
||||
product = bankAccount.accountType,
|
||||
_links = AccountDetailsLinksJsonV13(
|
||||
@ -471,7 +471,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
|
||||
|
||||
def createTransactionJSON(transaction : ModeratedTransaction) : TransactionJsonV13 = {
|
||||
val bookingDate = transaction.startDate.orNull
|
||||
val valueDate = transaction.finishDate.orNull
|
||||
val valueDate = if(transaction.finishDate.isDefined) Some(BgSpecValidation.formatToISODate(transaction.finishDate.orNull)) else None
|
||||
|
||||
val creditorName = transaction.otherBankAccount.map(_.label.display).getOrElse("")
|
||||
val creditorAccountIban = stringOrNone(transaction.otherBankAccount.map(_.iban.getOrElse("")).getOrElse(""))
|
||||
@ -495,14 +495,14 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
|
||||
else
|
||||
Some(BgTransactionAccountJson(iban = debtorAccountIdIban)),
|
||||
transactionAmount = AmountOfMoneyV13(
|
||||
transaction.currency.getOrElse(""),
|
||||
transaction.currency.getOrElse(""),
|
||||
if(bgRemoveSignOfAmounts)
|
||||
transaction.amount.get.toString().trim.stripPrefix("-")
|
||||
else
|
||||
transaction.amount.get.toString()
|
||||
),
|
||||
bookingDate = Some(BgSpecValidation.formatToISODate(bookingDate)) ,
|
||||
valueDate = Some(BgSpecValidation.formatToISODate(valueDate)),
|
||||
valueDate = valueDate,
|
||||
remittanceInformationUnstructured = transaction.description
|
||||
)
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ import java.security.AccessControlException
|
||||
import java.text.{ParsePosition, SimpleDateFormat}
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.regex.Pattern
|
||||
import java.util.{Calendar, Date, UUID}
|
||||
import java.util.{Calendar, Date, Locale, UUID}
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.collection.immutable.{List, Nil}
|
||||
import scala.collection.mutable
|
||||
@ -132,6 +132,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
val DateWithSecondsFormat = new SimpleDateFormat(DateWithSeconds)
|
||||
val DateWithMsFormat = new SimpleDateFormat(DateWithMs)
|
||||
val DateWithMsRollbackFormat = new SimpleDateFormat(DateWithMsAndTimeZoneOffset)
|
||||
val rfc7231Date = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH)
|
||||
|
||||
val DateWithYearExampleString: String = "1100"
|
||||
val DateWithMonthExampleString: String = "1100-01"
|
||||
@ -1138,6 +1139,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
value <- tryo(values.head.toBoolean) ?~! FilterIsDeletedFormatError
|
||||
deleted = OBPIsDeleted(value)
|
||||
} yield deleted
|
||||
case "sort_by" => Full(OBPSortBy(values.head))
|
||||
case "status" => Full(OBPStatus(values.head))
|
||||
case "consumer_id" => Full(OBPConsumerId(values.head))
|
||||
case "azp" => Full(OBPAzp(values.head))
|
||||
@ -1180,6 +1182,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
|
||||
def createQueriesByHttpParams(httpParams: List[HTTPParam]): Box[List[OBPQueryParam]] = {
|
||||
for{
|
||||
sortBy <- getHttpParamValuesByName(httpParams, "sort_by")
|
||||
sortDirection <- getSortDirection(httpParams)
|
||||
fromDate <- getFromDate(httpParams)
|
||||
toDate <- getToDate(httpParams)
|
||||
@ -1226,10 +1229,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
*
|
||||
*/
|
||||
//val sortBy = json.header("obp_sort_by")
|
||||
val sortBy = None
|
||||
val ordering = OBPOrdering(sortBy, sortDirection)
|
||||
val ordering = OBPOrdering(None, sortDirection)
|
||||
//This guarantee the order
|
||||
List(limit, offset, ordering, fromDate, toDate,
|
||||
List(limit, offset, ordering, sortBy, fromDate, toDate,
|
||||
anon, status, consumerId, azp, iss, consentId, userId, url, appName, implementedByPartialFunction, implementedInVersion,
|
||||
verb, correlationId, duration, excludeAppNames, excludeUrlPattern, excludeImplementedByPartialfunctions,
|
||||
includeAppNames, includeUrlPattern, includeImplementedByPartialfunctions,
|
||||
@ -1259,6 +1261,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
*/
|
||||
def createHttpParamsByUrl(httpRequestUrl: String): Box[List[HTTPParam]] = {
|
||||
val sleep = getHttpRequestUrlParam(httpRequestUrl,"sleep")
|
||||
val sortBy = getHttpRequestUrlParam(httpRequestUrl,"sort_by")
|
||||
val sortDirection = getHttpRequestUrlParam(httpRequestUrl,"sort_direction")
|
||||
val fromDate = getHttpRequestUrlParam(httpRequestUrl,"from_date")
|
||||
val toDate = getHttpRequestUrlParam(httpRequestUrl,"to_date")
|
||||
@ -1300,7 +1303,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
val connectorName = getHttpRequestUrlParam(httpRequestUrl, "connector_name")
|
||||
|
||||
Full(List(
|
||||
HTTPParam("sort_direction",sortDirection), HTTPParam("from_date",fromDate), HTTPParam("to_date", toDate), HTTPParam("limit",limit), HTTPParam("offset",offset),
|
||||
HTTPParam("sort_by",sortBy), HTTPParam("sort_direction",sortDirection), HTTPParam("from_date",fromDate), HTTPParam("to_date", toDate), HTTPParam("limit",limit), HTTPParam("offset",offset),
|
||||
HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("azp", azp), HTTPParam("iss", iss), HTTPParam("consent_id", consentId), HTTPParam("user_id", userId), HTTPParam("url", url), HTTPParam("app_name", appName),
|
||||
HTTPParam("implemented_by_partial_function",implementedByPartialFunction), HTTPParam("implemented_in_version",implementedInVersion), HTTPParam("verb", verb),
|
||||
HTTPParam("correlation_id", correlationId), HTTPParam("duration", duration), HTTPParam("exclude_app_names", excludeAppNames),
|
||||
@ -3026,6 +3029,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
Future { (fullBoxOrException(Empty ~> APIFailureNewStyle(message, 400, Some(cc.toLight))), Some(cc)) }
|
||||
} else if (authHeaders.size > 1) { // Check Authorization Headers ambiguity
|
||||
Future { (Failure(ErrorMessages.AuthorizationHeaderAmbiguity + s"${authHeaders}"), Some(cc)) }
|
||||
} else if (BerlinGroupCheck.hasUnwantedConsentIdHeaderForBGEndpoint(url, reqHeaders)) {
|
||||
val message = ErrorMessages.InvalidConsentIdUsage
|
||||
Future { (fullBoxOrException(Empty ~> APIFailureNewStyle(message, 400, Some(cc.toLight))), Some(cc)) }
|
||||
} else if (APIUtil.`hasConsent-ID`(reqHeaders)) { // Berlin Group's Consent
|
||||
Consent.applyBerlinGroupRules(APIUtil.`getConsent-ID`(reqHeaders), cc.copy(consumer = consumerByCertificate))
|
||||
} else if (APIUtil.hasConsentJWT(reqHeaders)) { // Open Bank Project's Consent
|
||||
@ -3395,7 +3401,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
val apiFailure = af.copy(failMsg = failuresMsg).copy(ccl = callContext)
|
||||
throw new Exception(JsonAST.compactRender(Extraction.decompose(apiFailure)))
|
||||
case ParamFailure(_, _, _, failure : APIFailure) =>
|
||||
val callContext = CallContextLight(partialFunctionName = "", directLoginToken= "", oAuthToken= "")
|
||||
val callContext = CallContextLight()
|
||||
val apiFailure = APIFailureNewStyle(failMsg = failure.msg, failCode = failure.responseCode, ccl = Some(callContext))
|
||||
throw new Exception(JsonAST.compactRender(Extraction.decompose(apiFailure)))
|
||||
case ParamFailure(msg,_,_,_) =>
|
||||
|
||||
@ -203,9 +203,9 @@ case class CallContextLight(gatewayLoginRequestPayload: Option[PayloadOfJwtJSON]
|
||||
httpBody: Option[String] = None,
|
||||
authReqHeaderField: Option[String] = None,
|
||||
requestHeaders: List[HTTPParam] = Nil,
|
||||
partialFunctionName: String,
|
||||
directLoginToken: String,
|
||||
oAuthToken: String,
|
||||
partialFunctionName: String = "",
|
||||
directLoginToken: String = "",
|
||||
oAuthToken: String = "",
|
||||
xRateLimitLimit : Long = -1,
|
||||
xRateLimitRemaining : Long = -1,
|
||||
xRateLimitReset : Long = -1,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package code.api.util
|
||||
|
||||
import code.api.berlin.group.ConstantsBG
|
||||
import code.api.berlin.group.v1_3.BgSpecValidation
|
||||
import code.api.{APIFailureNewStyle, RequestHeader}
|
||||
import code.api.util.APIUtil.{OBPReturnType, fullBoxOrException}
|
||||
import code.api.util.BerlinGroupSigning.{getCertificateFromTppSignatureCertificate, getHeaderValue}
|
||||
@ -29,6 +30,22 @@ object BerlinGroupCheck extends MdcLoggable {
|
||||
.map(_.trim.toLowerCase)
|
||||
.toList.filterNot(_.isEmpty)
|
||||
|
||||
def hasUnwantedConsentIdHeaderForBGEndpoint(path: String, reqHeaders: List[HTTPParam]): Boolean = {
|
||||
val headerMap: Map[String, HTTPParam] = reqHeaders.map(h => h.name.toLowerCase -> h).toMap
|
||||
val hasConsentIdId = headerMap.get(RequestHeader.`Consent-ID`.toLowerCase).flatMap(_.values.headOption).isDefined
|
||||
|
||||
val parts = path.stripPrefix("/").stripSuffix("/").split("/").toList
|
||||
val doesNotRequireConsentId = parts.reverse match {
|
||||
case "consents" :: restOfThePath => true
|
||||
case consentId :: "consents" :: restOfThePath => true
|
||||
case "status" :: consentId :: "consents" :: restOfThePath => true
|
||||
case "authorisations" :: consentId :: "consents" :: restOfThePath => true
|
||||
case authorisationId :: "authorisations" :: consentId :: "consents" :: restOfThePath => true
|
||||
case _ => false
|
||||
}
|
||||
doesNotRequireConsentId && hasConsentIdId && path.contains(ConstantsBG.berlinGroupVersion1.urlPrefix)
|
||||
}
|
||||
|
||||
private def validateHeaders(
|
||||
verb: String,
|
||||
url: String,
|
||||
@ -46,7 +63,22 @@ object BerlinGroupCheck extends MdcLoggable {
|
||||
berlinGroupMandatoryHeaders.filterNot(headerMap.contains)
|
||||
}
|
||||
|
||||
val resultWithMissingHeaderCheck: Option[(Box[User], Option[CallContext])] =
|
||||
val resultWithMissingHeaderCheck: Option[(Box[User], Option[CallContext])] = {
|
||||
val date: Option[String] = headerMap.get(RequestHeader.Date.toLowerCase).flatMap(_.values.headOption)
|
||||
if (date.isDefined && !DateTimeUtil.isValidRfc7231Date(date.get)) {
|
||||
val message = ErrorMessages.NotValidRfc7231Date
|
||||
Some(
|
||||
(
|
||||
fullBoxOrException(
|
||||
Empty ~> APIFailureNewStyle(message, 400, forwardResult._2.map(_.toLight))
|
||||
),
|
||||
forwardResult._2
|
||||
)
|
||||
)
|
||||
} else None
|
||||
}
|
||||
|
||||
val resultWithWrongDateHeaderCheck: Option[(Box[User], Option[CallContext])] =
|
||||
if (missingHeaders.nonEmpty) {
|
||||
val message = if (missingHeaders.size == 1)
|
||||
ErrorMessages.MissingMandatoryBerlinGroupHeaders.replace("headers", "header")
|
||||
@ -101,21 +133,33 @@ object BerlinGroupCheck extends MdcLoggable {
|
||||
maybeSignature.flatMap { header =>
|
||||
BerlinGroupSignatureHeaderParser.parseSignatureHeader(header) match {
|
||||
case Right(parsed) =>
|
||||
// Log parsed values
|
||||
logger.debug(s"Parsed Signature Header:")
|
||||
logger.debug(s" SN: ${parsed.keyId.sn}")
|
||||
logger.debug(s" CA: ${parsed.keyId.ca}")
|
||||
logger.debug(s" CN: ${parsed.keyId.cn}")
|
||||
logger.debug(s" O: ${parsed.keyId.o}")
|
||||
logger.debug(s" Headers: ${parsed.headers.mkString(", ")}")
|
||||
logger.debug(s" Algorithm: ${parsed.algorithm}")
|
||||
logger.debug(s" Signature: ${parsed.signature}")
|
||||
|
||||
val certificate = getCertificateFromTppSignatureCertificate(reqHeaders)
|
||||
val serialNumber = certificate.getSerialNumber.toString
|
||||
if(parsed.keyId.sn != serialNumber) {
|
||||
logger.debug(s"Serial number from certificate: $serialNumber")
|
||||
val certSerialNumber = certificate.getSerialNumber
|
||||
|
||||
logger.debug(s"Certificate serial number (decimal): ${certSerialNumber.toString}")
|
||||
logger.debug(s"Certificate serial number (hex): ${certSerialNumber.toString(16).toUpperCase}")
|
||||
|
||||
val snMatches = BerlinGroupSignatureHeaderParser.doesSerialNumberMatch(parsed.keyId.sn, certSerialNumber)
|
||||
|
||||
if (!snMatches) {
|
||||
logger.debug(s"Serial number mismatch. Parsed SN: ${parsed.keyId.sn}, Certificate decimal: ${certSerialNumber.toString}, Certificate hex: ${certSerialNumber.toString(16).toUpperCase}")
|
||||
Some(
|
||||
(
|
||||
fullBoxOrException(
|
||||
Empty ~> APIFailureNewStyle(s"${ErrorMessages.InvalidSignatureHeader}keyId.SN: ${parsed.keyId.sn} does not match the serial number from certificate: $serialNumber", 400, forwardResult._2.map(_.toLight))
|
||||
Empty ~> APIFailureNewStyle(
|
||||
s"${ErrorMessages.InvalidSignatureHeader} keyId.SN does not match the serial number from certificate",
|
||||
400,
|
||||
forwardResult._2.map(_.toLight)
|
||||
)
|
||||
),
|
||||
forwardResult._2
|
||||
)
|
||||
@ -123,11 +167,16 @@ object BerlinGroupCheck extends MdcLoggable {
|
||||
} else {
|
||||
None // All good
|
||||
}
|
||||
|
||||
case Left(error) =>
|
||||
Some(
|
||||
(
|
||||
fullBoxOrException(
|
||||
Empty ~> APIFailureNewStyle(s"${ErrorMessages.InvalidSignatureHeader}$error", 400, forwardResult._2.map(_.toLight))
|
||||
Empty ~> APIFailureNewStyle(
|
||||
s"${ErrorMessages.InvalidSignatureHeader} $error",
|
||||
400,
|
||||
forwardResult._2.map(_.toLight)
|
||||
)
|
||||
),
|
||||
forwardResult._2
|
||||
)
|
||||
@ -138,6 +187,7 @@ object BerlinGroupCheck extends MdcLoggable {
|
||||
|
||||
// Chain validation steps
|
||||
resultWithMissingHeaderCheck
|
||||
.orElse(resultWithMissingHeaderCheck)
|
||||
.orElse(resultWithInvalidRequestIdCheck)
|
||||
.orElse(resultWithRequestIdUsedTwiceCheck)
|
||||
.orElse(resultWithInvalidSignatureHeaderCheck)
|
||||
|
||||
@ -80,6 +80,7 @@ object BerlinGroupError {
|
||||
case "400" if message.contains("OBP-35001") => "CONSENT_UNKNOWN"
|
||||
case "403" if message.contains("OBP-35001") => "CONSENT_UNKNOWN"
|
||||
|
||||
case "400" if message.contains("OBP-50200") => "RESOURCE_UNKNOWN"
|
||||
case "404" if message.contains("OBP-30076") => "RESOURCE_UNKNOWN"
|
||||
case "404" if message.contains("OBP-40001") => "RESOURCE_UNKNOWN"
|
||||
|
||||
@ -95,6 +96,8 @@ object BerlinGroupError {
|
||||
case "400" if message.contains("OBP-20253") => "FORMAT_ERROR"
|
||||
case "400" if message.contains("OBP-20254") => "FORMAT_ERROR"
|
||||
case "400" if message.contains("OBP-20255") => "FORMAT_ERROR"
|
||||
case "400" if message.contains("OBP-20256") => "FORMAT_ERROR"
|
||||
case "400" if message.contains("OBP-20257") => "FORMAT_ERROR"
|
||||
case "400" if message.contains("OBP-20251") => "FORMAT_ERROR"
|
||||
case "400" if message.contains("OBP-20088") => "FORMAT_ERROR"
|
||||
case "400" if message.contains("OBP-20089") => "FORMAT_ERROR"
|
||||
@ -104,6 +107,8 @@ object BerlinGroupError {
|
||||
|
||||
case "400" if message.contains("OBP-50221") => "PAYMENT_FAILED"
|
||||
|
||||
case "405" if message.contains("OBP-10404") => "SERVICE_INVALID"
|
||||
|
||||
case "429" if message.contains("OBP-10018") => "ACCESS_EXCEEDED"
|
||||
|
||||
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package code.api.util
|
||||
|
||||
object BerlinGroupSignatureHeaderParser {
|
||||
import code.util.Helper.MdcLoggable
|
||||
|
||||
case class ParsedKeyId(sn: String, ca: String, o: String)
|
||||
object BerlinGroupSignatureHeaderParser extends MdcLoggable {
|
||||
|
||||
case class ParsedSignature(keyId: ParsedKeyId, headers: List[String], signature: String)
|
||||
case class ParsedKeyId(sn: String, ca: String, cn: String, o: String)
|
||||
|
||||
case class ParsedSignature(keyId: ParsedKeyId, algorithm: String, headers: List[String], signature: String)
|
||||
|
||||
def parseQuotedValue(value: String): String =
|
||||
value.stripPrefix("\"").stripSuffix("\"").trim
|
||||
@ -18,18 +20,37 @@ object BerlinGroupSignatureHeaderParser {
|
||||
}
|
||||
}.toMap
|
||||
|
||||
val caValue = kvMap.get("CA").map(_.stripPrefix("CN="))
|
||||
// mandatory fields
|
||||
val snOpt = kvMap.get("SN")
|
||||
val oOpt = kvMap.get("O")
|
||||
|
||||
val caOpt = kvMap.get("CA")
|
||||
val cnOpt = kvMap.get("CN")
|
||||
|
||||
val (caFinal, cnFinal): (String, String) = (caOpt, cnOpt) match {
|
||||
case (Some(caRaw), Some(cnRaw)) =>
|
||||
// Both CA and CN are present: use them as-is
|
||||
(caRaw, cnRaw)
|
||||
|
||||
case (Some(caRaw), None) if caRaw.startsWith("CN=") =>
|
||||
// Special case: CA=CN=... → set both CA and CN to value after CN=
|
||||
val value = caRaw.stripPrefix("CN=")
|
||||
(value, value)
|
||||
|
||||
(kvMap.get("SN"), caValue, kvMap.get("O")) match {
|
||||
case (Some(sn), Some(ca), Some(o)) =>
|
||||
Right(ParsedKeyId(sn, ca, o))
|
||||
case _ =>
|
||||
Left(s"Invalid or missing fields in keyId: found keys ${kvMap.keys.mkString(", ")}")
|
||||
return Left(s"Missing mandatory 'CN' field or invalid CA format: found keys ${kvMap.keys.mkString(", ")}")
|
||||
}
|
||||
|
||||
(snOpt, oOpt) match {
|
||||
case (Some(sn), Some(o)) =>
|
||||
Right(ParsedKeyId(sn, caFinal, cnFinal, o))
|
||||
case _ =>
|
||||
Left(s"Missing mandatory 'SN' or 'O' field: found keys ${kvMap.keys.mkString(", ")}")
|
||||
}
|
||||
}
|
||||
|
||||
def parseSignatureHeader(header: String): Either[String, ParsedSignature] = {
|
||||
val fields = header.split(",(?=\\s*(keyId|headers|signature)=)").map(_.trim)
|
||||
val fields = header.split(",(?=\\s*(keyId|headers|algorithm|signature)=)").map(_.trim)
|
||||
|
||||
val kvMap = fields.flatMap { field =>
|
||||
field.split("=", 2) match {
|
||||
@ -43,24 +64,61 @@ object BerlinGroupSignatureHeaderParser {
|
||||
keyId <- parseKeyIdField(keyIdStr)
|
||||
headers <- kvMap.get("headers").map(_.split("\\s+").toList).toRight("Missing 'headers' field")
|
||||
sig <- kvMap.get("signature").toRight("Missing 'signature' field")
|
||||
} yield ParsedSignature(keyId, headers, sig)
|
||||
algorithm <- kvMap.get("algorithm").toRight("Missing 'algorithm' field")
|
||||
} yield ParsedSignature(keyId, algorithm, headers, sig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect and match incoming SN as decimal or hex against certificate serial number.
|
||||
*/
|
||||
def doesSerialNumberMatch(incomingSn: String, certSerial: java.math.BigInteger): Boolean = {
|
||||
try {
|
||||
val incomingAsDecimal = new java.math.BigInteger(incomingSn, 10)
|
||||
if (incomingAsDecimal == certSerial) {
|
||||
logger.debug(s"SN matched in decimal")
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
case _: NumberFormatException =>
|
||||
logger.debug(s"Incoming SN is not valid decimal: $incomingSn")
|
||||
}
|
||||
|
||||
try {
|
||||
val incomingAsHex = new java.math.BigInteger(incomingSn, 16)
|
||||
if (incomingAsHex == certSerial) {
|
||||
logger.debug(s"SN matched in hex")
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
case _: NumberFormatException =>
|
||||
logger.debug(s"Incoming SN is not valid hex: $incomingSn")
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
// Example usage
|
||||
def main(args: Array[String]): Unit = {
|
||||
val header =
|
||||
"""keyId="CA=CN=MAIB Prisacaru Sergiu (Test), SN=43A, O=MAIB", headers="digest date x-request-id", signature="abc123+==" """
|
||||
val testHeaders = List(
|
||||
"""keyId="CA=CN=MAIB Prisacaru Sergiu (Test), SN=43A, O=MAIB", headers="digest date x-request-id", signature="abc123+=="""",
|
||||
"""keyId="CA=SomeCAValue, CN=SomeCNValue, SN=43A, O=MAIB", headers="digest date x-request-id", signature="def456+=="""",
|
||||
"""keyId="CA=MissingCN, SN=43A, O=MAIB", headers="digest date x-request-id", signature="should_fail""""
|
||||
)
|
||||
|
||||
parseSignatureHeader(header) match {
|
||||
case Right(parsed) =>
|
||||
println("Parsed Signature Header:")
|
||||
println(s"SN: ${parsed.keyId.sn}")
|
||||
println(s"CA: ${parsed.keyId.ca}")
|
||||
println(s"O: ${parsed.keyId.o}")
|
||||
println(s"Headers: ${parsed.headers.mkString(", ")}")
|
||||
println(s"Signature: ${parsed.signature}")
|
||||
case Left(error) =>
|
||||
println(s"Error: $error")
|
||||
testHeaders.foreach { header =>
|
||||
println(s"\nParsing header:\n$header")
|
||||
parseSignatureHeader(header) match {
|
||||
case Right(parsed) =>
|
||||
println("Parsed Signature Header:")
|
||||
println(s" SN: ${parsed.keyId.sn}")
|
||||
println(s" CA: ${parsed.keyId.ca}")
|
||||
println(s" CN: ${parsed.keyId.cn}")
|
||||
println(s" O: ${parsed.keyId.o}")
|
||||
println(s" Headers: ${parsed.headers.mkString(", ")}")
|
||||
println(s" Signature: ${parsed.signature}")
|
||||
case Left(error) =>
|
||||
println(s"Error: $error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
package code.api.util
|
||||
|
||||
import code.api.RequestHeader
|
||||
import code.api.util.APIUtil.OBPReturnType
|
||||
import code.api.{APIFailureNewStyle, RequestHeader}
|
||||
import code.api.util.APIUtil.{OBPReturnType, fullBoxOrException}
|
||||
import code.api.util.ErrorUtil.apiFailure
|
||||
import code.api.util.newstyle.RegulatedEntityNewStyle.getRegulatedEntitiesNewStyle
|
||||
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.{RegulatedEntityTrait, User}
|
||||
import net.liftweb.common.{Box, Failure, Full}
|
||||
import net.liftweb.common.{Box, Empty, Failure, Full}
|
||||
import net.liftweb.http.provider.HTTPParam
|
||||
import net.liftweb.util.Helpers
|
||||
|
||||
@ -181,16 +182,16 @@ object BerlinGroupSigning extends MdcLoggable {
|
||||
(isVerified, isValidated) match {
|
||||
case (true, true) => forwardResult
|
||||
case (true, false) if bypassValidation => forwardResult
|
||||
case (true, false) => (Failure(ErrorMessages.X509PublicKeyCannotBeValidated), forwardResult._2)
|
||||
case (false, _) => (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2)
|
||||
case (true, false) => apiFailure(ErrorMessages.X509PublicKeyCannotBeValidated, 401)(forwardResult)
|
||||
case (false, _) => apiFailure(ErrorMessages.X509PublicKeyCannotVerify, 401)(forwardResult)
|
||||
}
|
||||
} else { // The two DIGEST hashes do NOT match, the integrity of the request body is NOT confirmed.
|
||||
logger.debug(s"Generated digest: $generatedDigest")
|
||||
logger.debug(s"Request header digest: $requestHeaderDigest")
|
||||
(Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2)
|
||||
apiFailure(ErrorMessages.X509PublicKeyCannotVerify, 401)(forwardResult)
|
||||
}
|
||||
case Failure(msg, t, c) => (Failure(msg, t, c), forwardResult._2) // PEM certificate is not valid
|
||||
case _ => (Failure(ErrorMessages.X509GeneralError), forwardResult._2) // PEM certificate cannot be validated
|
||||
case _ => apiFailure(ErrorMessages.X509GeneralError, 401)(forwardResult) // PEM certificate cannot be validated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ import code.views.Views
|
||||
import com.nimbusds.jwt.JWTClaimsSet
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
import com.openbankproject.commons.model._
|
||||
import com.openbankproject.commons.util.ApiStandards
|
||||
import net.liftweb.common._
|
||||
import net.liftweb.http.provider.HTTPParam
|
||||
import net.liftweb.json.JsonParser.ParseException
|
||||
@ -239,32 +240,37 @@ object Consent extends MdcLoggable {
|
||||
val consentBox = Consents.consentProvider.vend.getConsentByConsentId(consent.jti)
|
||||
logger.debug(s"code.api.util.Consent.checkConsent.getConsentByConsentId: consentBox($consentBox)")
|
||||
val result = consentBox match {
|
||||
case Full(c) if c.mStatus.toString().toUpperCase == ConsentStatus.ACCEPTED.toString | c.mStatus.toString().toLowerCase() == ConsentStatus.valid.toString =>
|
||||
verifyHmacSignedJwt(consentIdAsJwt, c) match {
|
||||
case true =>
|
||||
(System.currentTimeMillis / 1000) match {
|
||||
case currentTimeInSeconds if currentTimeInSeconds < consent.nbf =>
|
||||
Failure(ErrorMessages.ConsentNotBeforeIssue)
|
||||
case currentTimeInSeconds if currentTimeInSeconds > consent.exp =>
|
||||
Failure(ErrorMessages.ConsentExpiredIssue)
|
||||
case _ =>
|
||||
logger.debug(s"start code.api.util.Consent.checkConsent.checkConsumerIsActiveAndMatched(consent($consent))")
|
||||
val result = checkConsumerIsActiveAndMatched(consent, callContext)
|
||||
logger.debug(s"end code.api.util.Consent.checkConsent.checkConsumerIsActiveAndMatched: result($result)")
|
||||
result
|
||||
case Full(c) =>
|
||||
// Always verify signature first
|
||||
if (!verifyHmacSignedJwt(consentIdAsJwt, c)) {
|
||||
Failure(ErrorMessages.ConsentVerificationIssue)
|
||||
} else {
|
||||
// Then check time constraints
|
||||
val currentTimeInSeconds = System.currentTimeMillis / 1000
|
||||
if (currentTimeInSeconds < consent.nbf) {
|
||||
Failure(ErrorMessages.ConsentNotBeforeIssue)
|
||||
} else if (currentTimeInSeconds > consent.exp) {
|
||||
ErrorUtil.apiFailureToBox(ErrorMessages.ConsentExpiredIssue, 401)(Some(callContext))
|
||||
} else {
|
||||
// Then check consent status
|
||||
if (c.apiStandard == ConstantsBG.berlinGroupVersion1.apiStandard &&
|
||||
c.status.toLowerCase != ConsentStatus.valid.toString) {
|
||||
Failure(s"${ErrorMessages.ConsentStatusIssue}${ConsentStatus.valid.toString}.")
|
||||
} else if ((c.apiStandard == ApiStandards.obp.toString || c.apiStandard.isBlank) &&
|
||||
c.mStatus.toString.toUpperCase != ConsentStatus.ACCEPTED.toString) {
|
||||
Failure(s"${ErrorMessages.ConsentStatusIssue}${ConsentStatus.ACCEPTED.toString}.")
|
||||
} else {
|
||||
logger.debug(s"start code.api.util.Consent.checkConsent.checkConsumerIsActiveAndMatched(consent($consent))")
|
||||
val consumerResult = checkConsumerIsActiveAndMatched(consent, callContext)
|
||||
logger.debug(s"end code.api.util.Consent.checkConsent.checkConsumerIsActiveAndMatched: result($consumerResult)")
|
||||
consumerResult
|
||||
}
|
||||
case false =>
|
||||
Failure(ErrorMessages.ConsentVerificationIssue)
|
||||
}
|
||||
}
|
||||
case Full(c) if c.apiStandard == ConstantsBG.berlinGroupVersion1.apiStandard && // Berlin Group Consent
|
||||
c.status.toLowerCase() != ConsentStatus.valid.toString =>
|
||||
Failure(s"${ErrorMessages.ConsentStatusIssue}${ConsentStatus.valid.toString}.")
|
||||
case Full(c) if c.mStatus.toString().toUpperCase() != ConsentStatus.ACCEPTED.toString =>
|
||||
Failure(s"${ErrorMessages.ConsentStatusIssue}${ConsentStatus.ACCEPTED.toString}.")
|
||||
case _ =>
|
||||
Failure(ErrorMessages.ConsentNotFound)
|
||||
}
|
||||
logger.debug(s"code.api.util.Consent.checkConsent.consentBox.result: result($result)")
|
||||
logger.debug(s"code.api.util.Consent.checkConsent.result: result($result)")
|
||||
result
|
||||
}
|
||||
|
||||
@ -588,18 +594,15 @@ object Consent extends MdcLoggable {
|
||||
Future(Failure(ErrorMessages.ConsentCheckExpiredIssue), Some(updatedCallContext))
|
||||
}
|
||||
} catch { // Possible exceptions
|
||||
case e: ParseException => {
|
||||
case e: ParseException =>
|
||||
logger.debug(s"code.api.util.JwtUtil.getSignedPayloadAsJson.ParseException: $e")
|
||||
Future(Failure("ParseException: " + e.getMessage), Some(updatedCallContext))
|
||||
}
|
||||
case e: MappingException => {
|
||||
case e: MappingException =>
|
||||
logger.debug(s"code.api.util.JwtUtil.getSignedPayloadAsJson.MappingException: $e")
|
||||
Future(Failure("MappingException: " + e.getMessage), Some(updatedCallContext))
|
||||
}
|
||||
case e: Throwable => {
|
||||
case e: Throwable =>
|
||||
logger.debug(s"code.api.util.JwtUtil.getSignedPayloadAsJson.Throwable: $e")
|
||||
Future(Failure("parsing failed: " + e.getMessage), Some(updatedCallContext))
|
||||
}
|
||||
Future(Failure(ErrorUtil.extractFailureMessage(e)), Some(updatedCallContext))
|
||||
}
|
||||
case failure@Failure(_, _, _) =>
|
||||
Future(failure, Some(updatedCallContext))
|
||||
@ -733,89 +736,95 @@ object Consent extends MdcLoggable {
|
||||
callContext: Option[CallContext]): Future[Box[String]] = {
|
||||
|
||||
val currentTimeInSeconds = System.currentTimeMillis / 1000
|
||||
val validUntilTimeInSeconds = validUntil match {
|
||||
case Some(date) => date.getTime() / 1000
|
||||
case _ => currentTimeInSeconds
|
||||
}
|
||||
// Write Consent's Auth Context to the DB
|
||||
user map { u =>
|
||||
val validUntilTimeInSeconds = validUntil.map(_.getTime / 1000).getOrElse(currentTimeInSeconds)
|
||||
|
||||
// Write Consent's Auth Context to DB
|
||||
user.foreach { u =>
|
||||
val authContexts = UserAuthContextProvider.userAuthContextProvider.vend.getUserAuthContextsBox(u.userId)
|
||||
.map(_.map(i => BasicUserAuthContext(i.key, i.value)))
|
||||
ConsentAuthContextProvider.consentAuthContextProvider.vend.createOrUpdateConsentAuthContexts(consentId, authContexts.getOrElse(Nil))
|
||||
}
|
||||
|
||||
// 1. Add access
|
||||
|
||||
// Helper to get ConsentView or fail box
|
||||
def getConsentView(ibanOpt: Option[String], viewId: String): Future[Box[ConsentView]] = {
|
||||
val iban = ibanOpt.getOrElse("")
|
||||
Connector.connector.vend.getBankAccountByIban(iban, callContext).map { bankAccount =>
|
||||
logger.debug(s"createBerlinGroupConsentJWT.bankAccount: $bankAccount")
|
||||
val error = s"${InvalidConnectorResponse} IBAN: $iban ${handleBox(bankAccount._1)}"
|
||||
bankAccount._1 match {
|
||||
case Full(acc) =>
|
||||
Full(ConsentView(
|
||||
bank_id = acc.bankId.value,
|
||||
account_id = acc.accountId.value,
|
||||
view_id = viewId,
|
||||
None
|
||||
))
|
||||
case _ =>
|
||||
ErrorUtil.apiFailureToBox(error, 400)(callContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare lists of future boxes
|
||||
val allAccesses = consent.access.accounts.getOrElse(Nil) :::
|
||||
consent.access.balances.getOrElse(Nil) ::: // Balances access implies and Account access as well
|
||||
consent.access.transactions.getOrElse(Nil) // Transactions access implies and Account access as well
|
||||
val accounts: List[Future[ConsentView]] = allAccesses.distinct map { account =>
|
||||
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount =>
|
||||
logger.debug(s"createBerlinGroupConsentJWT.accounts.bankAccount: $bankAccount")
|
||||
val error = s"${InvalidConnectorResponse} IBAN: ${account.iban.getOrElse("")} ${handleBox(bankAccount._1)}"
|
||||
ConsentView(
|
||||
bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""),
|
||||
account_id = bankAccount._1.map(_.accountId.value).openOrThrowException(error),
|
||||
view_id = Constant.SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID,
|
||||
None
|
||||
)
|
||||
consent.access.balances.getOrElse(Nil) :::
|
||||
consent.access.transactions.getOrElse(Nil)
|
||||
|
||||
val accounts: List[Future[Box[ConsentView]]] = allAccesses.distinct.map { account =>
|
||||
getConsentView(account.iban, Constant.SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID)
|
||||
}
|
||||
|
||||
val balances: List[Future[Box[ConsentView]]] = consent.access.balances.getOrElse(Nil).map { account =>
|
||||
getConsentView(account.iban, Constant.SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID)
|
||||
}
|
||||
val transactions: List[Future[Box[ConsentView]]] = consent.access.transactions.getOrElse(Nil).map { account =>
|
||||
getConsentView(account.iban, Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID)
|
||||
}
|
||||
|
||||
// Collect optional headers
|
||||
val headers = callContext.map(_.requestHeaders).getOrElse(Nil)
|
||||
val tppRedirectUri = headers.find(_.name == RequestHeader.`TPP-Redirect-URI`)
|
||||
val tppNokRedirectUri = headers.find(_.name == RequestHeader.`TPP-Nok-Redirect-URI`)
|
||||
val xRequestId = headers.find(_.name == RequestHeader.`X-Request-ID`)
|
||||
val psuDeviceId = headers.find(_.name == RequestHeader.`PSU-Device-ID`)
|
||||
val psuIpAddress = headers.find(_.name == RequestHeader.`PSU-IP-Address`)
|
||||
val psuGeoLocation = headers.find(_.name == RequestHeader.`PSU-Geo-Location`)
|
||||
|
||||
def sequenceBoxes[A](boxes: List[Box[A]]): Box[List[A]] = {
|
||||
boxes.foldRight(Full(Nil): Box[List[A]]) { (box, acc) =>
|
||||
for {
|
||||
x <- box
|
||||
xs <- acc
|
||||
} yield x :: xs
|
||||
}
|
||||
}
|
||||
val balances: List[Future[ConsentView]] = consent.access.balances.getOrElse(Nil) map { account =>
|
||||
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount =>
|
||||
logger.debug(s"createBerlinGroupConsentJWT.balances.bankAccount: $bankAccount")
|
||||
val error = s"${InvalidConnectorResponse} IBAN: ${account.iban.getOrElse("")} ${handleBox(bankAccount._1)}"
|
||||
ConsentView(
|
||||
bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""),
|
||||
account_id = bankAccount._1.map(_.accountId.value).openOrThrowException(error),
|
||||
view_id = Constant.SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID,
|
||||
None
|
||||
)
|
||||
}
|
||||
}
|
||||
val transactions: List[Future[ConsentView]] = consent.access.transactions.getOrElse(Nil) map { account =>
|
||||
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount =>
|
||||
logger.debug(s"createBerlinGroupConsentJWT.transactions.bankAccount: $bankAccount")
|
||||
val error = s"${InvalidConnectorResponse} IBAN: ${account.iban.getOrElse("")} ${handleBox(bankAccount._1)}"
|
||||
ConsentView(
|
||||
bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""),
|
||||
account_id = bankAccount._1.map(_.accountId.value).openOrThrowException(error),
|
||||
view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID,
|
||||
None
|
||||
)
|
||||
}
|
||||
}
|
||||
val tppRedirectUri: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`TPP-Redirect-URI`)
|
||||
val tppNokRedirectUri: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`TPP-Nok-Redirect-URI`)
|
||||
val xRequestId: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`X-Request-ID`)
|
||||
val psuDeviceId: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`PSU-Device-ID`)
|
||||
val psuIpAddress: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`PSU-IP-Address`)
|
||||
val psuGeoLocation: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`PSU-Geo-Location`)
|
||||
Future.sequence(accounts ::: balances ::: transactions) map { views =>
|
||||
|
||||
// Combine and build final JWT
|
||||
Future.sequence(accounts ::: balances ::: transactions).map { listOfBoxes =>
|
||||
sequenceBoxes(listOfBoxes).map { views =>
|
||||
val json = ConsentJWT(
|
||||
createdByUserId = user.map(_.userId).getOrElse(""),
|
||||
sub = APIUtil.generateUUID(),
|
||||
iss = Constant.HostName,
|
||||
aud = consumerId.getOrElse(""),
|
||||
jti = consentId,
|
||||
iat = currentTimeInSeconds,
|
||||
nbf = currentTimeInSeconds,
|
||||
exp = validUntilTimeInSeconds,
|
||||
request_headers = tppRedirectUri.toList :::
|
||||
tppNokRedirectUri.toList :::
|
||||
xRequestId.toList :::
|
||||
psuDeviceId.toList :::
|
||||
psuIpAddress.toList :::
|
||||
psuGeoLocation.toList,
|
||||
name = None,
|
||||
email = None,
|
||||
entitlements = Nil,
|
||||
views = views,
|
||||
access = Some(consent.access)
|
||||
)
|
||||
implicit val formats = CustomJsonFormats.formats
|
||||
val jwtPayloadAsJson = compactRender(Extraction.decompose(json))
|
||||
val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson)
|
||||
Full(CertificateUtil.jwtWithHmacProtection(jwtClaims, secret))
|
||||
createdByUserId = user.map(_.userId).getOrElse(""),
|
||||
sub = APIUtil.generateUUID(),
|
||||
iss = Constant.HostName,
|
||||
aud = consumerId.getOrElse(""),
|
||||
jti = consentId,
|
||||
iat = currentTimeInSeconds,
|
||||
nbf = currentTimeInSeconds,
|
||||
exp = validUntilTimeInSeconds,
|
||||
request_headers = List(
|
||||
tppRedirectUri, tppNokRedirectUri, xRequestId, psuDeviceId, psuIpAddress, psuGeoLocation
|
||||
).flatten,
|
||||
name = None,
|
||||
email = None,
|
||||
entitlements = Nil,
|
||||
views = views,
|
||||
access = Some(consent.access)
|
||||
)
|
||||
implicit val formats = CustomJsonFormats.formats
|
||||
val jwtPayloadAsJson = compactRender(Extraction.decompose(json))
|
||||
val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson)
|
||||
CertificateUtil.jwtWithHmacProtection(jwtClaims, secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
def updateAccountAccessOfBerlinGroupConsentJWT(access: ConsentAccessJson,
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package code.api.util
|
||||
|
||||
import code.api.util.APIUtil.rfc7231Date
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
object DateTimeUtil {
|
||||
@ -33,4 +35,20 @@ object DateTimeUtil {
|
||||
|
||||
if (parts.isEmpty) "less than a second" else parts.mkString(", ")
|
||||
}
|
||||
|
||||
// Define the correct RFC 7231 date format (IMF-fixdate)
|
||||
private val dateFormat = rfc7231Date
|
||||
// Force timezone to be GMT
|
||||
dateFormat.setLenient(false)
|
||||
|
||||
def isValidRfc7231Date(dateStr: String): Boolean = {
|
||||
try {
|
||||
val parsedDate = dateFormat.parse(dateStr)
|
||||
// Check that the timezone part is exactly "GMT"
|
||||
dateStr.endsWith(" GMT")
|
||||
} catch {
|
||||
case _: Exception => false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -277,6 +277,8 @@ object ErrorMessages {
|
||||
val InvalidUuidValue = "OBP-20253: Invalid format. Must be a UUID."
|
||||
val InvalidSignatureHeader = "OBP-20254: Invalid Signature header. "
|
||||
val InvalidRequestIdValueAlreadyUsed = "OBP-20255: Request Id value already used. "
|
||||
val InvalidConsentIdUsage = "OBP-20256: Consent-Id must not be used for this API Endpoint. "
|
||||
val NotValidRfc7231Date = "OBP-20257: Request header Date is not in accordance with RFC 7231 "
|
||||
|
||||
// X.509
|
||||
val X509GeneralError = "OBP-20300: PEM Encoded Certificate issue."
|
||||
|
||||
46
obp-api/src/main/scala/code/api/util/ErrorUtil.scala
Normal file
46
obp-api/src/main/scala/code/api/util/ErrorUtil.scala
Normal file
@ -0,0 +1,46 @@
|
||||
package code.api.util
|
||||
|
||||
import code.api.APIFailureNewStyle
|
||||
import code.api.util.APIUtil.fullBoxOrException
|
||||
import com.openbankproject.commons.model.User
|
||||
import net.liftweb.common.{Box, Empty, Failure}
|
||||
import net.liftweb.json._
|
||||
|
||||
|
||||
object ErrorUtil {
|
||||
def apiFailure(errorMessage: String, httpCode: Int)(forwardResult: (Box[User], Option[CallContext])): (Box[User], Option[CallContext]) = {
|
||||
val (_, second) = forwardResult
|
||||
val apiFailure = APIFailureNewStyle(
|
||||
failMsg = errorMessage,
|
||||
failCode = httpCode,
|
||||
ccl = second.map(_.toLight)
|
||||
)
|
||||
val failureBox = Empty ~> apiFailure
|
||||
(
|
||||
fullBoxOrException(failureBox),
|
||||
second
|
||||
)
|
||||
}
|
||||
|
||||
def apiFailureToBox[T](errorMessage: String, httpCode: Int)(cc: Option[CallContext]): Box[T] = {
|
||||
val apiFailure = APIFailureNewStyle(
|
||||
failMsg = errorMessage,
|
||||
failCode = httpCode,
|
||||
ccl = cc.map(_.toLight)
|
||||
)
|
||||
val failureBox: Box[T] = Empty ~> apiFailure
|
||||
fullBoxOrException(failureBox)
|
||||
}
|
||||
|
||||
|
||||
|
||||
implicit val formats: Formats = DefaultFormats
|
||||
def extractFailureMessage(e: Throwable): String = {
|
||||
parse(e.getMessage)
|
||||
.extractOpt[APIFailureNewStyle] // Extract message from APIFailureNewStyle
|
||||
.map(_.failMsg) // or provide a original one
|
||||
.getOrElse(e.getMessage)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -14,7 +14,7 @@ object KeycloakAdmin {
|
||||
// Initialize Logback logger
|
||||
private val logger = LoggerFactory.getLogger("okhttp3")
|
||||
|
||||
val integrateWithKeycloak = APIUtil.getPropsAsBoolValue("integrate_with_keycloak", defaultValue = false)
|
||||
val integrateWithKeycloak: Boolean = APIUtil.getPropsAsBoolValue("integrate_with_keycloak", defaultValue = false)
|
||||
// Define variables (replace with actual values)
|
||||
private val keycloakHost = Keycloak.keycloakHost
|
||||
private val realm = APIUtil.getPropsValue(nameOfProperty = "oauth2.keycloak.realm", "master")
|
||||
@ -30,7 +30,7 @@ object KeycloakAdmin {
|
||||
builder.build()
|
||||
}
|
||||
// Create OkHttp client with logging
|
||||
val client = createHttpClientWithLogback()
|
||||
val client: OkHttpClient = createHttpClientWithLogback()
|
||||
|
||||
def createKeycloakConsumer(consumer: Consumer): Box[Boolean] = {
|
||||
val isPublic =
|
||||
|
||||
@ -26,6 +26,7 @@ case class OBPFromDate(value: Date) extends OBPQueryParam
|
||||
case class OBPToDate(value: Date) extends OBPQueryParam
|
||||
case class OBPOrdering(field: Option[String], order: OBPOrder) extends OBPQueryParam
|
||||
case class OBPConsumerId(value: String) extends OBPQueryParam
|
||||
case class OBPSortBy(value: String) extends OBPQueryParam
|
||||
case class OBPAzp(value: String) extends OBPQueryParam
|
||||
case class OBPIss(value: String) extends OBPQueryParam
|
||||
case class OBPConsentId(value: String) extends OBPQueryParam
|
||||
|
||||
@ -2,6 +2,7 @@ package code.api.v5_1_0
|
||||
|
||||
|
||||
import code.api.Constant
|
||||
import code.api.OAuth2Login.Keycloak
|
||||
import code.api.Constant._
|
||||
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
|
||||
import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessAccountsJson, ConsentAccessJson}
|
||||
@ -44,6 +45,7 @@ import code.util.Helper
|
||||
import code.util.Helper.ObpS
|
||||
import code.views.Views
|
||||
import code.views.system.{AccountAccess, ViewDefinition}
|
||||
import code.webuiprops.{MappedWebUiPropsProvider, WebUiPropsCommons}
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
import com.openbankproject.commons.model._
|
||||
@ -137,6 +139,37 @@ trait APIMethods510 {
|
||||
}
|
||||
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
getOAuth2ServerWellKnown,
|
||||
implementedInApiVersion,
|
||||
"getOAuth2ServerWellKnown",
|
||||
"GET",
|
||||
"/well-known",
|
||||
"Get Well Known URIs",
|
||||
"""Get the OAuth2 server's public Well Known URIs.
|
||||
|
|
||||
""".stripMargin,
|
||||
EmptyBody,
|
||||
oAuth2ServerJwksUrisJson,
|
||||
List(
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagApi))
|
||||
|
||||
lazy val getOAuth2ServerWellKnown: OBPEndpoint = {
|
||||
case "well-known" :: Nil JsonGet _ => {
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
(_, callContext) <- anonymousAccess(cc)
|
||||
} yield {
|
||||
val keycloak: WellKnownUriJsonV510 = WellKnownUriJsonV510("keycloak", Keycloak.wellKnownOpenidConfiguration.toURL.toString)
|
||||
(WellKnownUrisJsonV510(List(keycloak)), HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
regulatedEntities,
|
||||
implementedInApiVersion,
|
||||
@ -5146,6 +5179,68 @@ trait APIMethods510 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
getWebUiProps,
|
||||
implementedInApiVersion,
|
||||
nameOf(getWebUiProps),
|
||||
"GET",
|
||||
"/webui-props",
|
||||
"Get WebUiProps",
|
||||
s"""
|
||||
|
|
||||
|Get the all WebUiProps key values, those props key with "webui_" can be stored in DB, this endpoint get all from DB.
|
||||
|
|
||||
|url query parameter:
|
||||
|active: It must be a boolean string. and If active = true, it will show
|
||||
| combination of explicit (inserted) + implicit (default) method_routings.
|
||||
|
|
||||
|eg:
|
||||
|${getObpApiRoot}/v5.1.0/webui-props
|
||||
|${getObpApiRoot}/v5.1.0/webui-props?active=true
|
||||
|
|
||||
|""",
|
||||
EmptyBody,
|
||||
ListResult(
|
||||
"webui-props",
|
||||
(List(WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com", Some("web-ui-props-id"))))
|
||||
)
|
||||
,
|
||||
List(
|
||||
UserHasMissingRoles,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagWebUiProps)
|
||||
)
|
||||
lazy val getWebUiProps: OBPEndpoint = {
|
||||
case "webui-props":: Nil JsonGet req => {
|
||||
cc => implicit val ec = EndpointContext(Some(cc))
|
||||
val active = ObpS.param("active").getOrElse("false")
|
||||
for {
|
||||
invalidMsg <- Future(s"""$InvalidFilterParameterFormat `active` must be a boolean, but current `active` value is: ${active} """)
|
||||
isActived <- NewStyle.function.tryons(invalidMsg, 400, cc.callContext) {
|
||||
active.toBoolean
|
||||
}
|
||||
explicitWebUiProps <- Future{ MappedWebUiPropsProvider.getAll() }
|
||||
implicitWebUiPropsRemovedDuplicated = if(isActived){
|
||||
val implicitWebUiProps = getWebUIPropsPairs.map(webUIPropsPairs=>WebUiPropsCommons(webUIPropsPairs._1, webUIPropsPairs._2, webUiPropsId= Some("default")))
|
||||
if(explicitWebUiProps.nonEmpty){
|
||||
//get the same name props in the `implicitWebUiProps`
|
||||
val duplicatedProps : List[WebUiPropsCommons]= explicitWebUiProps.map(explicitWebUiProp => implicitWebUiProps.filter(_.name == explicitWebUiProp.name)).flatten
|
||||
//remove the duplicated fields from `implicitWebUiProps`
|
||||
implicitWebUiProps diff duplicatedProps
|
||||
}
|
||||
else implicitWebUiProps.distinct
|
||||
} else {
|
||||
List.empty[WebUiPropsCommons]
|
||||
}
|
||||
} yield {
|
||||
val listCommons: List[WebUiPropsCommons] = explicitWebUiProps ++ implicitWebUiPropsRemovedDuplicated
|
||||
(ListResult("webui_props", listCommons), HttpCode.`200`(cc.callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -58,6 +58,9 @@ import java.util.Date
|
||||
import scala.util.Try
|
||||
|
||||
|
||||
case class WellKnownUrisJsonV510(well_known_uris: List[WellKnownUriJsonV510])
|
||||
case class WellKnownUriJsonV510(provider: String, url: String)
|
||||
|
||||
case class RegulatedEntityAttributeRequestJsonV510(
|
||||
name: String,
|
||||
attribute_type: String,
|
||||
|
||||
@ -1550,7 +1550,7 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable {
|
||||
currency=currencyExample.value,
|
||||
description=Some(transactionDescriptionExample.value),
|
||||
startDate=toDate(transactionStartDateExample),
|
||||
finishDate=toDate(transactionFinishDateExample),
|
||||
finishDate=Some(toDate(transactionFinishDateExample)),
|
||||
balance=BigDecimal(balanceExample.value),
|
||||
status=transactionStatusExample.value
|
||||
)))
|
||||
@ -1685,7 +1685,7 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable {
|
||||
currency=currencyExample.value,
|
||||
description=Some(transactionDescriptionExample.value),
|
||||
startDate=toDate(transactionStartDateExample),
|
||||
finishDate=toDate(transactionFinishDateExample),
|
||||
finishDate=Some(toDate(transactionFinishDateExample)),
|
||||
balance=BigDecimal(balanceExample.value),
|
||||
status=transactionStatusExample.value))
|
||||
),
|
||||
|
||||
@ -1498,7 +1498,7 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable {
|
||||
currency=currencyExample.value,
|
||||
description=Some(transactionDescriptionExample.value),
|
||||
startDate=toDate(transactionStartDateExample),
|
||||
finishDate=toDate(transactionFinishDateExample),
|
||||
finishDate=Some(toDate(transactionFinishDateExample)),
|
||||
balance=BigDecimal(balanceExample.value),
|
||||
status=transactionStatusExample.value)))
|
||||
),
|
||||
@ -1632,7 +1632,7 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable {
|
||||
currency=currencyExample.value,
|
||||
description=Some(transactionDescriptionExample.value),
|
||||
startDate=toDate(transactionStartDateExample),
|
||||
finishDate=toDate(transactionFinishDateExample),
|
||||
finishDate=Some(toDate(transactionFinishDateExample)),
|
||||
balance=BigDecimal(balanceExample.value),
|
||||
status=transactionStatusExample.value))
|
||||
),
|
||||
|
||||
@ -1479,7 +1479,7 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable {
|
||||
currency=currencyExample.value,
|
||||
description=Some(transactionDescriptionExample.value),
|
||||
startDate=toDate(transactionStartDateExample),
|
||||
finishDate=toDate(transactionFinishDateExample),
|
||||
finishDate=Some(toDate(transactionFinishDateExample)),
|
||||
balance=BigDecimal(balanceExample.value),
|
||||
status=transactionStatusExample.value)))
|
||||
),
|
||||
@ -1613,7 +1613,7 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable {
|
||||
currency=currencyExample.value,
|
||||
description=Some(transactionDescriptionExample.value),
|
||||
startDate=toDate(transactionStartDateExample),
|
||||
finishDate=toDate(transactionFinishDateExample),
|
||||
finishDate=Some(toDate(transactionFinishDateExample)),
|
||||
balance=BigDecimal(balanceExample.value),
|
||||
status=transactionStatusExample.value))
|
||||
),
|
||||
|
||||
@ -184,9 +184,14 @@ trait ConsentTrait {
|
||||
def transactionToDateTime: Date
|
||||
|
||||
/**
|
||||
* this will be a UUID later. now only use the primacyKey.toString for it.
|
||||
* this will be a UUID later. now only use the primacyKey.toString for it.
|
||||
*/
|
||||
def consentReferenceId: String
|
||||
|
||||
/**
|
||||
* Note about any important consent information.
|
||||
*/
|
||||
def note: String
|
||||
}
|
||||
|
||||
object ConsentStatus extends Enumeration {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package code.consent
|
||||
|
||||
import java.util.Date
|
||||
import code.api.util.{APIUtil, Consent, ErrorMessages, OBPBankId, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPStatus, OBPUserId, SecureRandomUtil}
|
||||
import code.api.util.{APIUtil, Consent, ErrorMessages, OBPBankId, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPSortBy, OBPStatus, OBPUserId, SecureRandomUtil}
|
||||
import code.consent.ConsentStatus.ConsentStatus
|
||||
import code.model.Consumer
|
||||
import code.util.MappedUUID
|
||||
@ -73,7 +73,23 @@ object MappedConsentProvider extends ConsentProvider {
|
||||
val consentId = queryParams.collectFirst { case OBPConsentId(value) => By(MappedConsent.mConsentId, value) }
|
||||
val userId = queryParams.collectFirst { case OBPUserId(value) => By(MappedConsent.mUserId, value) }
|
||||
val status = queryParams.collectFirst {
|
||||
case OBPStatus(value) => ByList(MappedConsent.mStatus, List(value.toLowerCase(), value.toUpperCase()))
|
||||
case OBPStatus(value) =>
|
||||
// Split the comma-separated string into a List, and trim whitespace from each element
|
||||
val statuses: List[String] = value.split(",").toList.map(_.trim)
|
||||
|
||||
// For each distinct status:
|
||||
// - create both lowercase ancheckIsLockedd uppercase versions
|
||||
// - flatten the resulting list of lists into a single list
|
||||
// - remove duplicates from the final list
|
||||
val distinctLowerAndUpperCaseStatuses: List[String] =
|
||||
statuses.distinct // Remove duplicates (case-sensitive)
|
||||
.flatMap(s => List( // For each element, generate:
|
||||
s.toLowerCase, // - lowercase version
|
||||
s.toUpperCase // - uppercase version
|
||||
))
|
||||
.distinct // Remove any duplicates caused by lowercase/uppercase repetition
|
||||
|
||||
ByList(MappedConsent.mStatus, distinctLowerAndUpperCaseStatuses)
|
||||
}
|
||||
|
||||
Seq(
|
||||
@ -86,14 +102,60 @@ object MappedConsentProvider extends ConsentProvider {
|
||||
).flatten
|
||||
}
|
||||
|
||||
private def sortConsents(consents: List[MappedConsent], sortByParam: String): List[MappedConsent] = {
|
||||
// Parse sort_by param like "created_date:desc,status:asc,consumer_id:asc"
|
||||
val sortFields: List[(String, String)] = sortByParam
|
||||
.split(",")
|
||||
.toList
|
||||
.map(_.trim)
|
||||
.filter(_.nonEmpty)
|
||||
.map { fieldSpec =>
|
||||
val parts = fieldSpec.split(":").map(_.trim.toLowerCase)
|
||||
val fieldName = parts(0)
|
||||
val sortOrder = parts.lift(1).getOrElse("asc") // default to asc
|
||||
(fieldName, sortOrder)
|
||||
}
|
||||
|
||||
// Apply sorting in reverse order, so first field becomes the last sort (because sortBy is stable)
|
||||
sortFields.reverse.foldLeft(consents) { case (acc, (fieldName, sortOrder)) =>
|
||||
val ascending = sortOrder != "desc"
|
||||
|
||||
fieldName match {
|
||||
case "created_date" =>
|
||||
if (ascending)
|
||||
acc.sortBy(_.createdAt.get)
|
||||
else
|
||||
acc.sortBy(_.createdAt.get)(Ordering[java.util.Date].reverse)
|
||||
|
||||
case "status" =>
|
||||
if (ascending)
|
||||
acc.sortBy(_.status)(Ordering[String])
|
||||
else
|
||||
acc.sortBy(_.status)(Ordering[String].reverse)
|
||||
|
||||
case "consumer_id" =>
|
||||
if (ascending)
|
||||
acc.sortBy(_.consumerId)(Ordering[String])
|
||||
else
|
||||
acc.sortBy(_.consumerId)(Ordering[String].reverse)
|
||||
|
||||
case _ =>
|
||||
// Unknown field → ignore
|
||||
acc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override def getConsents(queryParams: List[OBPQueryParam]): List[MappedConsent] = {
|
||||
val optionalParams = getQueryParams(queryParams)
|
||||
val sortBy: Option[String] = queryParams.collectFirst { case OBPSortBy(value) => value }
|
||||
val consents = MappedConsent.findAll(optionalParams: _*)
|
||||
val bankId: Option[String] = queryParams.collectFirst { case OBPBankId(value) => value }
|
||||
if(bankId.isDefined) {
|
||||
Consent.filterStrictlyByBank(consents, bankId.get)
|
||||
} else {
|
||||
consents
|
||||
sortConsents(consents, sortBy.getOrElse(""))
|
||||
}
|
||||
}
|
||||
override def createObpConsent(user: User, challengeAnswer: String, consentRequestId:Option[String], consumer: Option[Consumer]): Box[MappedConsent] = {
|
||||
@ -335,6 +397,7 @@ class MappedConsent extends ConsentTrait with LongKeyedMapper[MappedConsent] wit
|
||||
object mTransactionFromDateTime extends MappedDateTime(this)
|
||||
object mTransactionToDateTime extends MappedDateTime(this)
|
||||
object mStatusUpdateDateTime extends MappedDateTime(this)
|
||||
object mNote extends MappedText(this)
|
||||
|
||||
override def consentId: String = mConsentId.get
|
||||
override def userId: String = mUserId.get
|
||||
@ -364,6 +427,7 @@ class MappedConsent extends ConsentTrait with LongKeyedMapper[MappedConsent] wit
|
||||
override def creationDateTime= createdAt.get
|
||||
override def statusUpdateDateTime= mStatusUpdateDateTime.get
|
||||
override def consentReferenceId = id.get.toString
|
||||
override def note = mNote.get
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package code.management
|
||||
|
||||
|
||||
import java.util.Date
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.util.{APIUtil, CustomJsonFormats}
|
||||
import code.api.util.APIUtil.DateWithMsExampleObject
|
||||
import code.bankconnectors.{Connector, LocalMappedConnectorInternal}
|
||||
import code.tesobe.ErrorMessage
|
||||
import code.util.Helper.{MdcLoggable, ObpS}
|
||||
@ -93,7 +95,7 @@ object ImporterAPI extends RestHelper with MdcLoggable {
|
||||
val detailsJson = JObject(List( JField("type_en", JString(t.transactionType)),
|
||||
JField("type", JString(t.transactionType)),
|
||||
JField("posted", JString(formatDate(t.startDate))),
|
||||
JField("completed", JString(formatDate(t.finishDate))),
|
||||
JField("completed", JString(formatDate(t.finishDate.getOrElse(DateWithMsExampleObject)))),
|
||||
JField("other_data", JString("")),
|
||||
JField("new_balance", JObject(List( JField("currency", JString(t.currency)),
|
||||
JField("amount", JString(t.balance.toString))))),
|
||||
|
||||
@ -169,7 +169,7 @@ case class ViewExtended(val view: View) {
|
||||
else None
|
||||
|
||||
val transactionFinishDate =
|
||||
if (viewPermissions.exists(_ == CAN_SEE_TRANSACTION_FINISH_DATE)) Some(transaction.finishDate)
|
||||
if (viewPermissions.exists(_ == CAN_SEE_TRANSACTION_FINISH_DATE)) transaction.finishDate
|
||||
else None
|
||||
|
||||
val transactionBalance =
|
||||
|
||||
@ -8,11 +8,15 @@ import com.openbankproject.commons.util.{ApiStandards, ApiVersion}
|
||||
import net.liftweb.common.Full
|
||||
import net.liftweb.mapper.{By, By_<}
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
|
||||
object ConsentScheduler extends MdcLoggable {
|
||||
val dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.ENGLISH)
|
||||
def currentDate = dateFormat.format(new Date())
|
||||
|
||||
// Starts multiple scheduled tasks with different intervals
|
||||
def startAll(): Unit = {
|
||||
@ -61,8 +65,13 @@ object ConsentScheduler extends MdcLoggable {
|
||||
|
||||
outdatedConsents.foreach { consent =>
|
||||
Try {
|
||||
consent.mStatus(ConsentStatus.rejected.toString).save
|
||||
logger.warn(s"|---> Changed status to ${ConsentStatus.rejected.toString} for consent ID: ${consent.id}")
|
||||
val message = s"|---> Changed status from ${consent.status} to ${ConsentStatus.rejected} for consent ID: ${consent.id}"
|
||||
consent
|
||||
.mStatus(ConsentStatus.rejected.toString)
|
||||
.mNote(s"$currentDate\n$message")
|
||||
.mStatusUpdateDateTime(new Date())
|
||||
.save
|
||||
logger.warn(message)
|
||||
} match {
|
||||
case Failure(ex) => logger.error(s"Failed to update consent ID: ${consent.id}", ex)
|
||||
case Success(_) => // Already logged
|
||||
@ -87,8 +96,13 @@ object ConsentScheduler extends MdcLoggable {
|
||||
|
||||
expiredConsents.foreach { consent =>
|
||||
Try {
|
||||
consent.mStatus(ConsentStatus.expired.toString).save
|
||||
logger.warn(s"|---> Changed status to ${ConsentStatus.expired.toString} for consent ID: ${consent.id}")
|
||||
val message = s"|---> Changed status from ${consent.status} to ${ConsentStatus.expired} for consent ID: ${consent.id}"
|
||||
consent
|
||||
.mStatus(ConsentStatus.expired.toString)
|
||||
.mNote(s"$currentDate\n$message")
|
||||
.mStatusUpdateDateTime(new Date())
|
||||
.save
|
||||
logger.warn(message)
|
||||
} match {
|
||||
case Failure(ex) => logger.error(s"Failed to update consent ID: ${consent.id}", ex)
|
||||
case Success(_) => // Already logged
|
||||
@ -113,8 +127,13 @@ object ConsentScheduler extends MdcLoggable {
|
||||
|
||||
expiredConsents.foreach { consent =>
|
||||
Try {
|
||||
consent.mStatus(ConsentStatus.EXPIRED.toString).save
|
||||
logger.warn(s"|---> Changed status to ${ConsentStatus.EXPIRED.toString} for consent ID: ${consent.id}")
|
||||
val message = s"|---> Changed status from ${consent.status} to ${ConsentStatus.EXPIRED.toString} for consent ID: ${consent.id}"
|
||||
consent
|
||||
.mStatus(ConsentStatus.EXPIRED.toString)
|
||||
.mNote(s"$currentDate\n$message")
|
||||
.mStatusUpdateDateTime(new Date())
|
||||
.save
|
||||
logger.warn(message)
|
||||
} match {
|
||||
case Failure(ex) => logger.error(s"Failed to update consent ID: ${consent.id}", ex)
|
||||
case Success(_) => // Already logged
|
||||
|
||||
@ -154,7 +154,7 @@ class MappedTransaction extends LongKeyedMapper[MappedTransaction] with IdPK wit
|
||||
transactionCurrency,
|
||||
transactionDescription,
|
||||
tStartDate.get,
|
||||
tFinishDate.get,
|
||||
Some(tFinishDate.get),
|
||||
newBalance,
|
||||
status.get))
|
||||
}
|
||||
|
||||
@ -334,7 +334,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Match
|
||||
}
|
||||
|
||||
foundTransaction.startDate.getTime should equal(toDate(transaction.details.posted).getTime)
|
||||
foundTransaction.finishDate.getTime should equal(toDate(transaction.details.completed).getTime)
|
||||
foundTransaction.finishDate.head.getTime should equal(toDate(transaction.details.completed).getTime)
|
||||
|
||||
//a counterparty should exist
|
||||
val otherAcc = foundTransaction.otherAccount
|
||||
|
||||
89
obp-api/src/test/scala/code/api/v5_1_0/WebUiPropsTest.scala
Normal file
89
obp-api/src/test/scala/code/api/v5_1_0/WebUiPropsTest.scala
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
Open Bank Project - API
|
||||
Copyright (C) 2011-2019, TESOBE GmbH
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Email: contact@tesobe.com
|
||||
TESOBE GmbH
|
||||
Osloerstrasse 16/17
|
||||
Berlin 13359, Germany
|
||||
|
||||
This product includes software developed at
|
||||
TESOBE (http://www.tesobe.com/)
|
||||
*/
|
||||
package code.api.v5_1_0
|
||||
|
||||
import code.api.util.APIUtil.OAuth._
|
||||
import code.api.util.ApiRole._
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0
|
||||
import code.entitlement.Entitlement
|
||||
import code.webuiprops.WebUiPropsCommons
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.model.ErrorMessage
|
||||
import com.openbankproject.commons.util.ApiVersion
|
||||
import net.liftweb.json.Serialization.write
|
||||
import org.scalatest.Tag
|
||||
|
||||
|
||||
class WebUiPropsTest extends V510ServerSetup {
|
||||
|
||||
/**
|
||||
* Test tags
|
||||
* Example: To run tests with tag "getPermissions":
|
||||
* mvn test -D tagsToInclude
|
||||
*
|
||||
* This is made possible by the scalatest maven plugin
|
||||
*/
|
||||
object VersionOfApi extends Tag(ApiVersion.v5_1_0.toString)
|
||||
object ApiEndpoint extends Tag(nameOf(Implementations5_1_0.getWebUiProps))
|
||||
|
||||
val rightEntity = WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com")
|
||||
val wrongEntity = WebUiPropsCommons("hello_api_explorer_url", "https://apiexplorer.openbankproject.com") // name not start with "webui_"
|
||||
|
||||
|
||||
feature("Get WebUiPropss v5.1.0 ") {
|
||||
|
||||
scenario("successful case", VersionOfApi) {
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString)
|
||||
When("We make a request v3.1.0")
|
||||
val request510 = (v5_1_0_Request / "management" / "webui_props").POST <@(user1)
|
||||
val response510 = makePostRequest(request510, write(rightEntity))
|
||||
Then("We should get a 201")
|
||||
response510.code should equal(201)
|
||||
val customerJson = response510.body.extract[WebUiPropsCommons]
|
||||
|
||||
val requestGet510 = (v5_1_0_Request / "webui-props").GET
|
||||
val responseGet510 = makeGetRequest(requestGet510)
|
||||
Then("We should get a 200")
|
||||
responseGet510.code should equal(200)
|
||||
val json = responseGet510.body \ "webui_props"
|
||||
val webUiPropssGetJson = json.extract[List[WebUiPropsCommons]]
|
||||
|
||||
webUiPropssGetJson.size should be (1)
|
||||
|
||||
val requestGet510AddedQueryParameter = requestGet510.addQueryParameter("active", "true")
|
||||
val responseGet510AddedQueryParameter = makeGetRequest(requestGet510AddedQueryParameter)
|
||||
Then("We should get a 200")
|
||||
responseGet510AddedQueryParameter.code should equal(200)
|
||||
val responseJson = responseGet510AddedQueryParameter.body \ "webui_props"
|
||||
val responseGet510AddedQueryParameterJson = responseJson.extract[List[WebUiPropsCommons]]
|
||||
responseGet510AddedQueryParameterJson.size >1 should be (true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Binary file not shown.
@ -145,7 +145,7 @@ class ImporterTest extends ServerSetup with MdcLoggable with DefaultConnectorTes
|
||||
|
||||
//compare time as a long to avoid issues comparing Dates, e.g. java.util.Date vs java.sql.Date
|
||||
t.startDate.getTime should equal(importJsonDateFormat.parse(startDate).getTime)
|
||||
t.finishDate.getTime should equal(importJsonDateFormat.parse(endDate).getTime)
|
||||
t.finishDate.head.getTime should equal(importJsonDateFormat.parse(endDate).getTime)
|
||||
}
|
||||
|
||||
scenario("Attempting to import transactions without using a secret key") {
|
||||
|
||||
@ -1138,7 +1138,7 @@ case class Transaction(
|
||||
// The date the transaction was initiated
|
||||
startDate : Date,
|
||||
// The date when the money finished changing hands
|
||||
finishDate : Date,
|
||||
finishDate : Option[Date],
|
||||
//the new balance for the bank account
|
||||
balance : BigDecimal,
|
||||
status: String
|
||||
|
||||
Loading…
Reference in New Issue
Block a user