libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 4482f8d42bab26a787e154a3466be03d5d51d3b5
parent 2c3bb7c2ae984aada33ca32c88af8af0974eabc4
Author: ms <ms@taler.net>
Date:   Wed,  1 Dec 2021 10:42:41 +0100

Access API: implement create transactions.

Diffstat:
Mcli/bin/libeufin-cli | 43++++++++++++++++++++++++++++++++++++++++++-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt | 46+++++++++++++++++++++++++++++++++++++---------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt | 11+++++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 31+++++++++++++++++++++++++++++--
4 files changed, 119 insertions(+), 12 deletions(-)

diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli @@ -1187,11 +1187,52 @@ def sandbox_bankaccount(ctx): # This group deals with the new Access API # and the 'demobank' model. -@sandbox.group("demobank", help="manage customers") +@sandbox.group( + "demobank", + help="Subcommands for the 'demobank' model and the Access API." +) @click.pass_context def sandbox_demobank(ctx): pass +@sandbox_demobank.command("new-transaction", help="Initiate a new transaction.") +@click.option( + "--bank-account", + help="Label of the bank account to be debited for the transaction.", + required=True +) +# Including the subject in the payto to match the +# payto returned by the merchant backend helper program +# to create tip reserves. +@click.option( + "--payto-with-subject", + help="Payto address including the subject as a query parameter.", + required=True +) +@click.option( + "--amount", + help="Amount to transfer, in the $currency:X.Y format.", + required=True +) +@click.pass_obj +def sandbox_demobank_new_transaction(obj, bank_account, payto_with_subject, amount): + # expected to include the demobank name. + sandbox_base_url = obj.require_sandbox_base_url() + url = urljoin_nodrop(sandbox_base_url, f"/access-api/accounts/{bank_account}/transactions") + try: + body = dict(paytoUri=payto_with_subject, amount=amount) + resp = post( + url, + json=body, + auth=auth.HTTPBasicAuth(obj.username, obj.password), + ) + except Exception as e: + print(e) + print("Could not reach Sandbox at " + url) + exit(1) + + check_response_status(resp) + @sandbox_demobank.command("info", help="Return basic information of a bank account") @click.option( "--bank-account", diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt @@ -226,7 +226,7 @@ fun wireTransfer( return wireTransfer( debitAccount = args.first, creditAccount = args.second, - Demobank = args.third, + demobank = args.third, subject = subject, amount = amountObj.amount.toPlainString() ) @@ -244,7 +244,7 @@ fun wireTransfer( fun wireTransfer( debitAccount: BankAccountEntity, creditAccount: BankAccountEntity, - Demobank: DemobankConfigEntity, + demobank: DemobankConfigEntity, subject: String, amount: String, ): String { @@ -262,12 +262,12 @@ fun wireTransfer( debtorName = getPersonNameFromCustomer(debitAccount.owner) this.subject = subject this.amount = amount - this.currency = Demobank.currency + this.currency = demobank.currency date = timeStamp accountServicerReference = transactionRef account = creditAccount direction = "CRDT" - this.demobank = Demobank + this.demobank = demobank } BankAccountTransactionEntity.new { creditorIban = creditAccount.iban @@ -278,12 +278,12 @@ fun wireTransfer( debtorName = getPersonNameFromCustomer(debitAccount.owner) this.subject = subject this.amount = amount - this.currency = Demobank.currency + this.currency = demobank.currency date = timeStamp accountServicerReference = transactionRef account = debitAccount direction = "DBIT" - demobank = Demobank + this.demobank = demobank } } return transactionRef @@ -306,10 +306,10 @@ fun getBankAccountFromPayto(paytoUri: String): BankAccountEntity { fun getBankAccountFromIban(iban: String): BankAccountEntity { return transaction { - BankAccountEntity.find(BankAccountsTable.iban eq iban) - }.firstOrNull() ?: throw SandboxError( + BankAccountEntity.find(BankAccountsTable.iban eq iban).firstOrNull() + } ?: throw SandboxError( HttpStatusCode.NotFound, - "Did not find a bank account for ${iban}" + "Did not find a bank account for $iban" ) } @@ -385,4 +385,32 @@ fun getEbicsSubscriberFromDetails(userID: String, partnerID: String, hostID: Str "Ebics subscriber not found" ) } +} + +/** + * This helper tries to: + * 1. Authenticate the client. + * 2. Extract the bank account's label from the request's path + * 3. Return the bank account DB object if the client has access to it. + */ +fun getBankAccountWithAuth(call: ApplicationCall): BankAccountEntity { + val username = call.request.basicAuth() + val accountAccessed = call.getUriComponent("account_name") + val demobank = ensureDemobank(call) + val bankAccount = transaction { + val res = BankAccountEntity.find { + (BankAccountsTable.label eq accountAccessed).and( + BankAccountsTable.demoBank eq demobank.id + ) + }.firstOrNull() + res + } ?: throw notFound("Account '$accountAccessed' not found") + // Check rights. + if ( + WITH_AUTH + && (bankAccount.owner != username && username != "admin") + ) throw forbidden( + "Customer '$username' cannot access bank account '$accountAccessed'" + ) + return bankAccount } \ No newline at end of file diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt @@ -122,3 +122,13 @@ data class TalerWithdrawalSelection( val reserve_pub: String, val selected_exchange: String? ) + +data class NewTransactionReq( + /** + * This Payto address must contain the wire transfer + * subject among its query parameters -- 'message' parameter. + */ + val paytoUri: String, + // $currency:X.Y format + val amount: String +) +\ No newline at end of file diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -84,7 +84,7 @@ val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") private val currencyEnv: String? = System.getenv("LIBEUFIN_SANDBOX_CURRENCY") const val SANDBOX_DB_ENV_VAR_NAME = "LIBEUFIN_SANDBOX_DB_CONNECTION" private val adminPassword: String? = System.getenv("LIBEUFIN_SANDBOX_ADMIN_PASSWORD") -private var WITH_AUTH = true +var WITH_AUTH = true // Needed by helpers too, hence not making it private. data class SandboxError( val statusCode: HttpStatusCode, @@ -1070,6 +1070,33 @@ val sandboxApp: Application.() -> Unit = { } // Talk to Web UI. route("/access-api") { + post("/accounts/{account_name}/transactions") { + val bankAccount = getBankAccountWithAuth(call) + val req = call.receive<NewTransactionReq>() + val payto = parsePayto(req.paytoUri) + val amount = parseAmount(req.amount) + /** + * Need a transaction block only to let the + * 'demoBank' field of 'bankAccount' accessed. + * + * This could be fixed by making 'getBankAccountWithAuth()' + * return a pair, consisting of the bank account and the demobank + * hosting it. + */ + transaction { + wireTransfer( + debitAccount = bankAccount, + creditAccount = getBankAccountFromIban(payto.iban), + demobank = bankAccount.demoBank, + subject = payto.message ?: throw badRequest( + "'message' query parameter missing in Payto address" + ), + amount = amount.amount.toPlainString() + ) + } + call.respond(object {}) + return@post + } // Information about one withdrawal. get("/accounts/{account_name}/withdrawals/{withdrawal_id}") { val op = getWithdrawalOperation(call.getUriComponent("withdrawal_id")) @@ -1178,7 +1205,7 @@ val sandboxApp: Application.() -> Unit = { "Cannot transfer funds without reserve public key." ), // provide the currency. - Demobank = ensureDemobank(call) + demobank = ensureDemobank(call) ) wo.confirmationDone = true }