Merge pull request #2505 from constantine2nd/develop

A few tweaks
This commit is contained in:
Simon Redfern 2025-03-12 14:37:07 +01:00 committed by GitHub
commit 1f8e5bcb57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 195 additions and 172 deletions

View File

@ -56,7 +56,7 @@ object APIMethods_AccountInformationServiceAISApi extends RestHelper {
getConsentStatus ::
getTransactionDetails ::
getTransactionList ::
readAccountDetails ::
getAccountDetails ::
readCardAccount ::
startConsentAuthorisationTransactionAuthorisation ::
startConsentAuthorisationUpdatePsuAuthentication ::
@ -78,7 +78,7 @@ object APIMethods_AccountInformationServiceAISApi extends RestHelper {
unboxFullOrFail(_, callContext, s"$NoViewReadAccountsBerlinGroup ${viewId.value} userId : ${u.userId}. account : ${account.accountId}", 403)
}
}
resourceDocs += ResourceDoc(
createConsent,
apiVersion,
@ -87,22 +87,22 @@ object APIMethods_AccountInformationServiceAISApi extends RestHelper {
"/consents",
"Create consent",
s"""${mockedDataText(false)}
This method create a consent resource, defining access rights to dedicated accounts of
a given PSU-ID. These accounts are addressed explicitly in the method as
This method create a consent resource, defining access rights to dedicated accounts of
a given PSU-ID. These accounts are addressed explicitly in the method as
parameters as a core function.
**Side Effects**
When this Consent Request is a request where the "recurringIndicator" equals "true",
and if it exists already a former consent for recurring access on account information
for the addressed PSU, then the former consent automatically expires as soon as the new
When this Consent Request is a request where the "recurringIndicator" equals "true",
and if it exists already a former consent for recurring access on account information
for the addressed PSU, then the former consent automatically expires as soon as the new
consent request is authorised by the PSU.
Optional Extension:
As an option, an ASPSP might optionally accept a specific access right on the access on all psd2 related services for all available accounts.
As an option, an ASPSP might optionally accept a specific access right on the access on all psd2 related services for all available accounts.
As another option an ASPSP might optionally also accept a command, where only access rights are inserted without mentioning the addressed account.
The relation to accounts is then handled afterwards between PSU and ASPSP.
This option is not supported for the Embedded SCA Approach.
As another option an ASPSP might optionally also accept a command, where only access rights are inserted without mentioning the addressed account.
The relation to accounts is then handled afterwards between PSU and ASPSP.
This option is not supported for the Embedded SCA Approach.
As a last option, an ASPSP might in addition accept a command with access rights
* to see the list of available payment accounts or
* to see the list of available payment accounts with balances.
@ -164,9 +164,9 @@ As a last option, an ASPSP might in addition accept a command with access rights
validUntil <- NewStyle.function.tryons(failMsg, 400, callContext) {
new SimpleDateFormat(DateWithDay).parse(consentJson.validUntil)
}
_ <- NewStyle.function.getBankAccountsByIban(consentJson.access.accounts.getOrElse(Nil).map(_.iban.getOrElse("")), callContext)
createdConsent <- Future(Consents.consentProvider.vend.createBerlinGroupConsent(
createdByUser,
callContext.flatMap(_.consumer),
@ -194,7 +194,7 @@ As a last option, an ASPSP might in addition accept a command with access rights
_ <- Future(Consents.consentProvider.vend.setJsonWebToken(createdConsent.consentId, consentJWT)) map {
i => connectorEmptyResponse(i, callContext)
}
/* _ <- Future(Authorisations.authorisationProvider.vend.createAuthorization(
"",
createdConsent.consentId,
@ -210,7 +210,7 @@ As a last option, an ASPSP might in addition accept a command with access rights
}
}
}
resourceDocs += ResourceDoc(
deleteConsent,
apiVersion,
@ -243,7 +243,7 @@ As a last option, an ASPSP might in addition accept a command with access rights
}
}
}
resourceDocs += ResourceDoc(
getAccountList,
apiVersion,
@ -252,21 +252,21 @@ As a last option, an ASPSP might in addition accept a command with access rights
"/accounts",
"Read Account List",
s"""${mockedDataText(false)}
Read the identifiers of the available payment account together with
Read the identifiers of the available payment account together with
booking balance information, depending on the consent granted.
It is assumed that a consent of the PSU to this access is already given and stored on the ASPSP system.
The addressed list of accounts depends then on the PSU ID and the stored consent addressed by consentId,
respectively the OAuth2 access token.
It is assumed that a consent of the PSU to this access is already given and stored on the ASPSP system.
The addressed list of accounts depends then on the PSU ID and the stored consent addressed by consentId,
respectively the OAuth2 access token.
Returns all identifiers of the accounts, to which an account access has been granted to through
the /consents endpoint by the PSU.
In addition, relevant information about the accounts and hyperlinks to corresponding account
Returns all identifiers of the accounts, to which an account access has been granted to through
the /consents endpoint by the PSU.
In addition, relevant information about the accounts and hyperlinks to corresponding account
information resources are provided if a related consent has been already granted.
Remark: Note that the /consents endpoint optionally offers to grant an access on all available
payment accounts of a PSU.
In this case, this endpoint will deliver the information about all available payment accounts
Remark: Note that the /consents endpoint optionally offers to grant an access on all available
payment accounts of a PSU.
In this case, this endpoint will deliver the information about all available payment accounts
of the PSU at this ASPSP.
""",
EmptyBody,
@ -320,7 +320,7 @@ of the PSU at this ASPSP.
attribute.`type`.equals("STRING")&&
attribute.value.equalsIgnoreCase("card")
).isEmpty)
} yield {
(JSONFactory_BERLIN_GROUP_1_3.createAccountListJson(
bankAccountsFiltered,
@ -331,7 +331,7 @@ of the PSU at this ASPSP.
}
}
}
resourceDocs += ResourceDoc(
getBalances,
apiVersion,
@ -340,10 +340,10 @@ of the PSU at this ASPSP.
"/accounts/ACCOUNT_ID/balances",
"Read Balance",
s"""${mockedDataText(false)}
Reads account data from a given account addressed by "account-id".
Reads account data from a given account addressed by "account-id".
**Remark:** This account-id can be a tokenised identification due to data protection reason since the path
information might be logged on intermediary servers within the ASPSP sphere.
**Remark:** This account-id can be a tokenised identification due to data protection reason since the path
information might be logged on intermediary servers within the ASPSP sphere.
This account-id then can be retrieved by the "GET Account List" call.
The account-id is constant at least throughout the lifecycle of a given consent.
@ -383,7 +383,7 @@ The account-id is constant at least throughout the lifecycle of a given consent.
}
}
}
resourceDocs += ResourceDoc(
getCardAccounts,
apiVersion,
@ -392,10 +392,10 @@ The account-id is constant at least throughout the lifecycle of a given consent.
"/card-accounts",
"Reads a list of card accounts",
s"""${mockedDataText(false)}
Reads a list of card accounts with additional information, e.g. balance information.
It is assumed that a consent of the PSU to this access is already given and stored on the ASPSP system.
The addressed list of card accounts depends then on the PSU ID and the stored consent addressed by consentId,
respectively the OAuth2 access token.
Reads a list of card accounts with additional information, e.g. balance information.
It is assumed that a consent of the PSU to this access is already given and stored on the ASPSP system.
The addressed list of card accounts depends then on the PSU ID and the stored consent addressed by consentId,
respectively the OAuth2 access token.
""",
EmptyBody,
json.parse("""{
@ -434,19 +434,19 @@ respectively the OAuth2 access token.
(_, callContext) <- NewStyle.function.getPhysicalCardsForUser(u, callContext)
(accounts, callContext) <- NewStyle.function.getBankAccounts(availablePrivateAccounts, callContext)
//also see `getAccountList` endpoint
bankAccountsFiltered = accounts.filter(bankAccount =>
bankAccount.attributes.toList.flatten.find(attribute=>
attribute.name.equals("CashAccountTypeCode")&&
bankAccountsFiltered = accounts.filter(bankAccount =>
bankAccount.attributes.toList.flatten.find(attribute=>
attribute.name.equals("CashAccountTypeCode")&&
attribute.`type`.equals("STRING")&&
attribute.value.equalsIgnoreCase("card")
attribute.value.equalsIgnoreCase("card")
).isDefined)
} yield {
(JSONFactory_BERLIN_GROUP_1_3.createCardAccountListJson(bankAccountsFiltered, u), callContext)
}
}
}
resourceDocs += ResourceDoc(
getCardAccountBalances,
apiVersion,
@ -455,13 +455,13 @@ respectively the OAuth2 access token.
"/card-accounts/ACCOUNT_ID/balances",
"Read card account balances",
s"""${mockedDataText(false)}
Reads balance data from a given card account addressed by
"account-id".
Reads balance data from a given card account addressed by
"account-id".
Remark: This account-id can be a tokenised identification due
to data protection reason since the path information might be
logged on intermediary servers within the ASPSP sphere.
This account-id then can be retrieved by the
Remark: This account-id can be a tokenised identification due
to data protection reason since the path information might be
logged on intermediary servers within the ASPSP sphere.
This account-id then can be retrieved by the
"GET Card Account List" call
""",
EmptyBody,
@ -498,7 +498,7 @@ This account-id then can be retrieved by the
}
}
}
resourceDocs += ResourceDoc(
getCardAccountTransactionList,
apiVersion,
@ -596,7 +596,7 @@ Reads account data from a given card account addressed by "account-id".
}
}
}
resourceDocs += ResourceDoc(
getConsentAuthorisation,
apiVersion,
@ -629,7 +629,7 @@ This function returns an array of hyperlinks to all generated authorisation sub-
}
}
}
resourceDocs += ResourceDoc(
getConsentInformation,
apiVersion,
@ -638,8 +638,8 @@ This function returns an array of hyperlinks to all generated authorisation sub-
"/consents/CONSENTID",
"Get Consent Request",
s"""${mockedDataText(false)}
Returns the content of an account information consent object.
This is returning the data for the TPP especially in cases,
Returns the content of an account information consent object.
This is returning the data for the TPP especially in cases,
where the consent was directly managed between ASPSP and PSU e.g. in a re-direct SCA Approach.
""",
EmptyBody,
@ -702,7 +702,7 @@ where the consent was directly managed between ASPSP and PSU e.g. in a re-direct
.replace(ConsentStatus.REJECTED.toString, "failed")
scaStatus
}
resourceDocs += ResourceDoc(
getConsentScaStatus,
apiVersion,
@ -738,7 +738,7 @@ This method returns the SCA status of a consent initiation's authorisation sub-r
}
}
}
resourceDocs += ResourceDoc(
getConsentStatus,
apiVersion,
@ -770,22 +770,22 @@ This method returns the SCA status of a consent initiation's authorisation sub-r
.replace(ConsentStatus.REVOKED.toString.toLowerCase(), "revokedByPsu")
(JSONFactory_BERLIN_GROUP_1_3.ConsentStatusJsonV13(status), HttpCode.`200`(callContext))
}
}
}
resourceDocs += ResourceDoc(
getTransactionDetails,
apiVersion,
nameOf(getTransactionDetails),
"GET",
"/accounts/ACCOUNT_ID/transactions/TRANSACTIONID",
"GET",
"/accounts/ACCOUNT_ID/transactions/TRANSACTIONID",
"Read Transaction Details",
s"""${mockedDataText(false)}
Reads transaction details from a given transaction addressed by "transactionId" on a given account addressed
by "account-id". This call is only available on transactions as reported in a JSON format.
Reads transaction details from a given transaction addressed by "transactionId" on a given account addressed
by "account-id". This call is only available on transactions as reported in a JSON format.
**Remark:** Please note that the PATH might be already given in detail by the corresponding entry of the response
**Remark:** Please note that the PATH might be already given in detail by the corresponding entry of the response
of the "Read Transaction List" call within the _links subfield.
""",
@ -932,20 +932,20 @@ The ASPSP might add balance information, if transaction lists without balances a
}
}
}
resourceDocs += ResourceDoc(
readAccountDetails,
getAccountDetails,
apiVersion,
nameOf(readAccountDetails),
nameOf(getAccountDetails),
"GET",
"/accounts/ACCOUNT_ID",
"Read Account Details",
s"""${mockedDataText(false)}
Reads details about an account, with balances where required.
It is assumed that a consent of the PSU to this access is already given and stored on the ASPSP system.
The addressed details of this account depends then on the stored consent addressed by consentId,
respectively the OAuth2 access token. **NOTE:** The account-id can represent a multicurrency account.
In this case the currency code is set to "XXX". Give detailed information about the addressed account.
Reads details about an account, with balances where required.
It is assumed that a consent of the PSU to this access is already given and stored on the ASPSP system.
The addressed details of this account depends then on the stored consent addressed by consentId,
respectively the OAuth2 access token. **NOTE:** The account-id can represent a multicurrency account.
In this case the currency code is set to "XXX". Give detailed information about the addressed account.
Give detailed information about the addressed account together with balance information
""",
@ -972,7 +972,7 @@ Give detailed information about the addressed account together with balance info
ApiTag("Account Information Service (AIS)") :: apiTagBerlinGroupM :: Nil
)
lazy val readAccountDetails : OBPEndpoint = {
lazy val getAccountDetails : OBPEndpoint = {
case "accounts" :: accountId :: Nil JsonGet _ => {
cc =>
for {

View File

@ -105,9 +105,9 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats {
case class AccountBalance(
balanceAmount : AmountOfMoneyV13 = AmountOfMoneyV13("EUR","123"),
balanceType: String = "closingBooked",
lastChangeDateTime: String = "2020-07-02T10:23:57.814Z",
lastCommittedTransaction: String = "string",
referenceDate: String = "2020-07-02",
lastChangeDateTime: Option[String] = None,
lastCommittedTransaction: Option[String] = None,
referenceDate: Option[String] = None,
)
case class FromAccount(
@ -315,8 +315,8 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats {
x =>
val (iBan: String, bBan: String) = getIbanAndBban(x)
val commonPath = s"${OBP_BERLIN_GROUP_1_3.apiVersion.urlPrefix}/${OBP_BERLIN_GROUP_1_3.version}/accounts/${x.accountId.value}"
val balanceRef = LinkHrefJson(s"/$commonPath/accounts/${x.accountId.value}/balances")
val transactionRef = LinkHrefJson(s"/$commonPath/accounts/${x.accountId.value}/transactions")
val balanceRef = LinkHrefJson(s"/$commonPath/balances")
val transactionRef = LinkHrefJson(s"/$commonPath/transactions")
val canReadTransactions = canReadTransactionsAccounts.map(_.accountId.value).contains(x.accountId.value)
@ -401,9 +401,8 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats {
`balances` = accountBalances.balances.map(accountBalance => AccountBalance(
balanceAmount = AmountOfMoneyV13(accountBalance.balance.currency, accountBalance.balance.amount),
balanceType = accountBalance.balanceType,
lastChangeDateTime = APIUtil.dateOrNull(bankAccount.lastUpdate),
referenceDate = APIUtil.dateOrNull(bankAccount.lastUpdate),
lastCommittedTransaction = "String"
lastChangeDateTime = APIUtil.dateOrNone(bankAccount.lastUpdate),
referenceDate = APIUtil.dateOrNone(bankAccount.lastUpdate),
)
))
}

View File

@ -930,11 +930,11 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
case _ => OBPId
}
def dateOrNull(date : Date) =
def dateOrNone(date : Date): Option[String] =
if(date == null)
null
None
else
APIUtil.DateWithMsRollback.format(date)
Some(APIUtil.DateWithMsRollback.format(date))
def stringOrNull(text : String) =
if(text == null || text.isEmpty)
@ -2402,59 +2402,52 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
// Function checks does a user specified by a parameter userId has at least one role provided by a parameter roles at a bank specified by a parameter bankId
// i.e. does user has assigned at least one role from the list
// when roles is empty, that means no access control, treat as pass auth check
def handleEntitlementsAndScopes(bankId: String, userId: String, consumerId: String, roles: List[ApiRole]): Boolean = {
val requireScopesForListedRoles: List[String] = getPropsValue("require_scopes_for_listed_roles", "").split(",").toList
val requireScopesForRoles: immutable.Seq[String] = roles.map(_.toString()) intersect requireScopesForListedRoles
def handleAccessControlRegardingEntitlementsAndScopes(bankId: String, userId: String, consumerId: String, roles: List[ApiRole]): Boolean = {
if (roles.isEmpty) { // No access control, treat as pass auth check
true
} else {
val requireScopesForListedRoles = getPropsValue("require_scopes_for_listed_roles", "").split(",").toSet
val requireScopesForRoles = roles.map(_.toString).toSet.intersect(requireScopesForListedRoles)
def userHasTheRoles: Boolean = {
val userHasTheRole: Boolean = roles.isEmpty || roles.exists(hasEntitlement(bankId, userId, _))
userHasTheRole match {
case true => userHasTheRole // Just forward
case false =>
// If a user is trying to use a Role and the user could grant them selves the required Role(s),
// then just automatically grant the Role(s)!
getPropsAsBoolValue("create_just_in_time_entitlements", false) match {
case false => userHasTheRole // Just forward
case true => // Try to add missing roles
if (hasEntitlement(bankId, userId, ApiRole.canCreateEntitlementAtOneBank) ||
hasEntitlement("", userId, ApiRole.canCreateEntitlementAtAnyBank)) {
// Add missing roles
roles.map {
role =>
val addedEntitlement = Entitlement.entitlement.vend.addEntitlement(
bankId,
userId,
role.toString(),
"create_just_in_time_entitlements"
)
logger.info(s"Just in Time Entitlements: $addedEntitlement")
addedEntitlement
}.forall(_.isDefined)
} else {
userHasTheRole // Just forward
def userHasTheRoles: Boolean = {
val userHasTheRole: Boolean = roles.exists(hasEntitlement(bankId, userId, _))
userHasTheRole || {
getPropsAsBoolValue("create_just_in_time_entitlements", false) && {
// If a user is trying to use a Role and the user could grant them selves the required Role(s),
// then just automatically grant the Role(s)!
(hasEntitlement(bankId, userId, ApiRole.canCreateEntitlementAtOneBank) ||
hasEntitlement("", userId, ApiRole.canCreateEntitlementAtAnyBank)) &&
roles.forall { role =>
val addedEntitlement = Entitlement.entitlement.vend.addEntitlement(
bankId,
userId,
role.toString,
"create_just_in_time_entitlements"
)
logger.info(s"Just in Time Entitlements: $addedEntitlement")
addedEntitlement.isDefined
}
}
}
}
// Consumer AND User has the Role
if (ApiPropsWithAlias.requireScopesForAllRoles || requireScopesForRoles.nonEmpty) {
userHasTheRoles && roles.exists(hasScope(bankId, consumerId, _))
}
// Consumer OR User has the Role
else if (getPropsAsBoolValue("allow_entitlements_or_scopes", false)) {
roles.exists(role => hasScope(if (role.requiresBankId) bankId else "", consumerId, role)) || userHasTheRoles
}
// User has the Role
else {
userHasTheRoles
}
}
// Consumer AND User has the Role
if(ApiPropsWithAlias.requireScopesForAllRoles || !requireScopesForRoles.isEmpty) {
roles.isEmpty || (userHasTheRoles && roles.exists(hasScope(bankId, consumerId, _)))
}
// Consumer OR User has the Role
else if(getPropsAsBoolValue("allow_entitlements_or_scopes", false)) {
roles.isEmpty ||
userHasTheRoles ||
roles.exists(role => hasScope(if (role.requiresBankId) bankId else "", consumerId, role))
}
// User has the Role
else {
userHasTheRoles
}
}
// Function checks does a user specified by a parameter userId has all roles provided by a parameter roles at a bank specified by a parameter bankId
// i.e. does user has assigned all roles from the list
// when roles is empty, that means no access control, treat as pass auth check
@ -2686,7 +2679,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
def checkVersion: Boolean = {
val disabledVersions: List[String] = getDisabledVersions()
val enabledVersions: List[String] = getEnabledVersions()
if (// this is the short version: v4.0.0 this is for fullyQualifiedVersion: OBPv4.0.0
if (// this is the short version: v4.0.0 this is for fullyQualifiedVersion: OBPv4.0.0
(disabledVersions.find(disableVersion => (disableVersion == version.apiShortVersion || disableVersion == version.fullyQualifiedVersion )).isEmpty) &&
// Enabled versions or all
(enabledVersions.find(enableVersion => (enableVersion ==version.apiShortVersion || enableVersion == version.fullyQualifiedVersion)).isDefined || enabledVersions.isEmpty)
@ -2761,7 +2754,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
// Endpoint Operation Ids
val enabledEndpointOperationIds = getEnabledEndpointOperationIds
val routes = for (
item <- resourceDocs
@ -3010,12 +3003,12 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
case Some(consent) => // JWT value obtained via "Consent-Id" request header
Consent.applyRules(
Some(consent.jsonWebToken),
// Note: At this point we are getting the Consumer from the Consumer in the Consent.
// This may later be cross checked via the value in consumer_validation_method_for_consent.
// Note: At this point we are getting the Consumer from the Consumer in the Consent.
// This may later be cross checked via the value in consumer_validation_method_for_consent.
// Get the source of truth for Consumer (e.g. CONSUMER_CERTIFICATE) as early as possible.
cc.copy(consumer = Consent.getCurrentConsumerViaMtls(callContext = cc))
)
case _ =>
case _ =>
JwtUtil.checkIfStringIsJWTValue(consentValue.getOrElse("")).isDefined match {
case true => // It's JWT obtained via "Consent-JWT" request header
Consent.applyRules(APIUtil.getConsentJWT(reqHeaders), cc)
@ -3114,7 +3107,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
case _ =>
Future { (Failure(ErrorMessages.DAuthUnknownError), None) }
}
}
}
else if(Option(cc).flatMap(_.user).isDefined) {
Future{(cc.user, Some(cc))}
}
@ -3221,7 +3214,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
// val authUser = AuthUser.findUserByUsernameLocally(x._1.head.name).openOrThrowException("")
// tryo{AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(authUser, x._2)}.openOr(logger.error(s"${x._1} authenticatedAccess.grantEntitlementsToUseDynamicEndpointsInSpaces throw exception! "))
// make sure, if `refreshUserIfRequired` throw exception, do not break the `authenticatedAccess`,
// make sure, if `refreshUserIfRequired` throw exception, do not break the `authenticatedAccess`,
// TODO better move `refreshUserIfRequired` to other place.
// 2022-02-18 from now, we will put this method after user create UserAuthContext successfully.
// tryo{refreshUserIfRequired(x._1,x._2)}.openOr(logger.error(s"${x._1} authenticatedAccess.refreshUserIfRequired throw exception! "))
@ -3290,7 +3283,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
* @param cc Call Context of te current call
* @return Tuple (User, Call Context)
*/
def applicationAccess(cc: CallContext): Future[(Box[User], Option[CallContext])] =
def applicationAccess(cc: CallContext): Future[(Box[User], Option[CallContext])] =
getUserAndSessionContextFuture(cc) map { result =>
val url = result._2.map(_.url).getOrElse("None")
val verb = result._2.map(_.verb).getOrElse("None")
@ -3355,9 +3348,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
throw new Exception("Empty Box not allowed")
case obj1@ParamFailure(m,e,c,af: APIFailureNewStyle) =>
val obj = (m,e, c) match {
case ("", Empty, Empty) =>
case ("", Empty, Empty) =>
Empty ?~! af.translatedErrorMessage
case _ =>
case _ =>
Failure (m, e, c) ?~! af.translatedErrorMessage
}
val failuresMsg = filterMessage(obj)
@ -3377,7 +3370,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
throw new Exception(UnknownError)
}
}
def unboxFullAndWrapIntoFuture[T](box: Box[T])(implicit m: Manifest[T]) : Future[T] = {
Future {
unboxFull(fullBoxOrException(box))
@ -3471,7 +3464,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
//Note: this property name prefix is only used for system environment, not for Liftweb props.
val sysEnvironmentPropertyNamePrefix = Props.get("system_environment_property_name_prefix").openOr("OBP_")
//All the property will first check from system environment, if not find then from the liftweb props file
//All the property will first check from system environment, if not find then from the liftweb props file
//Replace "." with "_" (environment vars cannot include ".") and convert to upper case
// Append "OBP_" because all Open Bank Project environment vars are namespaced with OBP
val sysEnvironmentPropertyName = sysEnvironmentPropertyNamePrefix.concat(brandSpecificPropertyName.replace('.', '_').toUpperCase())
@ -3556,7 +3549,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
None
}
}
// TODO This function needs testing in a cluster environment
private def getActiveBrand(): Option[String] = {
val brandParameter = "brand"
@ -3644,7 +3637,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
* Note: The public views means you can use anonymous access which implies that the user is an optional value
*/
final def checkViewAccessAndReturnView(viewId : ViewId, bankIdAccountId: BankIdAccountId, user: Option[User], callContext: Option[CallContext]): Box[View] = {
val customView = MapperViews.customView(viewId, bankIdAccountId)
customView match { // CHECK CUSTOM VIEWS
// 1st: View is Pubic and Public views are NOT allowed on this instance.
@ -3674,7 +3667,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
}
final def checkAuthorisationToCreateTransactionRequest(viewId: ViewId, bankAccountId: BankIdAccountId, user: User, callContext: Option[CallContext]): Box[Boolean] = {
lazy val hasCanCreateAnyTransactionRequestRole = APIUtil.handleEntitlementsAndScopes(
lazy val hasCanCreateAnyTransactionRequestRole = APIUtil.handleAccessControlRegardingEntitlementsAndScopes(
bankAccountId.bankId.value,
user.userId,
APIUtil.getConsumerPrimaryKey(callContext),

View File

@ -1,19 +1,21 @@
package code.api.util
import code.api.{CertificateConstants, RequestHeader}
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model.User
import net.liftweb.common.{Box, Failure, Full}
import net.liftweb.http.provider.HTTPParam
import java.util.Base64
import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Paths}
import java.security._
import java.security.cert.{CertificateFactory, X509Certificate}
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec}
import java.util.Base64
import scala.util.matching.Regex
object BerlinGroupSigning {
object BerlinGroupSigning extends MdcLoggable {
// Step 1: Calculate Digest (SHA-256 Hash of the Body)
def calculateDigest(body: String): String = {
@ -35,9 +37,9 @@ object BerlinGroupSigning {
RequestHeader.Digest,
RequestHeader.Date,
RequestHeader.`X-Request-ID`,
RequestHeader.`TPP-Redirect-URL`,
//RequestHeader.`TPP-Redirect-URL`,
) // Example fields to be signed
orderedKeys.flatMap(headers.get).mkString(" ")
orderedKeys.flatMap(key => headers.get(key).map(value => s"${key.toLowerCase()}: $value")).mkString("\n")
}
// Step 3: Generate Signature using RSA Private Key
@ -62,14 +64,20 @@ object BerlinGroupSigning {
}
// Step 4: Attach Certificate (Load from PEM String)
def loadCertificate(certPem: String): String = {
def loadCertificate(certPem: String) = {
val certString = certPem
.replaceAll("-----BEGIN CERTIFICATE-----", "") // Remove the BEGIN header
.replaceAll("-----END CERTIFICATE-----", "") // Remove the END footer
.replaceAll("\\s", "") // Remove all whitespace and new lines
val certBytes = Base64.getDecoder.decode(certString)
Base64.getEncoder.encodeToString(certBytes)
// Decode Base64 public key
val keyBytes = Base64.getDecoder.decode(certPem)
val keySpec = new X509EncodedKeySpec(keyBytes)
val keyFactory = KeyFactory.getInstance("RSA")
val publicKey = keyFactory.generatePublic(keySpec)
publicKey
// val certBytes = Base64.getDecoder.decode(certString)
// Base64.getEncoder.encodeToString(certBytes)
}
// Step 5: Verify Request on ASPSP Side
@ -120,15 +128,19 @@ object BerlinGroupSigning {
X509.validate(certificatePem) match {
case Full(true) => // PEM certificate is ok
val digest = calculateDigest(body.getOrElse(""))
val headers = Map(
RequestHeader.Digest -> s"SHA-256=$digest",
RequestHeader.`X-Request-ID` -> getHeaderValue(RequestHeader.`X-Request-ID`, requestHeaders),
RequestHeader.Date -> getHeaderValue(RequestHeader.Date, requestHeaders),
RequestHeader.`TPP-Redirect-URL` -> getHeaderValue(RequestHeader.`TPP-Redirect-URL`, requestHeaders),
)
val signingString = createSigningString(headers)
val signatureHeaderValue = getHeaderValue(RequestHeader.Signature, requestHeaders)
val signature = parseSignatureHeader(signatureHeaderValue).getOrElse("signature", "NONE")
val headersss = parseSignatureHeader(signatureHeaderValue).getOrElse("headers", "").split(" ").toList
val headers = headersss.map(h =>
if(h.toLowerCase() == RequestHeader.Digest.toLowerCase()) {
s"$h: SHA-256=$digest"
} else {
s"$h: ${getHeaderValue(h, requestHeaders)}"
}
)
val signingString = headers.mkString("\n")
val isVerified = verifySignature(signingString, signature, certificatePem)
val isValidated = CertificateVerifier.validateCertificate(certificatePem)
(isVerified, isValidated) match {
@ -143,14 +155,33 @@ object BerlinGroupSigning {
}
def getHeaderValue(name: String, requestHeaders: List[HTTPParam]): String = {
requestHeaders.find(_.name == name).map(_.values.mkString).getOrElse("None")
requestHeaders.find(_.name.toLowerCase() == name.toLowerCase()).map(_.values.mkString).getOrElse("None")
}
def getPem(requestHeaders: List[HTTPParam]): String = {
val certificate = getHeaderValue(RequestHeader.`TPP-Signature-Certificate`, requestHeaders)
s"""${CertificateConstants.BEGIN_CERT}
|$certificate
|${CertificateConstants.END_CERT}
|""".stripMargin
// Decode the Base64 string
val decodedBytes = Base64.getDecoder.decode(certificate)
// Convert the bytes to a string (it could be PEM format for public key)
val decodedString = new String(decodedBytes, StandardCharsets.UTF_8)
// Extract the certificate portion from the decoded string
val certStart = "-----BEGIN CERTIFICATE-----"
val certEnd = "-----END CERTIFICATE-----"
// Find the start and end indices of the certificate
val startIndex = decodedString.indexOf(certStart)
val endIndex = decodedString.indexOf(certEnd, startIndex) + certEnd.length
if (startIndex >= 0 && endIndex >= 0) {
// Extract and print the certificate part
val extractedCert = decodedString.substring(startIndex, endIndex)
logger.debug("Extracted Certificate:")
logger.debug(extractedCert)
extractedCert
} else {
logger.debug("Certificate not found in the decoded string.")
""
}
}
def parseSignatureHeader(signatureHeader: String): Map[String, String] = {

View File

@ -1039,7 +1039,7 @@ object NewStyle extends MdcLoggable{
def handleEntitlementsAndScopes(failMsg: => String)(bankId: String, userId: String, roles: List[ApiRole], callContext: Option[CallContext]): Future[Box[Unit]] =
Helper.booleanToFuture(failMsg, cc=callContext) {
APIUtil.handleEntitlementsAndScopes(bankId, userId, APIUtil.getConsumerPrimaryKey(callContext),roles)
APIUtil.handleAccessControlRegardingEntitlementsAndScopes(bankId, userId, APIUtil.getConsumerPrimaryKey(callContext),roles)
} map validateRequestPayload(callContext)
def hasAtLeastOneEntitlement(bankId: String, userId: String, roles: List[ApiRole], callContext: Option[CallContext]): Future[Box[Unit]] = {

View File

@ -21,12 +21,12 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit
object getAccountList extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getAccountList))
object readAccountDetails extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.readAccountDetails))
object getAccountDetails extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getAccountDetails))
object getBalances extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getBalances))
object getTransactionList extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getTransactionList))
object getTransactionDetails extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getTransactionDetails))
object getCardAccountTransactionList extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getCardAccountTransactionList))
@ -38,21 +38,21 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit
object getConsentInformation extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getConsentInformation))
object getConsentStatus extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getConsentStatus))
object startConsentAuthorisationTransactionAuthorisation extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.startConsentAuthorisationTransactionAuthorisation))
object startConsentAuthorisationUpdatePsuAuthentication extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.startConsentAuthorisationUpdatePsuAuthentication))
object startConsentAuthorisationSelectPsuAuthenticationMethod extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.startConsentAuthorisationSelectPsuAuthenticationMethod))
object getConsentAuthorisation extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getConsentAuthorisation))
object getConsentScaStatus extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getConsentScaStatus))
object updateConsentsPsuDataTransactionAuthorisation extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.updateConsentsPsuDataTransactionAuthorisation))
object updateConsentsPsuDataUpdatePsuAuthentication extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.updateConsentsPsuDataUpdatePsuAuthentication))
object updateConsentsPsuDataUpdateSelectPsuAuthenticationMethod extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.updateConsentsPsuDataUpdateSelectPsuAuthenticationMethod))
object updateConsentsPsuDataUpdateAuthorisationConfirmation extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.updateConsentsPsuDataUpdateAuthorisationConfirmation))
feature(s"BG v1.3 - $getAccountList") {
scenario("Not Authentication User, test failed ", BerlinGroupV1_3, getAccountList) {
val requestGet = (V1_3_BG / "accounts").GET
@ -72,9 +72,9 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit
response.body.extract[ErrorMessagesBG].tppMessages.head.text should startWith(NoViewReadAccountsBerlinGroup)
}
}
feature(s"BG v1.3 - $readAccountDetails") {
scenario("Not Authentication User, test failed ", BerlinGroupV1_3, readAccountDetails) {
feature(s"BG v1.3 - $getAccountDetails") {
scenario("Not Authentication User, test failed ", BerlinGroupV1_3, getAccountDetails) {
val requestGet = (V1_3_BG / "accounts" / "accountId").GET
val response = makeGetRequest(requestGet)
@ -83,7 +83,7 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit
response.body.extract[ErrorMessagesBG].tppMessages.head.text should startWith(UserNotLoggedIn)
}
scenario("Authentication User, test succeed", BerlinGroupV1_3, readAccountDetails) {
scenario("Authentication User, test succeed", BerlinGroupV1_3, getAccountDetails) {
val bankId = APIUtil.defaultBankId
val accountId = testAccountId0.value