libeufin

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

commit 24838e2d9451d90e59d211cb9cd13baadb278742
parent 7e4b86e682a48edb64825efb1c1f8d0413a467a9
Author: MS <ms@taler.net>
Date:   Fri,  8 Jul 2022 13:10:58 +0200

(re)implement /admin/add-incoming

Diffstat:
Mcli/bin/libeufin-cli | 8+++++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 12++++++++++--
Mutil/src/main/kotlin/HTTP.kt | 7+++++++
4 files changed, 99 insertions(+), 6 deletions(-)

diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli @@ -1333,12 +1333,18 @@ def sandbox_demobank_delete(obj, bank_account): default="", help="Person name", ) +@click.option( + "--iban", + help="Uses this IBAN, instead of a random one.", +) @click.pass_obj -def sandbox_demobank_register(obj, public, name): +def sandbox_demobank_register(obj, public, name, iban): url = obj.access_api_url ("/testing/register") req = dict(username=obj.username, password=obj.password, isPublic=public) if name != "": req.update(name=name) + if iban: + req.update(iban=iban) try: resp = post( url, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt @@ -22,15 +22,18 @@ package tech.libeufin.nexus import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.ktor.application.ApplicationCall import io.ktor.application.call +import io.ktor.client.* +import io.ktor.client.features.* +import io.ktor.client.request.* import io.ktor.content.TextContent -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode +import io.ktor.http.* import io.ktor.request.receive import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.routing.Route import io.ktor.routing.get import io.ktor.routing.post +import io.ktor.util.* import org.jetbrains.exposed.dao.Entity import org.jetbrains.exposed.dao.id.IdTable import org.jetbrains.exposed.sql.* @@ -39,7 +42,7 @@ import tech.libeufin.nexus.bankaccount.addPaymentInitiation import tech.libeufin.nexus.iso20022.* import tech.libeufin.nexus.server.* import tech.libeufin.util.* -import java.net.URLEncoder +import java.net.URL import kotlin.math.abs import kotlin.math.min @@ -455,6 +458,71 @@ private suspend fun historyIncoming(call: ApplicationCall) { return call.respond(TextContent(customConverter(history), ContentType.Application.Json)) } +/** + * This call proxies /admin/add/incoming to the Sandbox, + * which is the service keeping the transactions ledger. + * The credentials are ASSUMED to be exchange/x (user/pass). + * + * In the future, a dedicate "add-incoming" facade should + * be provided, offering the mean to store the credentials + * at configuration time. + * + */ +private suspend fun addIncoming(call: ApplicationCall) { + val facadeId = ensureNonNull(call.parameters["fcid"]) + val currentBody = call.receive<String>() + val sandboxUrl = transaction { + val f = FacadeEntity.findByName(facadeId) ?: throw notFound("facade $facadeId not found") + val state = FacadeStateEntity.find { + FacadeStateTable.facade eq f.id + }.firstOrNull() ?: throw internalServerError("facade $facadeId has no state!") + val conn = NexusBankConnectionEntity.findByName(state.bankConnection) ?: throw internalServerError( + "state of facade $facadeId has no bank connection!" + ) + val ebicsData = NexusEbicsSubscribersTable.select { + NexusEbicsSubscribersTable.nexusBankConnection eq conn.id + }.firstOrNull() ?: throw internalServerError( + "Connection '${conn.connectionId}' doesn't have EBICS" + ) + // Resort Sandbox URL from EBICS endpoint. + val sandboxUrl = URL(ebicsData[NexusEbicsSubscribersTable.ebicsURL]) + // NOTE: the exchange username must be 'exchange', at the Sandbox. + return@transaction url { + protocol = URLProtocol(sandboxUrl.protocol, 80) + host = sandboxUrl.host + if (sandboxUrl.port != 80) port = sandboxUrl.port + path( + "demobanks", + "default", + "taler-wire-gateway", + "exchange", + "admin", + "add-incoming" + ) + + } + } + val client = HttpClient { followRedirects = true } + val resp = try { + client.post<String>( + urlString = sandboxUrl, + block = { + this.body = currentBody + this.header( + "Authorization", + buildBasicAuthLine("exchange", "x") + ) + this.header("Content-Type", "application/json") + } + ) + } catch (e: ClientRequestException) { + logger.error("Proxying /admin/add/incoming to the Sandbox failed: $e") + } catch (e: Exception) { + logger.error("Could not proxy /admin/add/incoming to the Sandbox: $e") + } + call.respond(resp) +} + private fun getCurrency(facadeName: String): String { return transaction { getFacadeState(facadeName).currency @@ -487,6 +555,10 @@ fun talerFacadeRoutes(route: Route) { historyIncoming(call) return@get } + route.post("/admin/add-incoming") { + addIncoming(call) + return@post + } route.get("") { call.respondText("Hello, this is a Taler Facade") return@get diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -1069,8 +1069,8 @@ val sandboxApp: Application.() -> Unit = { call.respond(getJsonFromDemobankConfig(demobank)) return@get } - route("/demobanks/{demobankid}") { + route("/demobanks/{demobankid}") { // NOTE: TWG assumes that username == bank account label. route("/taler-wire-gateway") { post("/{exchangeUsername}/admin/add-incoming") { @@ -1082,7 +1082,15 @@ val sandboxApp: Application.() -> Unit = { ) } logger.debug("TWG add-incoming passed authentication") - val body = call.receiveJson<TWGAdminAddIncoming>() + val body = try { + call.receiveJson<TWGAdminAddIncoming>() + } catch (e: Exception) { + logger.error("/admin/add-incoming failed at parsing the request body") + throw SandboxError( + HttpStatusCode.BadRequest, + "Invalid request" + ) + } transaction { val demobank = ensureDemobank(call) val bankAccountCredit = getBankAccountFromLabel(username, demobank) diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt @@ -153,6 +153,13 @@ fun getAuthorizationHeader(request: ApplicationRequest): String { ) } +// Builds the Authorization:-header value, given the credentials. +fun buildBasicAuthLine(username: String, password: String): String { + val ret = "Basic " + val cred = "$username:$password" + val enc = bytesToBase64(cred.toByteArray(Charsets.UTF_8)) + return ret+enc +} /** * This helper function parses a Authorization:-header line, decode the credentials * and returns a pair made of username and hashed (sha256) password. The hashed value