mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 15:47:01 +00:00
commit
1f8e5bcb57
@ -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 {
|
||||
|
||||
@ -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),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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] = {
|
||||
|
||||
@ -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]] = {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user