diff --git a/.github/Dockerfile_PreBuild b/.github/Dockerfile_PreBuild index cf711494f..782715024 100644 --- a/.github/Dockerfile_PreBuild +++ b/.github/Dockerfile_PreBuild @@ -1,5 +1,10 @@ FROM jetty:9.4-jdk11-alpine +ENV JMX_EXPORTER_VERSION=1.2.0 + +# To enable add "-javaagent:$JETTY_BASE/jmx-exporter.jar=8090:$JETTY_BASE/prometheus_config.yml" to the JAVA_OPTIONS +RUN wget https://github.com/prometheus/jmx_exporter/releases/download/$JMX_EXPORTER_VERSION/jmx_prometheus_javaagent-$JMX_EXPORTER_VERSION.jar -o /var/lib/jetty/jmx-exporter.jar +COPY .github/jmx_exporter.config /var/lib/jetty/prometheus_config.yml # Copy OBP source code # Copy build artifact (.war file) into jetty from 'maven' stage. COPY /obp-api/target/obp-api-1.*.war /var/lib/jetty/webapps/ROOT.war diff --git a/.github/jmx_exporter.config b/.github/jmx_exporter.config new file mode 100644 index 000000000..f9ea32557 --- /dev/null +++ b/.github/jmx_exporter.config @@ -0,0 +1,15 @@ +--- +lowercaseOutputLabelNames: true +lowercaseOutputName: true +whitelistObjectNames: ["java.lang:type=OperatingSystem"] +blacklistObjectNames: [] +rules: +- pattern: 'java.lang<>(committed_virtual_memory|free_physical_memory|free_swap_space|total_physical_memory|total_swap_space)_size:' + name: os_$1_bytes + type: GAUGE + attrNameSnakeCase: true +- pattern: 'java.lang<>((?!process_cpu_time)\w+):' + name: os_$1 + type: GAUGE + attrNameSnakeCase: true +- pattern: ".*" \ No newline at end of file diff --git a/.github/workflows/auto_update_base_image.yml b/.github/workflows/auto_update_base_image.yml new file mode 100644 index 000000000..3048faf15 --- /dev/null +++ b/.github/workflows/auto_update_base_image.yml @@ -0,0 +1,35 @@ +name: Regular base image update check +on: + schedule: + - cron: "0 5 * * *" + workflow_dispatch: + +env: + ## Sets environment variable + DOCKER_HUB_ORGANIZATION: ${{ vars.DOCKER_HUB_ORGANIZATION }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Docker Image Update Checker + id: baseupdatecheck + uses: lucacome/docker-image-update-checker@v2.0.0 + with: + base-image: jetty:9.4-jdk11-alpine + image: ${{ env.DOCKER_HUB_ORGANIZATION }}/obp-api:latest + + - name: Trigger build_container_develop_branch workflow + uses: actions/github-script@v6 + with: + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'build_container_develop_branch.yml', + ref: 'refs/heads/develop' + }); + if: steps.baseupdatecheck.outputs.needs-updating == 'true' diff --git a/.github/workflows/build_container_develop_branch.yml b/.github/workflows/build_container_develop_branch.yml index a7f68daa9..d3f355042 100644 --- a/.github/workflows/build_container_develop_branch.yml +++ b/.github/workflows/build_container_develop_branch.yml @@ -3,6 +3,7 @@ name: Build and publish container develop # read-write repo token # access to secrets on: + workflow_dispatch: push: branches: - develop diff --git a/.github/workflows/build_container_non_develop_branch.yml b/.github/workflows/build_container_non_develop_branch.yml index 7978496c3..946d81de4 100644 --- a/.github/workflows/build_container_non_develop_branch.yml +++ b/.github/workflows/build_container_non_develop_branch.yml @@ -28,12 +28,12 @@ jobs: --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Extract branch name shell: bash run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" - name: Set up JDK 11 - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'adopt' diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 2d77ddd92..66362085b 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -73,7 +73,7 @@ starConnector_supported_types=mapped,internal #endpointMapping.cache.ttl.seconds=0 ## webui props cache time-to-live in seconds -#webui.props.cache.ttl.seconds=20 +#webui.props.cache.ttl.seconds=0 ## DynamicEntity cache time-to-live in seconds, default is 30, the value is 0 at test environment ## no 0 value will cause new dynamic entity will be shown after that seconds @@ -183,9 +183,9 @@ jwt.use.ssl=false ## Enable writing API metrics (which APIs are called) to RDBMS -write_metrics=true +write_metrics=false ## Enable writing connector metrics (which methods are called)to RDBMS -write_connector_metrics=true +write_connector_metrics=false ## ElasticSearch #allow_elasticsearch=true @@ -304,7 +304,7 @@ sandbox_data_import_secret=change_me payments_enabled=true ## Transaction requests are replacing simple payments starting from 1.4.0 -transactionRequests_enabled=true +transactionRequests_enabled=false transactionRequests_connector=mapped ## Transaction Request Types that are supported on this server. Possible values might include SANDBOX_TAN, COUNTERPARTY, SEPA, FREE_FORM @@ -1147,6 +1147,9 @@ default_auth_context_update_request_key=CUSTOMER_NUMBER # the alias prefix path for BerlinGroupV1.3 (OBP built-in is berlin-group/v1.3), the format must be xxx/yyy, eg: 0.6/v1 #berlin_group_v1_3_alias_path= +# Berlin Group URL version +#berlin_group_version_1_canonical_path=v1.3 + # Show the path inside of Berlin Group error message #berlin_group_error_message_show_path = true @@ -1157,12 +1160,18 @@ default_auth_context_update_request_key=CUSTOMER_NUMBER ## Berlin Group Create Consent Frequency per Day Upper Limit #berlin_group_frequency_per_day_upper_limit = 4 +## Berlin Group Create Consent ASPSP-SCA-Approach response header value +#berlin_group_aspsp_sca_approach = redirect + # Support multiple brands on one instance. Note this needs checking on a clustered environment #brands_enabled=false # Support removing the app type checkbox during consumer registration #consumer_registration.display_app_type=true +# Default logo URL during of consumer +#consumer_default_logo_url= + # if set this props, we can automatically grant the Entitlements required to use all the Dynamic Endpoint roles belonging # to the bank_ids (Spaces) the User has access to via their validated email domain. Entitlements are generated /refreshed # both following manual login and Direct Login token generation (POST). diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index f29b72999..0865b59e4 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -567,6 +567,8 @@ class Boot extends MdcLoggable { Menu.i("Consents") / "consents", Menu.i("Debug") / "debug", Menu.i("debug-basic") / "debug" / "debug-basic", + Menu.i("debug-default-header") / "debug" / "debug-default-header", + Menu.i("debug-default-footer") / "debug" / "debug-default-footer", Menu.i("debug-localization") / "debug" / "debug-localization", Menu.i("debug-plain") / "debug" / "debug-plain", Menu.i("debug-webui") / "debug" / "debug-webui", diff --git a/obp-api/src/main/scala/code/api/berlin/group/ConstantsBG.scala b/obp-api/src/main/scala/code/api/berlin/group/ConstantsBG.scala index 0058a6fe1..11bf6659d 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/ConstantsBG.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/ConstantsBG.scala @@ -1,6 +1,15 @@ package code.api.berlin.group +import code.api.util.APIUtil +import com.openbankproject.commons.util.ApiVersion.berlinGroupV13 +import com.openbankproject.commons.util.ScannedApiVersion +import net.liftweb.common.Full + object ConstantsBG { + val berlinGroupVersion1: ScannedApiVersion = APIUtil.getPropsValue("berlin_group_version_1_canonical_path") match { + case Full(props) => berlinGroupV13.copy(apiShortVersion = props) + case _ => berlinGroupV13 + } object SigningBasketsStatus extends Enumeration { type SigningBasketsStatus = Value // Only the codes 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 ca92704d5..11b2ff93a 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 @@ -2,6 +2,7 @@ package code.api.builder.AccountInformationServiceAISApi import code.api.APIFailureNewStyle import code.api.Constant.{SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID} +import code.api.berlin.group.ConstantsBG import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{PostConsentResponseJson, _} import code.api.berlin.group.v1_3.model._ import code.api.berlin.group.v1_3.{BgSpecValidation, JSONFactory_BERLIN_GROUP_1_3, JvalueCaseClass} @@ -9,8 +10,8 @@ import code.api.util.APIUtil.{passesPsd2Aisp, _} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.NewStyle.HttpCode -import code.api.util.newstyle.BalanceNewStyle import code.api.util._ +import code.api.util.newstyle.BalanceNewStyle import code.bankconnectors.Connector import code.consent.{ConsentStatus, Consents} import code.context.{ConsentAuthContextProvider, UserAuthContextProvider} @@ -22,7 +23,6 @@ import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model._ import com.openbankproject.commons.model.enums.{ChallengeType, StrongCustomerAuthenticationStatus, SuppliedAnswerType} -import com.openbankproject.commons.util.ApiVersion import net.liftweb import net.liftweb.common.{Empty, Full} import net.liftweb.http.js.JE.JsRaw @@ -34,7 +34,7 @@ import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future object APIMethods_AccountInformationServiceAISApi extends RestHelper { - val apiVersion = ApiVersion.berlinGroupV13 + val apiVersion = ConstantsBG.berlinGroupVersion1 val resourceDocs = ArrayBuffer[ResourceDoc]() val apiRelations = ArrayBuffer[ApiRelation]() protected implicit def JvalueToSuper(what: JValue): JvalueCaseClass = JvalueCaseClass(what) @@ -156,11 +156,28 @@ recurringIndicator: consentJson <- NewStyle.function.tryons(failMsg, 400, callContext) { json.extract[PostConsentJson] } - _ <- Helper.booleanToFuture(failMsg = BerlinGroupConsentAccessIsEmpty, cc=callContext) { - consentJson.access.accounts.isDefined || - consentJson.access.balances.isDefined || - consentJson.access.transactions.isDefined + + _ <- if (consentJson.access.availableAccounts.isDefined) { + for { + _ <- Helper.booleanToFuture(failMsg = BerlinGroupConsentAccessAvailableAccounts, cc = callContext) { + consentJson.access.availableAccounts.contains("allAccounts") + } + _ <- Helper.booleanToFuture(failMsg = BerlinGroupConsentAccessRecurringIndicator, cc = callContext) { + !consentJson.recurringIndicator + } + _ <- Helper.booleanToFuture(failMsg = BerlinGroupConsentAccessFrequencyPerDay, cc = callContext) { + consentJson.frequencyPerDay == 1 + } + } yield Full(()) + } else { + Helper.booleanToFuture( + failMsg = BerlinGroupConsentAccessIsEmpty, cc = callContext) { + consentJson.access.accounts.isDefined || + consentJson.access.balances.isDefined || + consentJson.access.transactions.isDefined + } } + upperLimit = APIUtil.getPropsAsIntValue("berlin_group_frequency_per_day_upper_limit", 4) _ <- Helper.booleanToFuture(failMsg = FrequencyPerDayError, cc=callContext) { consentJson.frequencyPerDay > 0 && consentJson.frequencyPerDay <= upperLimit @@ -242,10 +259,15 @@ recurringIndicator: case "consents" :: consentId :: Nil JsonDelete _ => { cc => for { - (Full(user), callContext) <- authenticatedAccess(cc) + (_, callContext) <- applicationAccess(cc) _ <- passesPsd2Aisp(callContext) - _ <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { - unboxFullOrFail(_, callContext, ConsentNotFound) + consent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { + unboxFullOrFail(_, callContext, ConsentNotFound, 403) + } + consumerIdFromConsent = consent.mConsumerId.get + consumerIdFromCurrentCall = callContext.map(_.consumer.map(_.consumerId.get).getOrElse("None")).getOrElse("None") + _ <- Helper.booleanToFuture(failMsg = s"$ConsentNotFound $consumerIdFromConsent != $consumerIdFromCurrentCall", failCode = 403, cc = cc.callContext) { + consumerIdFromConsent == consumerIdFromCurrentCall } _ <- Future(Consents.consentProvider.vend.revokeBerlinGroupConsent(consentId)) map { i => connectorEmptyResponse(i, callContext) @@ -755,7 +777,7 @@ This method returns the SCA status of a consent initiation's authorisation sub-r (_, callContext) <- authenticatedAccess(cc) _ <- passesPsd2Aisp(callContext) _ <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { - unboxFullOrFail(_, callContext, s"$ConsentNotFound ($consentId)") + unboxFullOrFail(_, callContext, s"$ConsentNotFound ($consentId)", 403) } (challenges, callContext) <- NewStyle.function.getChallengesByConsentId(consentId, callContext) } yield { @@ -787,10 +809,10 @@ This method returns the SCA status of a consent initiation's authorisation sub-r case "consents" :: consentId:: "status" :: Nil JsonGet _ => { cc => for { - (Full(u), callContext) <- authenticatedAccess(cc) + (_, callContext) <- applicationAccess(cc) _ <- passesPsd2Aisp(callContext) consent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { - unboxFullOrFail(_, callContext, ConsentNotFound) + unboxFullOrFail(_, callContext, ConsentNotFound, 403) } } yield { val status = consent.status @@ -1137,7 +1159,7 @@ using the extended forms as indicated above. (Full(u), callContext) <- authenticatedAccess(cc) _ <- passesPsd2Aisp(callContext) consent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { - unboxFullOrFail(_, callContext, ConsentNotFound) + unboxFullOrFail(_, callContext, ConsentNotFound, 403) } (challenges, callContext) <- NewStyle.function.createChallengesC2( List(u.userId), @@ -1300,7 +1322,7 @@ Maybe in a later version the access path will change. (Full(u), callContext) <- authenticatedAccess(cc) _ <- passesPsd2Aisp(callContext) _ <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { - unboxFullOrFail(_, callContext, ConsentNotFound) + unboxFullOrFail(_, callContext, ConsentNotFound, 403) } failMsg = s"$InvalidJsonFormat The Json body should be the $TransactionAuthorisation " updateJson <- NewStyle.function.tryons(failMsg, 400, callContext) { diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala index e480710f7..082c2fcd5 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala @@ -34,7 +34,7 @@ object BgSpecValidation { if (date.isBefore(today)) { Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) cannot be in the past!") - } else if (date.isAfter(MaxValidDays)) { + } else if (date.isEqual(MaxValidDays) || date.isAfter(MaxValidDays)) { Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) exceeds the maximum allowed period of 180 days (until $MaxValidDays).") } else { Right(date) // Valid date diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/CommonServicesApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/CommonServicesApi.scala index 0a32939b6..34b0f54f6 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/CommonServicesApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/CommonServicesApi.scala @@ -1,5 +1,6 @@ package code.api.builder.CommonServicesApi +import code.api.berlin.group.ConstantsBG import code.api.berlin.group.v1_3.{JvalueCaseClass, OBP_BERLIN_GROUP_1_3} import code.api.builder.AccountInformationServiceAISApi.APIMethods_AccountInformationServiceAISApi import code.api.builder.PaymentInitiationServicePISApi.APIMethods_PaymentInitiationServicePISApi @@ -14,7 +15,7 @@ import scala.collection.mutable.ArrayBuffer //TODO maybe we can remove this common services, it just show other apis in this tag. no new ones. object APIMethods_CommonServicesApi extends RestHelper { - val apiVersion = ApiVersion.berlinGroupV13 + val apiVersion = ConstantsBG.berlinGroupVersion1 val resourceDocs = ArrayBuffer[ResourceDoc]() val apiRelations = ArrayBuffer[ApiRelation]() val codeContext = CodeContext(resourceDocs, apiRelations) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/ConfirmationOfFundsServicePIISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/ConfirmationOfFundsServicePIISApi.scala index 94c33db47..7e5108aa7 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/ConfirmationOfFundsServicePIISApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/ConfirmationOfFundsServicePIISApi.scala @@ -1,5 +1,6 @@ package code.api.builder.ConfirmationOfFundsServicePIISApi +import code.api.berlin.group.ConstantsBG import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3._ import code.api.berlin.group.v1_3.{JvalueCaseClass, OBP_BERLIN_GROUP_1_3} import code.api.util.APIUtil._ @@ -20,7 +21,7 @@ import scala.collection.immutable.Nil import scala.collection.mutable.ArrayBuffer object APIMethods_ConfirmationOfFundsServicePIISApi extends RestHelper { - val apiVersion = ApiVersion.berlinGroupV13 + val apiVersion = ConstantsBG.berlinGroupVersion1 val resourceDocs = ArrayBuffer[ResourceDoc]() val apiRelations = ArrayBuffer[ApiRelation]() protected implicit def JvalueToSuper(what: JValue): JvalueCaseClass = JvalueCaseClass(what) 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 6d5897d0f..edc24e7ca 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 @@ -1,5 +1,6 @@ package code.api.berlin.group.v1_3 +import code.api.berlin.group.ConstantsBG import code.api.berlin.group.v1_3.model.TransactionStatus.mapTransactionStatus import code.api.berlin.group.v1_3.model._ import code.api.util.APIUtil._ @@ -63,7 +64,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ case class CoreAccountJsonV13( resourceId: String, iban: String, - bban: String, + bban: Option[String], currency: String, name: String, product: String, @@ -138,6 +139,10 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ iban: String, currency : Option[String] = None, ) + case class FromAccountJson( + iban: String, + currency : Option[String] = None, + ) case class TransactionJsonV13( transactionId: String, creditorName: String, @@ -191,7 +196,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ ) case class TransactionsJsonV13( - account:FromAccount, + account: FromAccountJson, transactions:TransactionsV13Transactions, ) @@ -260,7 +265,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ recurringIndicator: Boolean, validUntil: String, frequencyPerDay: Int, - combinedServiceIndicator: Boolean, + combinedServiceIndicator: Option[Boolean], lastActionDate: String, consentStatus: String ) @@ -346,12 +351,12 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ CoreAccountJsonV13( resourceId = x.accountId.value, iban = iBan, - bban = bBan, + bban = None, currency = x.currency, name = x.name, cashAccountType = cashAccountType, product = x.accountType, - balances = accountBalances, + balances = if(canReadBalances) accountBalances else None, _links = CoreAccountLinksJsonV13( balances = if(canReadBalances) Some(balanceRef) else None, transactions = if(canReadTransactions) Some(transactionRef) else None, @@ -377,7 +382,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ CoreAccountJsonV13( resourceId = x.accountId.value, iban = iBan, - bban = bBan, + bban = None, currency = x.currency, name = x.name, cashAccountType = x.accountType, @@ -454,14 +459,17 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ )) } - def createTransactionJSON(bankAccount: BankAccount, transaction : ModeratedTransaction, creditorAccount: CreditorAccountJson) : TransactionJsonV13 = { - val bookingDate = transaction.startDate.getOrElse(null) - val valueDate = transaction.finishDate.getOrElse(null) + def createTransactionJSON(bankAccount: BankAccount, transaction : ModeratedTransaction) : TransactionJsonV13 = { + val bookingDate = transaction.startDate.orNull + val valueDate = transaction.finishDate.orNull val creditorName = bankAccount.label TransactionJsonV13( transactionId = transaction.id.value, creditorName = creditorName, - creditorAccount = creditorAccount, + creditorAccount = CreditorAccountJson( + transaction.otherBankAccount.map(_.iban.orNull).orNull, + transaction.currency + ), transactionAmount = AmountOfMoneyV13(APIUtil.stringOptionOrNull(transaction.currency), transaction.amount.get.toString().trim.stripPrefix("-")), bookingDate = BgSpecValidation.formatToISODate(bookingDate) , valueDate = BgSpecValidation.formatToISODate(valueDate), @@ -490,16 +498,19 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ } - def createTransactionFromRequestJSON(bankAccount: BankAccount, transactionRequest : TransactionRequest, creditorAccount: CreditorAccountJson) : TransactionJsonV13 = { + def createTransactionFromRequestJSON(bankAccount: BankAccount, tr : TransactionRequest) : TransactionJsonV13 = { val creditorName = bankAccount.accountHolder - val remittanceInformationUnstructured = stringOrNull(transactionRequest.body.description) + val remittanceInformationUnstructured = stringOrNull(tr.body.description) TransactionJsonV13( - transactionId = transactionRequest.id.value, + transactionId = tr.id.value, creditorName = creditorName, - creditorAccount = creditorAccount, - transactionAmount = AmountOfMoneyV13(transactionRequest.charge.value.currency, transactionRequest.charge.value.amount.trim.stripPrefix("-")), - bookingDate = BgSpecValidation.formatToISODate(transactionRequest.start_date), - valueDate = BgSpecValidation.formatToISODate(transactionRequest.end_date), + creditorAccount = CreditorAccountJson( + if (tr.other_account_routing_scheme == "IBAN") tr.other_account_routing_address else "", + Some(tr.body.value.currency) + ), + transactionAmount = AmountOfMoneyV13(tr.charge.value.currency, tr.charge.value.amount.trim.stripPrefix("-")), + bookingDate = BgSpecValidation.formatToISODate(tr.start_date), + valueDate = BgSpecValidation.formatToISODate(tr.end_date), remittanceInformationUnstructured = remittanceInformationUnstructured ) } @@ -508,18 +519,16 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ val accountId = bankAccount.accountId.value val (iban: String, bban: String) = getIbanAndBban(bankAccount) - val creditorAccount = CreditorAccountJson( + val account = FromAccountJson( iban = iban, currency = Some(bankAccount.currency) ) TransactionsJsonV13( - FromAccount( - iban = iban, - ), + account, TransactionsV13Transactions( - booked= transactions.map(transaction => createTransactionJSON(bankAccount, transaction, creditorAccount)), - pending = transactionRequests.filter(_.status!="COMPLETED").map(transactionRequest => createTransactionFromRequestJSON(bankAccount, transactionRequest, creditorAccount)), - _links = TransactionsV13TransactionsLinks(LinkHrefJson(s"/v1.3/accounts/$accountId")) + booked= transactions.map(transaction => createTransactionJSON(bankAccount, transaction)), + pending = transactionRequests.filter(_.status!="COMPLETED").map(transactionRequest => createTransactionFromRequestJSON(bankAccount, transactionRequest)), + _links = TransactionsV13TransactionsLinks(LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/accounts/$accountId")) ) ) } @@ -565,7 +574,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ CardTransactionsV13Transactions( booked= transactions.map(t => createCardTransactionJson(t)), pending = Nil, - _links = CardTransactionsLinksV13(LinkHrefJson(s"/v1.3/card-accounts/$accountId")) + _links = CardTransactionsLinksV13(LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/card-accounts/$accountId")) ) ) } @@ -576,7 +585,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ consentId = consent.consentId, consentStatus = consent.status.toLowerCase(), _links = ConsentLinksV13( - startAuthorisation = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations")) + startAuthorisation = Some(Href(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/consents/${consent.consentId}/authorisations")) ) ) } @@ -595,9 +604,9 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ consentStatus = consent.status.toLowerCase(), _links = ConsentLinksV13( scaRedirect = Some(Href(s"$scaRedirectUrl")), - status = Some(Href(s"/v1.3/consents/${consent.consentId}/status")), + status = Some(Href(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/consents/${consent.consentId}/status")), // TODO Introduce a working link - // scaStatus = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations/AUTHORISATIONID")), + // scaStatus = Some(Href(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/consents/${consent.consentId}/authorisations/AUTHORISATIONID")), ) ) case Full("redirection_with_dedicated_start_of_authorization") => @@ -607,7 +616,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ consentId = consent.consentId, consentStatus = consent.status.toLowerCase(), _links = ConsentLinksV13( - startAuthorisationWithPsuAuthentication = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations")) + startAuthorisationWithPsuAuthentication = Some(Href(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/consents/${consent.consentId}/authorisations")) ) ) case Full("decoupled") => @@ -615,7 +624,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ consentId = consent.consentId, consentStatus = consent.status.toLowerCase(), _links = ConsentLinksV13( - startAuthorisationWithPsuIdentification = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations")) + startAuthorisationWithPsuIdentification = Some(Href(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/consents/${consent.consentId}/authorisations")) ) ) case _ => @@ -626,7 +635,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ def createPutConsentResponseJson(consent: ConsentTrait) : ScaStatusResponse = { ScaStatusResponse( scaStatus = consent.status.toLowerCase(), - _links = Some(LinksAll(scaStatus = Some(HrefType(Some(s"/v1.3/consents/${consent.consentId}/authorisations"))))) + _links = Some(LinksAll(scaStatus = Some(HrefType(Some(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/consents/${consent.consentId}/authorisations"))))) ) } @@ -640,7 +649,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ recurringIndicator = createdConsent.recurringIndicator, validUntil = if(createdConsent.validUntil == null) null else new SimpleDateFormat(DateWithDay).format(createdConsent.validUntil), frequencyPerDay = createdConsent.frequencyPerDay, - combinedServiceIndicator= createdConsent.combinedServiceIndicator, + combinedServiceIndicator = None, lastActionDate = if(createdConsent.lastActionDate == null) null else new SimpleDateFormat(DateWithDay).format(createdConsent.lastActionDate), consentStatus = createdConsent.status.toLowerCase() ) @@ -651,7 +660,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ scaStatus = challenge.scaStatus.map(_.toString).getOrElse("None"), authorisationId = challenge.authenticationMethodId.getOrElse("None"), pushMessage = "started", //TODO Not implement how to fill this. - _links = ScaStatusJsonV13(s"/v1.3/consents/${consent.consentId}/authorisations/${challenge.challengeId}")//TODO, Not sure, what is this for?? + _links = ScaStatusJsonV13(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/consents/${consent.consentId}/authorisations/${challenge.challengeId}")//TODO, Not sure, what is this for?? ) } @@ -704,9 +713,9 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ paymentId = paymentId, _links = InitiatePaymentResponseLinks( scaRedirect = LinkHrefJson(s"$scaRedirectUrl/$paymentId"), - self = LinkHrefJson(s"/v1.3/payments/sepa-credit-transfers/$paymentId"), - status = LinkHrefJson(s"/v1.3/payments/$paymentId/status"), - scaStatus = LinkHrefJson(s"/v1.3/payments/$paymentId/authorisations/${paymentId}") + self = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/sepa-credit-transfers/$paymentId"), + status = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/$paymentId/status"), + scaStatus = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/$paymentId/authorisations/${paymentId}") ) ) } @@ -715,9 +724,9 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ CancelPaymentResponseJson( "ACTC", _links = CancelPaymentResponseLinks( - self = LinkHrefJson(s"/v1.3/payments/sepa-credit-transfers/$paymentId"), - status = LinkHrefJson(s"/v1.3/payments/sepa-credit-transfers/$paymentId/status"), - startAuthorisation = LinkHrefJson(s"/v1.3/payments/sepa-credit-transfers/cancellation-authorisations/${paymentId}") + self = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/sepa-credit-transfers/$paymentId"), + status = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/sepa-credit-transfers/$paymentId/status"), + startAuthorisation = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/sepa-credit-transfers/cancellation-authorisations/${paymentId}") ) ) } @@ -731,7 +740,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ scaStatus = challenge.scaStatus.map(_.toString).getOrElse(""), authorisationId = challenge.challengeId, psuMessage = "Please check your SMS at a mobile device.", - _links = ScaStatusJsonV13(s"/v1.3/payments/sepa-credit-transfers/${challenge.challengeId}") + _links = ScaStatusJsonV13(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/sepa-credit-transfers/${challenge.challengeId}") ) } @@ -739,7 +748,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ ScaStatusResponse( scaStatus = challenge.scaStatus.map(_.toString).getOrElse(""), psuMessage = Some("Please check your SMS at a mobile device."), - _links = Some(LinksAll(scaStatus = Some(HrefType(Some(s"/v1.3/payments/sepa-credit-transfers/${challenge.challengeId}")))) + _links = Some(LinksAll(scaStatus = Some(HrefType(Some(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/payments/sepa-credit-transfers/${challenge.challengeId}")))) ) ) } @@ -751,7 +760,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ ScaStatusResponse( scaStatus = challenge.scaStatus.map(_.toString).getOrElse(""), psuMessage = Some("Please check your SMS at a mobile device."), - _links = Some(LinksAll(scaStatus = Some(HrefType(Some(s"/v1.3/${paymentService}/${paymentProduct}/${paymentId}/cancellation-authorisations/${challenge.challengeId}")))) + _links = Some(LinksAll(scaStatus = Some(HrefType(Some(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/${paymentService}/${paymentProduct}/${paymentId}/cancellation-authorisations/${challenge.challengeId}")))) ) ) } @@ -767,7 +776,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ authorisationId = Some(challenge.challengeId), psuMessage = Some("Please check your SMS at a mobile device."), _links = Some(LinksUpdatePsuAuthentication( - scaStatus = Some(HrefType(Some(s"/v1.3/${paymentService}/${paymentProduct}/${paymentId}/cancellation-authorisations/${challenge.challengeId}")))) + scaStatus = Some(HrefType(Some(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/${paymentService}/${paymentProduct}/${paymentId}/cancellation-authorisations/${challenge.challengeId}")))) ) ) } @@ -778,7 +787,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ scaStatus = challenge.scaStatus.map(_.toString).getOrElse(""), authorisationId = challenge.challengeId, psuMessage = "Please check your SMS at a mobile device.", - _links = ScaStatusJsonV13(s"/v1.3/signing-baskets/${basketId}/authorisations/${challenge.challengeId}") + _links = ScaStatusJsonV13(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/signing-baskets/${basketId}/authorisations/${challenge.challengeId}") ) } @@ -787,9 +796,9 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{ basketId = basket.basketId, transactionStatus = basket.status.toLowerCase(), _links = SigningBasketLinksV13( - self = LinkHrefJson(s"/v1.3/signing-baskets/${basket.basketId}"), - status = LinkHrefJson(s"/v1.3/signing-baskets/${basket.basketId}/status"), - startAuthorisation = LinkHrefJson(s"/v1.3/signing-baskets/${basket.basketId}/authorisations") + self = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/signing-baskets/${basket.basketId}"), + status = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/signing-baskets/${basket.basketId}/status"), + startAuthorisation = LinkHrefJson(s"/${ConstantsBG.berlinGroupVersion1.apiShortVersion}/signing-baskets/${basket.basketId}/authorisations") ) ) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3.scala index 5a488877f..0fea84db8 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3.scala @@ -32,6 +32,7 @@ package code.api.berlin.group.v1_3 import code.api.OBPRestHelper +import code.api.berlin.group.ConstantsBG import code.api.builder.AccountInformationServiceAISApi.APIMethods_AccountInformationServiceAISApi import code.api.builder.CommonServicesApi.APIMethods_CommonServicesApi import code.api.builder.ConfirmationOfFundsServicePIISApi.APIMethods_ConfirmationOfFundsServicePIISApi @@ -40,7 +41,7 @@ import code.api.builder.SigningBasketsApi.APIMethods_SigningBasketsApi import code.api.util.APIUtil.{OBPEndpoint, ResourceDoc, getAllowedEndpoints} import code.api.util.ScannedApis import code.util.Helper.MdcLoggable -import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion,ApiVersionStatus} +import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus, ScannedApiVersion} import scala.collection.mutable.ArrayBuffer @@ -52,7 +53,7 @@ This file defines which endpoints from all the versions are available in v1 */ object OBP_BERLIN_GROUP_1_3 extends OBPRestHelper with MdcLoggable with ScannedApis { - override val apiVersion = ApiVersion.berlinGroupV13 + override val apiVersion = ConstantsBG.berlinGroupVersion1 val versionStatus = ApiVersionStatus.DRAFT.toString val endpoints = diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3_Alias.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3_Alias.scala index 279925c98..35eaab593 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3_Alias.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/OBP_BERLIN_GROUP_1_3_Alias.scala @@ -47,7 +47,7 @@ object OBP_BERLIN_GROUP_1_3_Alias extends OBPRestHelper with MdcLoggable with Sc override val allResourceDocs: ArrayBuffer[ResourceDoc] = if(berlinGroupV13AliasPath.nonEmpty){ OBP_BERLIN_GROUP_1_3.allResourceDocs.map(resourceDoc => resourceDoc.copy( - implementedInApiVersion = apiVersion, + implementedInApiVersion = apiVersion.copy(apiStandard = resourceDoc.implementedInApiVersion.apiStandard), )) } else ArrayBuffer.empty[ResourceDoc] diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApi.scala index 6472e7b48..9c88b8f30 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/PaymentInitiationServicePISApi.scala @@ -1,5 +1,6 @@ package code.api.builder.PaymentInitiationServicePISApi +import code.api.berlin.group.ConstantsBG import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{CancelPaymentResponseJson, CancelPaymentResponseLinks, LinkHrefJson, UpdatePaymentPsuDataJson, checkAuthorisationConfirmation, checkSelectPsuAuthenticationMethod, checkTransactionAuthorisation, checkUpdatePsuAuthentication, createCancellationTransactionRequestJson} import code.api.berlin.group.v1_3.model.TransactionStatus.mapTransactionStatus import code.api.berlin.group.v1_3.model._ @@ -30,7 +31,7 @@ import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future object APIMethods_PaymentInitiationServicePISApi extends RestHelper { - val apiVersion = ApiVersion.berlinGroupV13 + val apiVersion = ConstantsBG.berlinGroupVersion1 val resourceDocs = ArrayBuffer[ResourceDoc]() val apiRelations = ArrayBuffer[ApiRelation]() protected implicit def JvalueToSuper(what: JValue): JvalueCaseClass = JvalueCaseClass(what) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/SigningBasketsApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/SigningBasketsApi.scala index 5d041bc3a..8b1c05891 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/SigningBasketsApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/SigningBasketsApi.scala @@ -28,7 +28,7 @@ import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future object APIMethods_SigningBasketsApi extends RestHelper { - val apiVersion = ApiVersion.berlinGroupV13 + val apiVersion = ConstantsBG.berlinGroupVersion1 val resourceDocs = ArrayBuffer[ResourceDoc]() val apiRelations = ArrayBuffer[ApiRelation]() protected implicit def JvalueToSuper(what: JValue): JvalueCaseClass = JvalueCaseClass(what) @@ -260,7 +260,7 @@ This method returns the SCA status of a signing basket's authorisation sub-resou (Full(u), callContext) <- authenticatedAccess(cc) _ <- passesPsd2Pisp(callContext) _ <- Future(SigningBasketX.signingBasketProvider.vend.getSigningBasketByBasketId(basketId)) map { - unboxFullOrFail(_, callContext, s"$ConsentNotFound ($basketId)") + unboxFullOrFail(_, callContext, s"$ConsentNotFound ($basketId)", 403) } (challenges, callContext) <- NewStyle.function.getChallengesByBasketId(basketId, callContext) } yield { diff --git a/obp-api/src/main/scala/code/api/constant/constant.scala b/obp-api/src/main/scala/code/api/constant/constant.scala index 97fa6ddab..da60249f8 100644 --- a/obp-api/src/main/scala/code/api/constant/constant.scala +++ b/obp-api/src/main/scala/code/api/constant/constant.scala @@ -149,6 +149,10 @@ object RequestHeader { final lazy val `PSD2-CERT` = "PSD2-CERT" final lazy val `If-None-Match` = "If-None-Match" + final lazy val `PSU-Geo-Location` = "PSU-Geo-Location" // Berlin Group + final lazy val `PSU-Device-Name` = "PSU-Device-Name" // Berlin Group + final lazy val `PSU-Device-ID` = "PSU-Device-ID" // Berlin Group + final lazy val `PSU-IP-Address` = "PSU-IP-Address" // Berlin Group final lazy val `X-Request-ID` = "X-Request-ID" // Berlin Group final lazy val `TPP-Redirect-URI` = "TPP-Redirect-URI" // Berlin Group final lazy val `TPP-Nok-Redirect-URI` = "TPP-Nok-Redirect-URI" // Redirect URI in case of an error. @@ -171,6 +175,7 @@ object RequestHeader { final lazy val `If-Modified-Since` = "If-Modified-Since" } object ResponseHeader { + final lazy val `ASPSP-SCA-Approach` = "ASPSP-SCA-Approach" // Berlin Group final lazy val `Correlation-Id` = "Correlation-Id" final lazy val `WWW-Authenticate` = "WWW-Authenticate" final lazy val ETag = "ETag" 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 4dea90c35..05440e775 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -34,6 +34,7 @@ import code.api.OAuthHandshake._ import code.api.UKOpenBanking.v2_0_0.OBP_UKOpenBanking_200 import code.api.UKOpenBanking.v3_1_0.OBP_UKOpenBanking_310 import code.api._ +import code.api.berlin.group.ConstantsBG import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ErrorMessageBG, ErrorMessagesBG} import code.api.cache.Caching import code.api.dynamic.endpoint.OBPAPIDynamicEndpoint @@ -448,8 +449,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ private def getHeadersNewStyle(cc: Option[CallContextLight]) = { CustomResponseHeaders( - getGatewayLoginHeader(cc).list ::: - getRateLimitHeadersNewStyle(cc).list ::: + getGatewayLoginHeader(cc).list ::: + getRequestHeadersBerlinGroup(cc).list ::: + getRateLimitHeadersNewStyle(cc).list ::: getPaginationHeadersNewStyle(cc).list ::: getRequestHeadersToMirror(cc).list ::: getRequestHeadersToEcho(cc).list @@ -520,7 +522,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val mirrorByProperties = getPropsValue("mirror_request_headers_to_response", "").split(",").toList.map(_.trim) val mirrorRequestHeadersToResponse: List[String] = - if (callContext.exists(_.url.contains(ApiVersion.berlinGroupV13.urlPrefix))) { + if (callContext.exists(_.url.contains(ConstantsBG.berlinGroupVersion1.urlPrefix))) { // Berlin Group Specification RequestHeader.`X-Request-ID` :: mirrorByProperties } else { @@ -555,6 +557,18 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ CustomResponseHeaders(Nil) } } + + def getRequestHeadersBerlinGroup(callContext: Option[CallContextLight]): CustomResponseHeaders = { + val aspspScaApproach = getPropsValue("berlin_group_aspsp_sca_approach", defaultValue = "redirect") + callContext match { + case Some(cc) if cc.url.contains(ConstantsBG.berlinGroupVersion1.urlPrefix) && cc.url.endsWith("/consents") => + CustomResponseHeaders(List( + (ResponseHeader.`ASPSP-SCA-Approach`, aspspScaApproach) + )) + case _ => + CustomResponseHeaders(Nil) + } + } /** * * @param jwt is a JWT value extracted from GatewayLogin Authorization Header. @@ -725,7 +739,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } def composeErrorMessage() = { val path = callContextLight.map(_.url).getOrElse("") - if (path.contains(ApiVersion.berlinGroupV13.urlPrefix)) { + if (path.contains(ConstantsBG.berlinGroupVersion1.urlPrefix)) { val path = if(APIUtil.getPropsAsBoolValue("berlin_group_error_message_show_path", defaultValue = true)) callContextLight.map(_.url) @@ -3001,12 +3015,12 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val res = if (authHeadersWithEmptyValues.nonEmpty) { // Check Authorization Headers Empty Values val message = ErrorMessages.EmptyRequestHeaders + s"Header names: ${authHeadersWithEmptyValues.mkString(", ")}" - Future { (fullBoxOrException(Empty ~> APIFailureNewStyle(message, 400, Some(cc.toLight))), None) } + Future { (fullBoxOrException(Empty ~> APIFailureNewStyle(message, 400, Some(cc.toLight))), Some(cc)) } } else if (authHeadersWithEmptyNames.nonEmpty) { // Check Authorization Headers Empty Names val message = ErrorMessages.EmptyRequestHeaders + s"Header values: ${authHeadersWithEmptyNames.mkString(", ")}" - Future { (fullBoxOrException(Empty ~> APIFailureNewStyle(message, 400, Some(cc.toLight))), None) } + Future { (fullBoxOrException(Empty ~> APIFailureNewStyle(message, 400, Some(cc.toLight))), Some(cc)) } } else if (authHeaders.size > 1) { // Check Authorization Headers ambiguity - Future { (Failure(ErrorMessages.AuthorizationHeaderAmbiguity + s"${authHeaders}"), None) } + Future { (Failure(ErrorMessages.AuthorizationHeaderAmbiguity + s"${authHeaders}"), Some(cc)) } } else if (APIUtil.`hasConsent-ID`(reqHeaders)) { // Berlin Group's Consent Consent.applyBerlinGroupRules(APIUtil.`getConsent-ID`(reqHeaders), cc.copy(consumer = consumerByCertificate)) } else if (APIUtil.hasConsentJWT(reqHeaders)) { // Open Bank Project's Consent diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala index 0dd184a67..a1061462e 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala @@ -1,7 +1,9 @@ package code.api.util -import code.api.APIFailureNewStyle +import code.api.berlin.group.ConstantsBG +import code.api.{APIFailureNewStyle, RequestHeader} import code.api.util.APIUtil.{OBPReturnType, fullBoxOrException} +import code.api.util.BerlinGroupSigning.getHeaderValue import code.util.Helper.MdcLoggable import com.openbankproject.commons.model.User import com.openbankproject.commons.util.ApiVersion @@ -27,7 +29,7 @@ object BerlinGroupCheck extends MdcLoggable { private def validateHeaders(verb: String, url: String, reqHeaders: List[HTTPParam], forwardResult: (Box[User], Option[CallContext])): (Box[User], Option[CallContext]) = { val headerMap = reqHeaders.map(h => h.name.toLowerCase -> h).toMap - val missingHeaders = if(url.contains(ApiVersion.berlinGroupV13.urlPrefix) && url.endsWith("/consents")) + val missingHeaders = if(url.contains(ConstantsBG.berlinGroupVersion1.urlPrefix) && url.endsWith("/consents")) (berlinGroupMandatoryHeaders ++ berlinGroupMandatoryHeaderConsent).filterNot(headerMap.contains) else berlinGroupMandatoryHeaders.filterNot(headerMap.contains) @@ -43,8 +45,22 @@ object BerlinGroupCheck extends MdcLoggable { } } + def isTppRequestsWithoutPsuInvolvement(requestHeaders: List[HTTPParam]): Boolean = { + val psuIpAddress = getHeaderValue(RequestHeader.`PSU-IP-Address`, requestHeaders) + val psuDeviceId = getHeaderValue(RequestHeader.`PSU-Device-ID`, requestHeaders) + val psuDeviceNAme = getHeaderValue(RequestHeader.`PSU-Device-Name`, requestHeaders) + if(psuIpAddress == "0.0.0.0" || psuDeviceId == "no-psu-involved" || psuDeviceNAme == "no-psu-involved") { + logger.debug(s"isTppRequestsWithoutPsuInvolvement.psuIpAddress: $psuIpAddress") + logger.debug(s"isTppRequestsWithoutPsuInvolvement.psuDeviceId: $psuDeviceId") + logger.debug(s"isTppRequestsWithoutPsuInvolvement.psuDeviceNAme: $psuDeviceNAme") + true + } else { + false + } + } + def validate(body: Box[String], verb: String, url: String, reqHeaders: List[HTTPParam], forwardResult: (Box[User], Option[CallContext])): OBPReturnType[Box[User]] = { - if(url.contains(ApiVersion.berlinGroupV13.urlPrefix)) { + if(url.contains(ConstantsBG.berlinGroupVersion1.urlPrefix)) { validateHeaders(verb, url, reqHeaders, forwardResult) match { case (user, _) if user.isDefined || user == Empty => // All good. Chain another check // Verify signed request (Berlin Group) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala index e39065907..196375a37 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala @@ -69,8 +69,6 @@ object BerlinGroupError { case "401" if message.contains("OBP-35018") => "CONSENT_INVALID" case "401" if message.contains("OBP-35005") => "CONSENT_INVALID" - case "403" if message.contains("OBP-35001") => "CONSENT_UNKNOWN" - case "401" if message.contains("OBP-20300") => "CERTIFICATE_BLOCKED" case "401" if message.contains("OBP-20312") => "CERTIFICATE_INVALID" case "401" if message.contains("OBP-20300") => "CERTIFICATE_INVALID" @@ -80,6 +78,7 @@ object BerlinGroupError { case "400" if message.contains("OBP-35018") => "CONSENT_UNKNOWN" case "400" if message.contains("OBP-35001") => "CONSENT_UNKNOWN" + case "403" if message.contains("OBP-35001") => "CONSENT_UNKNOWN" case "404" if message.contains("OBP-30076") => "RESOURCE_UNKNOWN" @@ -91,6 +90,9 @@ object BerlinGroupError { case "400" if message.contains("OBP-20252") => "FORMAT_ERROR" case "400" if message.contains("OBP-20251") => "FORMAT_ERROR" case "400" if message.contains("OBP-20088") => "FORMAT_ERROR" + case "400" if message.contains("OBP-20089") => "FORMAT_ERROR" + case "400" if message.contains("OBP-20090") => "FORMAT_ERROR" + case "400" if message.contains("OBP-20091") => "FORMAT_ERROR" case "429" if message.contains("OBP-10018") => "ACCESS_EXCEEDED" case _ => code 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 ce97f3451..73338e414 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -160,14 +160,15 @@ object BerlinGroupSigning extends MdcLoggable { val certificate = getCertificateFromTppSignatureCertificate(requestHeaders) X509.validateCertificate(certificate) match { case Full(true) => // PEM certificate is ok - val digest = generateDigest(body.getOrElse("")) - if(digest == getHeaderValue(RequestHeader.Digest, requestHeaders)) { // Verifying the Hash in the Digest Field + val generatedDigest = generateDigest(body.getOrElse("")) + val requestHeaderDigest = getHeaderValue(RequestHeader.Digest, requestHeaders) + if(generatedDigest == requestHeaderDigest) { // Verifying the Hash in the Digest Field val signatureHeaderValue = getHeaderValue(RequestHeader.Signature, requestHeaders) val signature = parseSignatureHeader(signatureHeaderValue).getOrElse("signature", "NONE") val headersToSign = parseSignatureHeader(signatureHeaderValue).getOrElse("headers", "").split(" ").toList val headers = headersToSign.map(h => if (h.toLowerCase() == RequestHeader.Digest.toLowerCase()) { - s"$h: $digest" + s"$h: $generatedDigest" } else { s"$h: ${getHeaderValue(h, requestHeaders)}" } @@ -183,6 +184,8 @@ object BerlinGroupSigning extends MdcLoggable { case (false, _) => (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2) } } else { // The two DIGEST hashes do NOT match, the integrity of the request body is NOT confirmed. + logger.debug(s"Generated digest: $generatedDigest") + logger.debug(s"Request header digest: $requestHeaderDigest") (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2) } case Failure(msg, t, c) => (Failure(msg, t, c), forwardResult._2) // PEM certificate is not valid @@ -298,7 +301,8 @@ object BerlinGroupSigning extends MdcLoggable { developerEmail = extractedEmail, redirectURL = None, createdByUserId = None, - certificate = None + certificate = None, + logoUrl = APIUtil.getPropsValue("consumer_default_logo_url") ) // Set or update certificate diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index db2c6ea75..2b7b9eb5f 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -1,21 +1,28 @@ package code.api.util +import code.accountholders.AccountHolders +import code.api.berlin.group.ConstantsBG + import java.text.SimpleDateFormat import java.util.{Date, UUID} import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessJson, PostConsentJson} +import code.api.util.APIUtil.fullBoxOrException import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, canCreateEntitlementAtOneBank} import code.api.util.BerlinGroupSigning.getHeaderValue import code.api.util.ErrorMessages.{CouldNotAssignAccountAccess, InvalidConnectorResponse, NoViewReadAccountsBerlinGroup} import code.api.v3_1_0.{PostConsentBodyCommonJson, PostConsentEntitlementJsonV310, PostConsentViewJsonV310} import code.api.v5_0_0.HelperInfoJson -import code.api.{APIFailure, Constant, RequestHeader} +import code.api.{APIFailure, APIFailureNewStyle, Constant, RequestHeader} import code.bankconnectors.Connector import code.consent +import code.consent.ConsentStatus.ConsentStatus import code.consent.{ConsentStatus, Consents, MappedConsent} import code.consumer.Consumers import code.context.{ConsentAuthContextProvider, UserAuthContextProvider} import code.entitlement.Entitlement import code.model.Consumer +import code.model.dataAccess.BankAccountRouting +import code.scheduler.ConsentScheduler.logger import code.users.Users import code.util.Helper.MdcLoggable import code.util.HydraUtil @@ -253,7 +260,7 @@ object Consent extends MdcLoggable { case false => Failure(ErrorMessages.ConsentVerificationIssue) } - case Full(c) if c.apiStandard == ApiVersion.berlinGroupV13.apiStandard && // Berlin Group Consent + case Full(c) if c.apiStandard == ConstantsBG.berlinGroupVersion1.apiStandard && // Berlin Group Consent c.status.toLowerCase() != ConsentStatus.valid.toString => Failure(s"${ErrorMessages.ConsentStatusIssue}${ConsentStatus.valid.toString}.") case Full(c) if c.mStatus.toString().toUpperCase() != ConsentStatus.ACCEPTED.toString => @@ -566,9 +573,11 @@ object Consent extends MdcLoggable { logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT].checkConsent.consentBox: $consent") consentBox match { // Check is it Consent-JWT expired case (Full(true)) => // OK - // Update MappedConsent.usesSoFarTodayCounter field - val consentUpdatedBox = Consents.consentProvider.vend.updateBerlinGroupConsent(consentId, currentCounterState + 1) - logger.debug(s"applyBerlinGroupConsentRulesCommon.consentUpdatedBox: $consentUpdatedBox") + if(BerlinGroupCheck.isTppRequestsWithoutPsuInvolvement(callContext.requestHeaders)) { + // Update MappedConsent.usesSoFarTodayCounter field + val consentUpdatedBox = Consents.consentProvider.vend.updateBerlinGroupConsent(consentId, currentCounterState + 1) + logger.debug(s"applyBerlinGroupConsentRulesCommon.consentUpdatedBox: $consentUpdatedBox") + } applyConsentRules(consent, updatedCallContext) case failure@Failure(_, _, _) => // Handled errors Future(failure, Some(updatedCallContext)) @@ -595,12 +604,14 @@ object Consent extends MdcLoggable { Future(Failure("Cannot extract data from: " + consentId), Some(updatedCallContext)) } } else { - Future(Failure(ErrorMessages.TooManyRequests + s" ${RequestHeader.`Consent-ID`}: $consentId"), Some(updatedCallContext)) + val errorMessage = ErrorMessages.TooManyRequests + s" ${RequestHeader.`Consent-ID`}: $consentId" + Future(fullBoxOrException(Empty ~> APIFailureNewStyle(errorMessage, 429, Some(callContext.toLight))), Some(callContext)) } case failure@Failure(_, _, _) => Future(failure, Some(callContext)) case _ => - Future(Failure(ErrorMessages.ConsentNotFound + s" ($consentId)"), Some(callContext)) + val errorMessage = ErrorMessages.ConsentNotFound + s" ($consentId)" + Future(fullBoxOrException(Empty ~> APIFailureNewStyle(errorMessage, 400, Some(callContext.toLight))), Some(callContext)) } } def applyBerlinGroupRules(consentId: Option[String], callContext: CallContext): Future[(Box[User], Option[CallContext])] = { @@ -772,6 +783,10 @@ object Consent extends MdcLoggable { } val tppRedirectUri: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`TPP-Redirect-URI`) val tppNokRedirectUri: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`TPP-Nok-Redirect-URI`) + val xRequestId: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`X-Request-ID`) + val psuDeviceId: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`PSU-Device-ID`) + val psuIpAddress: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`PSU-IP-Address`) + val psuGeoLocation: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`PSU-Geo-Location`) Future.sequence(accounts ::: balances ::: transactions) map { views => val json = ConsentJWT( createdByUserId = user.map(_.userId).getOrElse(""), @@ -782,7 +797,12 @@ object Consent extends MdcLoggable { iat = currentTimeInSeconds, nbf = currentTimeInSeconds, exp = validUntilTimeInSeconds, - request_headers = tppRedirectUri.toList ::: tppNokRedirectUri.toList, + request_headers = tppRedirectUri.toList ::: + tppNokRedirectUri.toList ::: + xRequestId.toList ::: + psuDeviceId.toList ::: + psuIpAddress.toList ::: + psuGeoLocation.toList, name = None, email = None, entitlements = Nil, @@ -855,19 +875,78 @@ object Consent extends MdcLoggable { } } } - def updateUserIdOfBerlinGroupConsentJWT(createdByUserId: String, - consent: MappedConsent, - callContext: Option[CallContext]): Future[Box[String]] = { + def updateViewsOfBerlinGroupConsentJWT(user: User, + consent: MappedConsent, + callContext: Option[CallContext]): Future[Box[MappedConsent]] = { implicit val dateFormats = CustomJsonFormats.formats val payloadToUpdate: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken) // Payload as JSON string .map(net.liftweb.json.parse(_).extract[ConsentJWT]) // Extract case class - Future { - val updatedPayload = payloadToUpdate.map(i => i.copy(createdByUserId = createdByUserId)) // Update only the field "createdByUserId" - val jwtPayloadAsJson = compactRender(Extraction.decompose(updatedPayload)) - val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson) - Full(CertificateUtil.jwtWithHmacProtection(jwtClaims, consent.secret)) + val availableAccountsUserIbans: List[String] = payloadToUpdate match { + case Full(consentJwt) => + val availableAccountsUserIbans: List[String] = + if (consentJwt.access.map(_.availableAccounts.contains("allAccounts")).isDefined) { + // Get all accounts held by the current user + val userAccounts: List[BankIdAccountId] = + AccountHolders.accountHolders.vend.getAccountsHeldByUser(user, Some(null)).toList + userAccounts.flatMap { acc => + BankAccountRouting.find( + By(BankAccountRouting.BankId, acc.bankId.value), + By(BankAccountRouting.AccountId, acc.accountId.value), + By(BankAccountRouting.AccountRoutingScheme, "IBAN") + ).map(_.AccountRoutingAddress.get) + } + } else { + val emptyList: List[String] = Nil + emptyList + } + availableAccountsUserIbans + case _ => + val emptyList: List[String] = Nil + emptyList } + + + // 1. Add access + val availableAccounts: List[Future[ConsentView]] = availableAccountsUserIbans.distinct map { iban => + Connector.connector.vend.getBankAccountByIban(iban, callContext) map { bankAccount => + logger.debug(s"createBerlinGroupConsentJWT.accounts.bankAccount: $bankAccount") + val error = s"${InvalidConnectorResponse} IBAN: ${iban} ${handleBox(bankAccount._1)}" + ConsentView( + bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""), + account_id = bankAccount._1.map(_.accountId.value).openOrThrowException(error), + view_id = Constant.SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID, + None + ) + } + } + + Future.sequence(availableAccounts) map { views => + if(views.isEmpty) { + Empty + } else { + val updatedPayload = payloadToUpdate.map(i => + i.copy(views = views) // Update the field "views" + ) + val jwtPayloadAsJson = compactRender(Extraction.decompose(updatedPayload)) + val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson) + val jwt = CertificateUtil.jwtWithHmacProtection(jwtClaims, consent.secret) + Consents.consentProvider.vend.setJsonWebToken(consent.consentId, jwt) + } + } + } + + def updateUserIdOfBerlinGroupConsentJWT(createdByUserId: String, + consent: MappedConsent, + callContext: Option[CallContext]): Box[String] = { + implicit val dateFormats = CustomJsonFormats.formats + val payloadToUpdate: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken) // Payload as JSON string + .map(net.liftweb.json.parse(_).extract[ConsentJWT]) // Extract case class + + val updatedPayload = payloadToUpdate.map(i => i.copy(createdByUserId = createdByUserId)) // Update only the field "createdByUserId" + val jwtPayloadAsJson = compactRender(Extraction.decompose(updatedPayload)) + val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson) + Full(CertificateUtil.jwtWithHmacProtection(jwtClaims, consent.secret)) } def createUKConsentJWT( @@ -1036,6 +1115,26 @@ object Consent extends MdcLoggable { consentsOfBank } + def expireAllPreviousValidBerlinGroupConsents(consent: MappedConsent, updateTostatus: ConsentStatus): Boolean = { + if(updateTostatus == ConsentStatus.valid && + consent.apiStandard == ConstantsBG.berlinGroupVersion1.apiStandard) { + MappedConsent.findAll( // Find all + By(MappedConsent.mApiStandard, ConstantsBG.berlinGroupVersion1.apiStandard), // Berlin Group + By(MappedConsent.mRecurringIndicator, true), // recurring + By(MappedConsent.mStatus, ConsentStatus.valid.toString), // and valid consents + By(MappedConsent.mUserId, consent.userId), // for the same PSU + By(MappedConsent.mConsumerId, consent.consumerId), // from the same TPP + ).filterNot(_.consentId == consent.consentId) // Exclude current consent + .map{ c => // Set to expired + val changedStatus = c.mStatus(ConsentStatus.expired.toString).mLastActionDate(new Date()).save + if(changedStatus) logger.warn(s"|---> Changed status to ${ConsentStatus.expired.toString} for consent ID: ${c.id}") + changedStatus + }.forall(_ == true) + } else { + true + } + } + /* // Example Usage val box1: Box[String] = Full("Hello, World!") diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 21ed948e7..27c61a20f 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -244,7 +244,10 @@ object ErrorMessages { s"OBP-20087: The current source view.can_revoke_access_to_custom_views is false." val BerlinGroupConsentAccessIsEmpty = s"OBP-20088: An access must be requested." - + val BerlinGroupConsentAccessRecurringIndicator = s"OBP-20089: Recurring indicator must be false when availableAccounts is used." + val BerlinGroupConsentAccessFrequencyPerDay = s"OBP-20090: Frequency per day must be 1 when availableAccounts is used." + val BerlinGroupConsentAccessAvailableAccounts = s"OBP-20091: availableAccounts must be exactly 'allAccounts'." + val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements:" val CannotGetOrCreateUser = "OBP-20102: Cannot get or create user." val InvalidUserProvider = "OBP-20103: Invalid DAuth User Provider." @@ -268,6 +271,7 @@ object ErrorMessages { val OneTimePasswordExpired = "OBP-20211: The One Time Password (OTP) has expired. " val Oauth2IsNotRecognized = "OBP-20214: OAuth2 Access Token is not recognised at this instance." val Oauth2ValidateAccessTokenError = "OBP-20215: There was a problem validating the OAuth2 access token. " + val OneTimePasswordInvalid = "OBP-20216: The One Time Password (OTP) is invalid. " val AuthorizationHeaderAmbiguity = "OBP-20250: Request headers used for authorization are ambiguous. " val MissingMandatoryBerlinGroupHeaders= "OBP-20251: Missing mandatory request headers. " diff --git a/obp-api/src/main/scala/code/api/util/HashUtil.scala b/obp-api/src/main/scala/code/api/util/HashUtil.scala index e7960564e..6fb85eaea 100644 --- a/obp-api/src/main/scala/code/api/util/HashUtil.scala +++ b/obp-api/src/main/scala/code/api/util/HashUtil.scala @@ -2,6 +2,7 @@ package code.api.util import java.math.BigInteger import net.liftweb.common.Box +import org.iban4j.IbanUtil object HashUtil { def Sha256Hash(in: String): String = { @@ -25,5 +26,8 @@ object HashUtil { val hashedText = Sha256Hash(plainText) println("Password: " + plainText) println("Hashed password: " + hashedText) + println("BBAN: " + IbanUtil.getBban("AT483200000012345864")) + println("Bank code: " + IbanUtil.getBankCode("AT483200000012345864")) + println("Country code: " + IbanUtil.getCountryCode("AT483200000012345864")) } } diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index b7bf2973a..01ea36639 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -8081,7 +8081,9 @@ trait APIMethods400 extends MdcLoggable { consent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { i => connectorEmptyResponse(i, callContext) } - _ <- Helper.booleanToFuture(ConsentUserAlreadyAdded, cc=callContext) { consent.userId != null } + _ <- Helper.booleanToFuture(ConsentUserAlreadyAdded, cc = cc.callContext) { + Option(consent.userId).forall(_.isBlank) // checks whether userId is not populated + } consent <- Future(Consents.consentProvider.vend.updateConsentUser(consentId, user)) map { i => connectorEmptyResponse(i, callContext) } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 7112b89a8..37c91d520 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -1439,7 +1439,7 @@ trait APIMethods510 { nameOf(updateConsentUserIdByConsentId), "PUT", "/management/banks/BANK_ID/consents/CONSENT_ID/created-by-user", - "Update Consent Created by User by CONSENT_ID", + "Update Created by User of Consent by CONSENT_ID", s""" | |This endpoint is used to Update the User bound to a consent. @@ -1493,11 +1493,11 @@ trait APIMethods510 { consent <- Future(Consents.consentProvider.vend.updateConsentUser(consentId, user)) map { i => connectorEmptyResponse(i, cc.callContext) } - consentJWT <- Consent.updateUserIdOfBerlinGroupConsentJWT( + consentJWT <- Future(Consent.updateUserIdOfBerlinGroupConsentJWT( consentJson.user_id, consent, cc.callContext - ) map { + )) map { i => connectorEmptyResponse(i, cc.callContext) } updatedConsent <- Future(Consents.consentProvider.vend.setJsonWebToken(consent.consentId, consentJWT)) map { diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index adfa21e00..8d4980bb2 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -27,6 +27,7 @@ package code.api.v5_1_0 import code.api.Constant +import code.api.berlin.group.ConstantsBG import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.ConsentAccessJson import code.api.util.APIUtil.{DateWithDay, DateWithSeconds, gitCommit, stringOrNull} import code.api.util._ @@ -943,8 +944,8 @@ object JSONFactory510 extends CustomJsonFormats { last_action_date = if (c.lastActionDate != null) new SimpleDateFormat(DateWithDay).format(c.lastActionDate) else null, last_usage_date = if (c.usesSoFarTodayCounterUpdatedAt != null) new SimpleDateFormat(DateWithSeconds).format(c.usesSoFarTodayCounterUpdatedAt) else null, jwt_payload = jwtPayload, - frequency_per_day = if(c.apiStandard == ApiVersion.berlinGroupV13.apiStandard) Some(c.frequencyPerDay) else None, - remaining_requests = if(c.apiStandard == ApiVersion.berlinGroupV13.apiStandard) Some(c.frequencyPerDay - c.usesSoFarTodayCounter) else None, + frequency_per_day = if(c.apiStandard == ConstantsBG.berlinGroupVersion1.apiStandard) Some(c.frequencyPerDay) else None, + remaining_requests = if(c.apiStandard == ConstantsBG.berlinGroupVersion1.apiStandard) Some(c.frequencyPerDay - c.usesSoFarTodayCounter) else None, api_standard = c.apiStandard, api_version = c.apiVersion ) diff --git a/obp-api/src/main/scala/code/cardano/cardano.scala b/obp-api/src/main/scala/code/cardano/cardano.scala new file mode 100644 index 000000000..586127c66 --- /dev/null +++ b/obp-api/src/main/scala/code/cardano/cardano.scala @@ -0,0 +1,154 @@ +import java.io.{File, PrintWriter} +import scala.sys.process._ +import java.security.MessageDigest + +object CardanoMetadataWriter { + + // Function to generate SHA-256 hash of a string + def generateHash(transactionData: String): String = { + val digest = MessageDigest.getInstance("SHA-256") + val hashBytes = digest.digest(transactionData.getBytes("UTF-8")) + hashBytes.map("%02x".format(_)).mkString + } + + // Function to write metadata JSON file + def writeMetadataFile(transactionHash: String, filePath: String): Unit = { + val jsonContent = + s""" + |{ + | "674": { + | "transaction_hash": "$transactionHash" + | } + |} + |""".stripMargin + + val file = new File(filePath) + val writer = new PrintWriter(file) + writer.write(jsonContent) + writer.close() + println(s"Metadata file written to: $filePath") + } + + // Function to submit transaction to Cardano + def submitHashToCardano(transactionHash: String, txIn: String, txOut: String, signingKey: String, network: String): Unit = { + val metadataFilePath = "metadata.json" + + // Write metadata to file + writeMetadataFile(transactionHash, metadataFilePath) + + // Build transaction + val buildCommand = s"cardano-cli transaction build-raw --tx-in $txIn --tx-out $txOut --metadata-json-file $metadataFilePath --out-file tx.raw" + buildCommand.! + + // Sign transaction + val signCommand = s"cardano-cli transaction sign --tx-body-file tx.raw --signing-key-file $signingKey --$network --out-file tx.signed" + signCommand.! + + // Submit transaction + val submitCommand = s"cardano-cli transaction submit --tx-file tx.signed --$network" + submitCommand.! + + println("Transaction submitted to Cardano blockchain.") + } + + // Example Usage + def main(args: Array[String]): Unit = { + val transactionData = "123|100.50|EUR|2025-03-16 12:30:00" + val transactionHash = generateHash(transactionData) + + val txIn = "8c293647e5cb51c4d29e57e162a0bb4a0500096560ce6899a4b801f2b69f2813:0" // This is a tx_id:0 ///"YOUR_UTXO_HERE" // Replace with actual UTXO + val txOut = "addr_test1qruvtthh7mndxu2ncykn47tksar9yqr3u97dlkq2h2dhzwnf3d755n99t92kp4rydpzgv7wmx4nx2j0zzz0g802qvadqtczjhn:1234" // "YOUR_RECEIVER_ADDRESS+LOVELACE" // Replace with receiver address and amount + val signingKey = "payment.skey" // Path to your signing key file + val network = "--testnet-magic" // "--testnet-magic 1097911063" // Use --mainnet for mainnet transactions + + submitHashToCardano(transactionHash, txIn, txOut, signingKey, network) + } +} + +// TODO +// Create second wallet +// Find version of Pre Prod i'm running +// Get CLI for that version +// Use faucet to get funds + + + + + +/* +import com.bloxbean.cardano.client.account.Account +import com.bloxbean.cardano.client.api.UtxoSupplier +import com.bloxbean.cardano.client.backend.impl.local.LocalNodeBackendService +import com.bloxbean.cardano.client.backend.api.TransactionService +import com.bloxbean.cardano.client.backend.api.UtxoService +import com.bloxbean.cardano.client.backend.model.Utxo +import com.bloxbean.cardano.client.common.model.Network +import com.bloxbean.cardano.client.metadata.cbor.CBORMetadata +import com.bloxbean.cardano.client.transaction.spec.Transaction +import com.bloxbean.cardano.client.api.helper.TransactionBuilder +import java.security.MessageDigest + +object CardanoMetadataWriter { + + // Function to generate SHA-256 hash + def generateHash(transactionData: String): String = { + val digest = MessageDigest.getInstance("SHA-256") + val hashBytes = digest.digest(transactionData.getBytes("UTF-8")) + hashBytes.map("%02x".format(_)).mkString + } + + // Function to submit metadata transaction + def submitMetadataToCardano(mnemonic: String, transactionData: String): Unit = { + val network = Network.TESTNET // Change to Network.MAINNET for mainnet + + // Load Daedalus wallet from mnemonic + val account = new Account(network, mnemonic) + + // Generate hash of transaction data + val transactionHash = generateHash(transactionData) + + println(s"Generated Hash: $transactionHash") + + // Create metadata object + val metadata = new CBORMetadata() + metadata.put("674", Map("transaction_hash" -> transactionHash)) + + // Initialize local Cardano node backend + val backendService = new LocalNodeBackendService("http://localhost:8080") + val transactionService: TransactionService = backendService.getTransactionService + val utxoService: UtxoService = backendService.getUtxoService + + // Get available UTXOs from the wallet + val utxos: java.util.List[Utxo] = utxoService.getUtxos(account.baseAddress, 1, 10).getValue + + if (utxos.isEmpty) { + println("No UTXOs found. Please fund your wallet.") + return + } + + // Build transaction + val transaction = TransactionBuilder.create() + .account(account) + .metadata(metadata) + .utxos(utxos) + .changeAddress(account.baseAddress) + .network(network) + .build() + + // Sign transaction + val signedTransaction: Transaction = account.sign(transaction) + + // Submit transaction + val txHash: String = transactionService.submitTransaction(signedTransaction).getValue + println(s"✅ Transaction submitted! TxHash: $txHash") + } + + // Main method + def main(args: Array[String]): Unit = { + val mnemonic = "YOUR_12_OR_24_WORD_MNEMONIC_HERE" + val transactionData = "123|100.50|USD|2025-03-16 12:30:00" + + submitMetadataToCardano(mnemonic, transactionData) + } +} +*/ diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index 2b10dba67..072b1d50b 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -30,6 +30,7 @@ object MappedConsentProvider extends ConsentProvider { override def updateConsentStatus(consentId: String, status: ConsentStatus): Box[MappedConsent] = { MappedConsent.find(By(MappedConsent.mConsentId, consentId)) match { case Full(consent) => + Consent.expireAllPreviousValidBerlinGroupConsents(consent, status) tryo(consent .mStatus(status.toString) .mLastActionDate(now) //maybe not right, but for the create we use the `now`, we need to update it later. diff --git a/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala b/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala index ac927d726..dc4098f92 100644 --- a/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala +++ b/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala @@ -71,7 +71,9 @@ trait ConsumersProvider { developerEmail: Option[String], redirectURL: Option[String], createdByUserId: Option[String], - certificate: Option[String] = None): Box[Consumer] + certificate: Option[String] = None, + logoUrl: Option[String] = None + ): Box[Consumer] def populateMissingUUIDs(): Boolean } \ No newline at end of file diff --git a/obp-api/src/main/scala/code/model/OAuth.scala b/obp-api/src/main/scala/code/model/OAuth.scala index 3e8645bef..bf23e5719 100644 --- a/obp-api/src/main/scala/code/model/OAuth.scala +++ b/obp-api/src/main/scala/code/model/OAuth.scala @@ -389,7 +389,9 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { developerEmail: Option[String], redirectURL: Option[String], createdByUserId: Option[String], - certificate: Option[String]): Box[Consumer] = { + certificate: Option[String], + logoUrl: Option[String], + ): Box[Consumer] = { val consumer: Box[Consumer] = // 1st try to find via UUID issued by OBP-API back end @@ -473,6 +475,10 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { case Some(v) => c.clientCertificate(v) case None => } + logoUrl match { + case Some(v) => c.logoUrl(v) + case None => + } consumerId match { case Some(v) => c.consumerId(v) case None => diff --git a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala index a3df0b335..7d8113a60 100644 --- a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala +++ b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala @@ -1,5 +1,6 @@ package code.scheduler +import code.api.berlin.group.ConstantsBG import code.api.util.APIUtil import code.consent.{ConsentStatus, MappedConsent} import code.util.Helper.MdcLoggable @@ -52,7 +53,7 @@ object ConsentScheduler extends MdcLoggable { val outdatedConsents = MappedConsent.findAll( By(MappedConsent.mStatus, ConsentStatus.received.toString), - By(MappedConsent.mApiStandard, ApiVersion.berlinGroupV13.apiStandard), + By(MappedConsent.mApiStandard, ConstantsBG.berlinGroupVersion1.apiStandard), By_<(MappedConsent.updatedAt, SchedulerUtil.someSecondsAgo(seconds)) ) @@ -78,7 +79,7 @@ object ConsentScheduler extends MdcLoggable { val expiredConsents = MappedConsent.findAll( By(MappedConsent.mStatus, ConsentStatus.valid.toString), - By(MappedConsent.mApiStandard, ApiVersion.berlinGroupV13.apiStandard), + By(MappedConsent.mApiStandard, ConstantsBG.berlinGroupVersion1.apiStandard), By_<(MappedConsent.mValidUntil, new Date()) ) diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 6f73a0034..e55a343c2 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -110,7 +110,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 for { // Fetch the consent by ID consent: MappedConsent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { - APIUtil.unboxFullOrFail(_, None, s"$ConsentNotFound ($consentId)", 404) + APIUtil.unboxFullOrFail(_, None, s"$ConsentNotFound ($consentId)", 400) } // Update the consent JWT with new access details consentJWT <- Consent.updateAccountAccessOfBerlinGroupConsentJWT( @@ -171,6 +171,19 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 // Select all IBANs selectedAccountsIbansValue.set(userIbans) + var canReadAccountsIbansAvailableAccounts: List[String] = List() + if(json.access.availableAccounts.contains("allAccounts")) { // + /* + Access is requested via: + "access": + { + "availableAccounts": "allAccounts" + } + */ + accessAccountsDefinedVar.set(true) + canReadAccountsIbansAvailableAccounts = userIbans.toList + } + // Determine which IBANs the user can access for accounts, balances, and transactions val canReadAccountsIbans: List[String] = json.access.accounts match { case Some(accounts) if accounts.isEmpty => // Access is requested via "accounts": [] @@ -226,7 +239,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } // all Selected IBANs - val ibansFromGetConsentResponseJson = (canReadAccountsIbans ::: canReadBalancesIbans ::: canReadTransactionsIbans).distinct + val ibansFromGetConsentResponseJson = (canReadAccountsIbansAvailableAccounts ::: canReadAccountsIbans ::: canReadBalancesIbans ::: canReadTransactionsIbans).distinct /** * Generates toggle switches for IBAN lists. @@ -385,10 +398,19 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 */ private def denyConsentRequestProcess() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") - Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.rejected) - S.redirectTo( - s"$redirectUriValue?CONSENT_ID=${consentId}" - ) + Consents.consentProvider.vend.getConsentByConsentId(consentId) match { + case Full(consent) if otpValue.is == consent.challenge => + updateConsentUser(consent) + updateConsentJwt(consent) map { i => + Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.rejected) + } + S.redirectTo( + s"$redirectUriValue?CONSENT_ID=${consentId}" + ) + case _ => + S.error(ErrorMessages.ConsentNotFound) + } + } /** @@ -398,15 +420,29 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 val consentId = ObpS.param("CONSENT_ID") openOr ("") Consents.consentProvider.vend.getConsentByConsentId(consentId) match { case Full(consent) if otpValue.is == consent.challenge => - Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.valid) + updateConsentUser(consent) + updateConsentJwt(consent) map { i => + Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.valid) + } S.redirectTo( s"/confirm-bg-consent-request-redirect-uri?CONSENT_ID=${consentId}" ) case _ => - S.error("Wrong OTP value") + S.error(ErrorMessages.OneTimePasswordInvalid) } } + private def updateConsentUser(consent: MappedConsent): Box[MappedConsent] = { + val loggedInUser = AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn) + Consents.consentProvider.vend.updateConsentUser(consent.consentId, loggedInUser) + val jwt = Consent.updateUserIdOfBerlinGroupConsentJWT(loggedInUser.userId, consent, None).openOrThrowException(ErrorMessages.InvalidConnectorResponse) + Consents.consentProvider.vend.setJsonWebToken(consent.consentId, jwt) + } + private def updateConsentJwt(consent: MappedConsent) = { + val loggedInUser = AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn) + Consent.updateViewsOfBerlinGroupConsentJWT(loggedInUser, consent, None) + } + private def getTppRedirectUri() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") s"$redirectUriValue?CONSENT_ID=${consentId}" diff --git a/obp-api/src/main/webapp/debug.html b/obp-api/src/main/webapp/debug.html index 02de2e0ae..7e24b175a 100644 --- a/obp-api/src/main/webapp/debug.html +++ b/obp-api/src/main/webapp/debug.html @@ -31,7 +31,9 @@ Berlin 13359, Germany

Here are the debugging pages.

debug-plain -- no Liftweb involved.

-

debug-basic -- call LiftWeb code 'surround'.

+

debug-basic (default)-- call LiftWeb default code 'surround'.

+

debug-default-header -- call LiftWeb default header code 'surround'.

+

debug-default-footer -- call LiftWeb default footer code 'surround'.

debug-localization -- call Localization 'lift:loc' method.

debug-webui -- call webui method 'apiDocumentationLink' method.

diff --git a/obp-api/src/main/webapp/debug/debug-default-footer.html b/obp-api/src/main/webapp/debug/debug-default-footer.html new file mode 100644 index 000000000..ad3c25c9f --- /dev/null +++ b/obp-api/src/main/webapp/debug/debug-default-footer.html @@ -0,0 +1,20 @@ + + + + + + +Basic Liftweb Suround with default + +
+ +

I call LiftWeb code surround

+ with a link +

Link to static

static image +

Link to SDKs

SDKs + +
+ + + + \ No newline at end of file diff --git a/obp-api/src/main/webapp/debug/debug-default-header.html b/obp-api/src/main/webapp/debug/debug-default-header.html new file mode 100644 index 000000000..1b352e080 --- /dev/null +++ b/obp-api/src/main/webapp/debug/debug-default-header.html @@ -0,0 +1,20 @@ + + + + + + +Basic Liftweb Suround with default + +
+ +

I call LiftWeb code surround

+ with a link +

Link to static

static image +

Link to SDKs

SDKs + +
+ + + + \ No newline at end of file diff --git a/obp-api/src/main/webapp/templates-hidden/default-footer.html b/obp-api/src/main/webapp/templates-hidden/default-footer.html new file mode 100644 index 000000000..91bee85c2 --- /dev/null +++ b/obp-api/src/main/webapp/templates-hidden/default-footer.html @@ -0,0 +1,275 @@ + + + + + + + + + + + Open Bank Project: + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ + The main content gets bound here +
+ + + +
+ + diff --git a/obp-api/src/main/webapp/templates-hidden/default-header.html b/obp-api/src/main/webapp/templates-hidden/default-header.html new file mode 100644 index 000000000..96384d792 --- /dev/null +++ b/obp-api/src/main/webapp/templates-hidden/default-header.html @@ -0,0 +1,264 @@ + + + + + + + + + + + Open Bank Project: + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + +
+ + + + + + + + +
+ +
+ left logo image +
+
+
+ right logo image +
+
+
+
+
+ Skip to main content + Skip to main content + Skip to main content + Skip to main content + Skip to main content + Skip to main content + +
+ + +
+ +
+
+ + +
+ + The main content gets bound here +
+ + +
+ + diff --git a/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala b/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala index b9ab1ad22..152cca121 100644 --- a/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala +++ b/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala @@ -1,6 +1,7 @@ package code.api.ResourceDocs1_4_0 import code.api.ResourceDocs1_4_0.ResourceDocs140.ImplementationsResourceDocs +import code.api.berlin.group.ConstantsBG import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn} import code.api.util.APIUtil.OAuth._ @@ -283,7 +284,7 @@ class ResourceDocsTest extends ResourceDocsV140ServerSetup with PropsReset with } scenario(s"We will test ${ApiEndpoint1.name} Api -v1.3", ApiEndpoint1, VersionOfApi) { - val requestGetObp = (ResourceDocsV4_0Request / "resource-docs" / "v1.3" / "obp").GET + val requestGetObp = (ResourceDocsV4_0Request / "resource-docs" / ConstantsBG.berlinGroupVersion1.apiShortVersion / "obp").GET val responseGetObp = makeGetRequest(requestGetObp) And("We should get 200 and the response can be extract to case classes") val responseDocs = responseGetObp.body.extract[ResourceDocsJson] @@ -293,7 +294,7 @@ class ResourceDocsTest extends ResourceDocsV140ServerSetup with PropsReset with } scenario(s"We will test ${ApiEndpoint1.name} Api -BGv1.3", ApiEndpoint1, VersionOfApi) { - val requestGetObp = (ResourceDocsV4_0Request / "resource-docs" / "BGv1.3" / "obp").GET + val requestGetObp = (ResourceDocsV4_0Request / "resource-docs" / s"BG${ConstantsBG.berlinGroupVersion1.apiShortVersion}" / "obp").GET val responseGetObp = makeGetRequest(requestGetObp) And("We should get 200 and the response can be extract to case classes") val responseDocs = responseGetObp.body.extract[ResourceDocsJson] @@ -540,7 +541,7 @@ class ResourceDocsTest extends ResourceDocsV140ServerSetup with PropsReset with } scenario(s"We will test ${ApiEndpoint2.name} Api -v1.3", ApiEndpoint1, VersionOfApi) { - val requestGetObp = (ResourceDocsV1_4Request /"banks"/ testBankId1.value/ "resource-docs" / "v1.3" / "obp").GET + val requestGetObp = (ResourceDocsV1_4Request /"banks"/ testBankId1.value/ "resource-docs" / ConstantsBG.berlinGroupVersion1.apiShortVersion / "obp").GET val responseGetObp = makeGetRequest(requestGetObp) And("We should get 200 and the response can be extract to case classes") val responseDocs = responseGetObp.body.extract[ResourceDocsJson] @@ -550,7 +551,7 @@ class ResourceDocsTest extends ResourceDocsV140ServerSetup with PropsReset with } scenario(s"We will test ${ApiEndpoint2.name} Api -BGv1.3", ApiEndpoint1, VersionOfApi) { - val requestGetObp = (ResourceDocsV1_4Request /"banks"/ testBankId1.value/ "resource-docs" / "BGv1.3" / "obp").GET + val requestGetObp = (ResourceDocsV1_4Request /"banks"/ testBankId1.value/ "resource-docs" / s"BG${ConstantsBG.berlinGroupVersion1.apiShortVersion}" / "obp").GET val responseGetObp = makeGetRequest(requestGetObp) And("We should get 200 and the response can be extract to case classes") val responseDocs = responseGetObp.body.extract[ResourceDocsJson] 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 9e993fc2a..eb5ff33ba 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 @@ -1,7 +1,7 @@ package code.api.berlin.group.v1_3 import code.api.Constant -import code.api.Constant.{SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID} +import code.api.Constant.{SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID} import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3._ import code.api.builder.AccountInformationServiceAISApi.APIMethods_AccountInformationServiceAISApi import code.api.util.APIUtil @@ -9,10 +9,9 @@ import code.api.util.APIUtil.OAuth._ import code.api.util.ErrorMessages._ import code.api.v4_0_0.PostViewJsonV400 import code.consent.ConsentStatus -import code.model.dataAccess.{BankAccountRouting, MappedBankAccount} +import code.model.dataAccess.BankAccountRouting import code.setup.{APIResponse, DefaultUsers} import com.github.dwickern.macros.NameOf.nameOf -import com.openbankproject.commons.model.{AccountId, BankId, ErrorMessage} import com.openbankproject.commons.model.enums.AccountRoutingScheme import net.liftweb.json.Serialization.write import net.liftweb.mapper.By @@ -103,13 +102,40 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit user1, PostViewJsonV400(view_id = SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID, is_system = true) ) + grantUserAccessToViewViaEndpoint( + bankId, + accountId, + resourceUser1.userId, + user1, + PostViewJsonV400(view_id = SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID, is_system = true) + ) + grantUserAccessToViewViaEndpoint( + bankId, + accountId, + resourceUser1.userId, + user1, + PostViewJsonV400(view_id = SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID, is_system = true) + ) val requestGet = (V1_3_BG / "accounts" / accountId).GET <@ (user1) val response = makeGetRequest(requestGet) Then("We should get a 200 ") response.code should equal(200) - response.body.extract[AccountDetailsJsonV13].account.resourceId should be (accountId) + val jsonResponse = response.body.extract[AccountDetailsJsonV13] + jsonResponse.account.resourceId should be (accountId) + + jsonResponse.account._links.balances match { + case Some(link) => + link.href.contains(berlinGroupVersion1) shouldBe true + case None => // Nothing to check + } + jsonResponse.account._links.transactions match { + case Some(link) => + link.href.contains(berlinGroupVersion1) shouldBe true + case None => // Nothing to check + } + } } @@ -232,6 +258,68 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit } } + feature(s"BG v1.3 - $createConsent - postJsonBodyAvailableAccounts") { + lazy val postJsonBody = PostConsentJson( + access = ConsentAccessJson( + accounts = None, + balances = None, + transactions = None, + availableAccounts = Some("allAccounts"), + allPsd2 = None + ), + recurringIndicator = false, + validUntil = getNextMonthDate(), + frequencyPerDay = 1, + combinedServiceIndicator = Some(false) + ) + val postJsonBodyWrong1 = postJsonBody.copy( + access = postJsonBody.access.copy( + availableAccounts = Some("wrong") + ) + ) + val postJsonBodyWrong2 = postJsonBody.copy( + frequencyPerDay = 2 + ) + val postJsonBodyWrong3 = postJsonBody.copy( + recurringIndicator = true + ) + + scenario("Authentication User, test failed due to availableAccounts wrong value", BerlinGroupV1_3, createConsent) { + val requestPost = (V1_3_BG / "consents" ).POST <@ (user1) + val response: APIResponse = makePostRequest(requestPost, write(postJsonBodyWrong1)) + + Then("We should get a 400") + response.code should equal(400) + response.body.extract[ErrorMessagesBG].tppMessages.head.text should startWith(BerlinGroupConsentAccessAvailableAccounts) + } + scenario("Authentication User, test failed due to frequency per day", BerlinGroupV1_3, createConsent) { + val requestPost = (V1_3_BG / "consents" ).POST <@ (user1) + val response: APIResponse = makePostRequest(requestPost, write(postJsonBodyWrong2)) + + Then("We should get a 400") + response.code should equal(400) + response.body.extract[ErrorMessagesBG].tppMessages.head.text should startWith(BerlinGroupConsentAccessFrequencyPerDay) + } + scenario("Authentication User, test failed due to recurringIndicator = true", BerlinGroupV1_3, createConsent) { + val requestPost = (V1_3_BG / "consents" ).POST <@ (user1) + val response: APIResponse = makePostRequest(requestPost, write(postJsonBodyWrong3)) + + Then("We should get a 400") + response.code should equal(400) + response.body.extract[ErrorMessagesBG].tppMessages.head.text should startWith(BerlinGroupConsentAccessRecurringIndicator) + } + scenario("Authentication User, test succeed", BerlinGroupV1_3, createConsent) { + val requestPost = (V1_3_BG / "consents" ).POST <@ (user1) + val response: APIResponse = makePostRequest(requestPost, write(postJsonBody)) + + Then("We should get a 201 ") + response.code should equal(201) + val jsonResponse = response.body.extract[PostConsentResponseJson] + jsonResponse.consentId should not be (empty) + jsonResponse.consentStatus should be (ConsentStatus.received.toString) + } + } + feature(s"BG v1.3 - $createConsent") { scenario("Authentication User, test succeed", BerlinGroupV1_3, createConsent) { val testBankId = testAccountId1 @@ -262,8 +350,9 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit Then("We should get a 201 ") response.code should equal(201) - response.body.extract[PostConsentResponseJson].consentId should not be (empty) - response.body.extract[PostConsentResponseJson].consentStatus should be (ConsentStatus.received.toString) + val jsonResponse = response.body.extract[PostConsentResponseJson] + jsonResponse.consentId should not be (empty) + jsonResponse.consentStatus should be (ConsentStatus.received.toString) } } @@ -484,7 +573,7 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit scenario("Authentication User, only mocked data, just test succeed", BerlinGroupV1_3, updateConsentsPsuDataTransactionAuthorisation) { val requestStartConsentAuthorisation = (V1_3_BG / "consents"/"consentId" /"authorisations"/ "AUTHORISATIONID" ).PUT <@ (user1) val responseStartConsentAuthorisation = makePutRequest(requestStartConsentAuthorisation, """{"scaAuthenticationData":""}""") - responseStartConsentAuthorisation.code should be (400) + responseStartConsentAuthorisation.code should be (403) } diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupServerSetupV1_3.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupServerSetupV1_3.scala index 466de7774..3c55ed5ee 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupServerSetupV1_3.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupServerSetupV1_3.scala @@ -2,20 +2,21 @@ package code.api.berlin.group.v1_3 import code.api.Constant import code.api.Constant.SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID +import code.api.berlin.group.ConstantsBG import code.api.util.APIUtil.OAuth._ -import code.api.util.APIUtil.OAuth.{Consumer, Token} import code.api.v3_0_0.ViewJsonV300 import code.api.v4_0_0.{PostAccountAccessJsonV400, PostViewJsonV400} import code.setup.ServerSetupWithTestData import code.views.Views -import com.openbankproject.commons.util.ApiVersion import dispatch.Req import net.liftweb.json.Serialization.write import org.scalatest.Tag trait BerlinGroupServerSetupV1_3 extends ServerSetupWithTestData { + + val berlinGroupVersion1: String = ConstantsBG.berlinGroupVersion1.apiShortVersion object BerlinGroupV1_3 extends Tag("BerlinGroup_v1_3") - val V1_3_BG = baseRequest / ApiVersion.berlinGroupV13.urlPrefix / "v1.3" + val V1_3_BG = baseRequest / ConstantsBG.berlinGroupVersion1.urlPrefix / ConstantsBG.berlinGroupVersion1.apiShortVersion def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0" override def beforeEach() = { diff --git a/obp-api/src/test/scala/code/util/APIUtilHeavyTest.scala b/obp-api/src/test/scala/code/util/APIUtilHeavyTest.scala index ade584eb3..722909e79 100644 --- a/obp-api/src/test/scala/code/util/APIUtilHeavyTest.scala +++ b/obp-api/src/test/scala/code/util/APIUtilHeavyTest.scala @@ -30,6 +30,7 @@ package code.util import code.api.Constant.SYSTEM_OWNER_VIEW_ID import code.api.UKOpenBanking.v2_0_0.{APIMethods_UKOpenBanking_200, OBP_UKOpenBanking_200} import code.api.UKOpenBanking.v3_1_0.{APIMethods_AccountAccessApi, OBP_UKOpenBanking_310} +import code.api.berlin.group.ConstantsBG import code.api.berlin.group.v1_3.OBP_BERLIN_GROUP_1_3 import code.api.builder.AccountInformationServiceAISApi.APIMethods_AccountInformationServiceAISApi import code.api.util.APIUtil.OBPEndpoint @@ -42,6 +43,8 @@ import code.views.system.ViewDefinition import com.openbankproject.commons.util.ApiVersion class APIUtilHeavyTest extends V400ServerSetup with PropsReset { + + val bgVersion = ConstantsBG.berlinGroupVersion1.apiShortVersion feature("test APIUtil.versionIsAllowed method") { //This mean, we are only disabled the v4.0.0, all other versions should be enabled @@ -101,70 +104,72 @@ class APIUtilHeavyTest extends V400ServerSetup with PropsReset { feature("test APIUtil.getAllowedEndpoints method") { - val obpEndpointsV400: List[OBPEndpoint] = OBPAPI4_0_0.endpointsOf4_0_0.toList - val obpAllResourceDocsV400 = Implementations4_0_0.resourceDocs + scenario(s"Test the APIUtil.getAllowedEndpoints method") { + val obpEndpointsV400: List[OBPEndpoint] = OBPAPI4_0_0.endpointsOf4_0_0.toList + val obpAllResourceDocsV400 = Implementations4_0_0.resourceDocs - val allowedEndpoints: List[APIUtil.ResourceDoc] = APIUtil.getAllowedResourceDocs(obpEndpointsV400, obpAllResourceDocsV400).toList + val allowedEndpoints: List[APIUtil.ResourceDoc] = APIUtil.getAllowedResourceDocs(obpEndpointsV400, obpAllResourceDocsV400).toList - val allowedOperationIds = allowedEndpoints.map(_.operationId) + val allowedOperationIds = allowedEndpoints.map(_.operationId) - allowedOperationIds contains("OBPv4.0.0-getLogoutLink") should be (true) + allowedOperationIds contains("OBPv4.0.0-getLogoutLink") should be (true) - setPropsValues( - "api_disabled_endpoints" -> "[OBPv4.0.0-getLogoutLink,OBPv4.0.0-getMapperDatabaseInfo,OBPv4.0.0-callsLimit,OBPv4.0.0-getBanks,OBPv4.0.0-ibanChecker]", - "api_enabled_endpoints" -> "[]" - ) - val allowedEndpoints2: List[APIUtil.ResourceDoc] = APIUtil.getAllowedResourceDocs(obpEndpointsV400, obpAllResourceDocsV400).toList + setPropsValues( + "api_disabled_endpoints" -> "[OBPv4.0.0-getLogoutLink,OBPv4.0.0-getMapperDatabaseInfo,OBPv4.0.0-callsLimit,OBPv4.0.0-getBanks,OBPv4.0.0-ibanChecker]", + "api_enabled_endpoints" -> "[]" + ) + val allowedEndpoints2: List[APIUtil.ResourceDoc] = APIUtil.getAllowedResourceDocs(obpEndpointsV400, obpAllResourceDocsV400).toList - val allowedOperationIds2 = allowedEndpoints2.map(_.operationId) + val allowedOperationIds2 = allowedEndpoints2.map(_.operationId) - allowedOperationIds2 contains("OBPv4.0.0-getLogoutLink") should be (false) - allowedOperationIds2 contains("OBPv4.0.0-getMapperDatabaseInfo") should be (false) - allowedOperationIds2 contains("OBPv4.0.0-callsLimit") should be (false) + allowedOperationIds2 contains("OBPv4.0.0-getLogoutLink") should be (false) + allowedOperationIds2 contains("OBPv4.0.0-getMapperDatabaseInfo") should be (false) + allowedOperationIds2 contains("OBPv4.0.0-callsLimit") should be (false) - val bgResourceDocsV13 = APIMethods_AccountInformationServiceAISApi.resourceDocs - val bgEndpointsV13 = APIMethods_AccountInformationServiceAISApi.endpoints + val bgResourceDocsV13 = APIMethods_AccountInformationServiceAISApi.resourceDocs + val bgEndpointsV13 = APIMethods_AccountInformationServiceAISApi.endpoints - setPropsValues( - "api_disabled_endpoints" -> "[BGv1.3-createConsent,BGv1.3-deleteConsent]", - "api_enabled_endpoints" -> "[]" - ) - - val allowedEndpoints3: List[APIUtil.ResourceDoc] = APIUtil.getAllowedResourceDocs(bgEndpointsV13, bgResourceDocsV13).toList - val allowedOperationIds3 = allowedEndpoints3.map(_.operationId) - - allowedOperationIds3 contains("BGv1.3-getCardAccountTransactionList") should be (true) - allowedOperationIds3 contains("BGv1.3-createConsent") should be (false) - allowedOperationIds3 contains("BGv1.3-deleteConsent") should be (false) + setPropsValues( + "api_disabled_endpoints" -> s"[BG${bgVersion}-createConsent,BG${bgVersion}-deleteConsent]", + "api_enabled_endpoints" -> "[]" + ) - val ukResourceDocsV31 = APIMethods_AccountAccessApi.resourceDocs - val ukEndpointsV31 = APIMethods_AccountAccessApi.endpoints + val allowedEndpoints3: List[APIUtil.ResourceDoc] = APIUtil.getAllowedResourceDocs(bgEndpointsV13, bgResourceDocsV13).toList + val allowedOperationIds3 = allowedEndpoints3.map(_.operationId) - setPropsValues( - "api_disabled_endpoints" -> "[UKv3.1-createAccountAccessConsents,UKv3.1-deleteConsent]", - "api_enabled_endpoints" -> "[]" - ) + allowedOperationIds3 contains(s"BG${bgVersion}-getCardAccountTransactionList") should be (true) + allowedOperationIds3 contains(s"BG${bgVersion}-createConsent") should be (false) + allowedOperationIds3 contains(s"BG${bgVersion}-deleteConsent") should be (false) - val allowedEndpoints4: List[APIUtil.ResourceDoc] = APIUtil.getAllowedResourceDocs(ukEndpointsV31, ukResourceDocsV31).toList - val allowedOperationIds4 = allowedEndpoints4.map(_.operationId) + val ukResourceDocsV31 = APIMethods_AccountAccessApi.resourceDocs + val ukEndpointsV31 = APIMethods_AccountAccessApi.endpoints - allowedOperationIds4 contains("UKv3.1-getAccountAccessConsentsConsentId") should be (true) - allowedOperationIds4 contains("UKv3.1-createAccountAccessConsents") should be (false) - allowedOperationIds4 contains("UKv3.1-deleteConsent") should be (false) + setPropsValues( + "api_disabled_endpoints" -> "[UKv3.1-createAccountAccessConsents,UKv3.1-deleteConsent]", + "api_enabled_endpoints" -> "[]" + ) - setPropsValues( - "api_disabled_endpoints" -> "[]", - "api_enabled_endpoints" -> "[UKv3.1-createAccountAccessConsents]" - ) + val allowedEndpoints4: List[APIUtil.ResourceDoc] = APIUtil.getAllowedResourceDocs(ukEndpointsV31, ukResourceDocsV31).toList + val allowedOperationIds4 = allowedEndpoints4.map(_.operationId) - val allowedEndpoints5: List[APIUtil.ResourceDoc] = APIUtil.getAllowedResourceDocs(ukEndpointsV31, ukResourceDocsV31).toList - val allowedOperationIds5 = allowedEndpoints5.map(_.operationId) + allowedOperationIds4 contains("UKv3.1-getAccountAccessConsentsConsentId") should be (true) + allowedOperationIds4 contains("UKv3.1-createAccountAccessConsents") should be (false) + allowedOperationIds4 contains("UKv3.1-deleteConsent") should be (false) - allowedOperationIds5.length should be (1) - allowedOperationIds5 contains("UKv3.1-createAccountAccessConsents") should be (true) - allowedOperationIds5 contains("UKv3.1-deleteConsent") should be (false) + setPropsValues( + "api_disabled_endpoints" -> "[]", + "api_enabled_endpoints" -> "[UKv3.1-createAccountAccessConsents]" + ) + + val allowedEndpoints5: List[APIUtil.ResourceDoc] = APIUtil.getAllowedResourceDocs(ukEndpointsV31, ukResourceDocsV31).toList + val allowedOperationIds5 = allowedEndpoints5.map(_.operationId) + + allowedOperationIds5.length should be (1) + allowedOperationIds5 contains("UKv3.1-createAccountAccessConsents") should be (true) + allowedOperationIds5 contains("UKv3.1-deleteConsent") should be (false) + } } feature("test APIUtil.getPermissionPairFromViewDefinition method") {