diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala index ccc0db137..bb37fd533 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala @@ -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 { diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala index 8cefe014f..42705fdb4 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala @@ -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), ) )) } diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 4d7367dc9..af0006dac 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -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), diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index c63fc89cc..ac89f1181 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -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] = { diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index cd749c3f9..84d31cccb 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -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]] = { diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala index 30a06c382..5036ca8fb 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala @@ -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