libeufin

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

commit 8e45d1be81105acd292502a02c03e001a60f6327
parent 94c6f605ef7ddeb9aa3453fba044a2a733e46ed4
Author: Antoine A <>
Date:   Thu,  5 Oct 2023 12:28:09 +0000

Fix taler-wire-gateway auth and improve test

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt | 47++++++++++++++++++++++++++++-------------------
Mbank/src/test/kotlin/TalerApiTest.kt | 82++++++++++++++++++++++++++++++++++++++++---------------------------------------
2 files changed, 70 insertions(+), 59 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt @@ -37,14 +37,33 @@ import kotlin.math.abs private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus") fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) { + /** Authenticate and check access rights */ + suspend fun ApplicationCall.authCheck(scope: TokenScope, withAdmin: Boolean): String { + val authCustomer = authenticateBankRequest(db, scope) ?: throw unauthorized() + val username = getResourceName("USERNAME") + if (!username.canI(authCustomer, withAdmin)) throw forbidden() + return username + } + + /** Retrieve the bank account for the selected username*/ + suspend fun ApplicationCall.bankAccount(): BankAccount { + val username = getResourceName("USERNAME") + val customer = db.customerGetFromLogin(username) ?: throw notFound( + hint = "Customer $username not found", + talerEc = TalerErrorCode.TALER_EC_END // FIXME: need EC. + ) + val bankAccount = db.bankAccountGetFromOwnerId(customer.expectRowId()) + ?: throw internalServerError("Exchange does not have a bank account") + return bankAccount + } + get("/taler-wire-gateway/config") { call.respond(TWGConfigResponse(currency = ctx.currency)) return@get } post("/accounts/{USERNAME}/taler-wire-gateway/transfer") { - val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized() - if (!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw forbidden() + call.authCheck(TokenScope.readwrite, true) val req = call.receive<TransferRequest>() // Checking for idempotency. val maybeDoneAlready = db.talerTransferGetFromUid(req.request_uid.encoded) @@ -73,8 +92,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) val internalCurrency = ctx.currency if (internalCurrency != req.amount.currency) throw badRequest("Currency mismatch: $internalCurrency vs ${req.amount.currency}") - val exchangeBankAccount = db.bankAccountGetFromOwnerId(c.expectRowId()) - ?: throw internalServerError("Exchange does not have a bank account") + val exchangeBankAccount = call.bankAccount() val transferTimestamp = Instant.now() val dbRes = db.talerTransferCreate( req = req, @@ -103,18 +121,11 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) } get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") { - val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw unauthorized() - val accountName = call.getResourceName("USERNAME") - if (!accountName.canI(c, withAdmin = true)) throw forbidden() + val username = call.authCheck(TokenScope.readonly, true) val params = getHistoryParams(call.request) - val accountCustomer = db.customerGetFromLogin(accountName) ?: throw notFound( - hint = "Customer $accountName not found", - talerEc = TalerErrorCode.TALER_EC_END // FIXME: need EC. - ) - val bankAccount = db.bankAccountGetFromOwnerId(accountCustomer.expectRowId()) - ?: throw internalServerError("Customer '$accountName' lacks bank account.") + val bankAccount = call.bankAccount() if (!bankAccount.isTalerExchange) throw forbidden("History is not related to a Taler exchange.") - + val resp = IncomingHistory(credit_account = bankAccount.internalPaytoUri) var start = params.start var delta = params.delta @@ -135,7 +146,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) // This should usually not happen in the first place, // because transactions to the exchange without a valid // reserve pub should be bounced. - logger.warn("exchange account ${c.login} contains invalid incoming transaction ${it.expectRowId()}") + logger.warn("exchange account $username contains invalid incoming transaction ${it.expectRowId()}") } else { // Register new transacation resp.incoming_transactions.add( @@ -162,8 +173,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) } post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") { - val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized() - if (!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw forbidden() + call.authCheck(TokenScope.readwrite, false); val req = call.receive<AddIncomingRequest>() val internalCurrency = ctx.currency if (req.amount.currency != internalCurrency) @@ -182,8 +192,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) "debit_account not found", TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT ) - val exchangeAccount = db.bankAccountGetFromOwnerId(c.expectRowId()) - ?: throw internalServerError("exchange bank account not found, despite it's a customer") + val exchangeAccount = call.bankAccount() val txTimestamp = Instant.now() val op = BankInternalTransaction( debtorAccountId = walletAccount.expectRowId(), diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt @@ -1,6 +1,7 @@ import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.client.statement.* +import io.ktor.client.HttpClient import io.ktor.http.* import io.ktor.server.testing.* import kotlinx.serialization.json.Json @@ -50,9 +51,7 @@ class TalerApiTest { cashoutCurrency = "KUDOS" ) - // Testing the POST /transfer call from the TWG API. - @Test - fun transfer() { + fun commonSetup(): Pair<Database, BankApplicationContext> { val db = initDb() val ctx = getTestContext() // Creating the exchange and merchant accounts first. @@ -60,12 +59,44 @@ class TalerApiTest { assertNotNull(db.bankAccountCreate(bankAccountFoo)) assertNotNull(db.customerCreate(customerBar)) assertNotNull(db.bankAccountCreate(bankAccountBar)) + return Pair(db, ctx) + } + + // Test endpoint is correctly authenticated + suspend fun authRoutine(client: HttpClient, path: String, method: HttpMethod = HttpMethod.Post) { + // No body because authentication must happen before parsing the body + + // Unknown account + client.request(path) { + this.method = method + basicAuth("unknown", "password") + }.assertStatus(HttpStatusCode.Unauthorized) + + // Wrong password + client.request(path) { + this.method = method + basicAuth("foo", "wrong_password") + }.assertStatus(HttpStatusCode.Unauthorized) + + // Wrong account + client.request(path) { + this.method = method + basicAuth("bar", "secret") + }.assertStatus(HttpStatusCode.Forbidden) + } + + // Testing the POST /transfer call from the TWG API. + @Test + fun transfer() { + val (db, ctx) = commonSetup() // Do POST /transfer. testApplication { application { corebankWebApp(db, ctx) } + authRoutine(client, "/accounts/foo/taler-wire-gateway/transfer") + val valid_req = json { "request_uid" to randHashCode() "amount" to "KUDOS:55" @@ -74,24 +105,6 @@ class TalerApiTest { "credit_account" to "${stripIbanPayto(bankAccountBar.internalPaytoUri)}" }; - // Unkown account - client.post("/accounts/foo/taler-wire-gateway/transfer") { - basicAuth("unknown", "password") - jsonBody(valid_req) - }.assertStatus(HttpStatusCode.Unauthorized) - - // Wrong password - client.post("/accounts/foo/taler-wire-gateway/transfer") { - basicAuth("foo", "password") - jsonBody(valid_req) - }.assertStatus(HttpStatusCode.Unauthorized) - - // Wrong account - client.post("/accounts/foo/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody(valid_req) - }.assertStatus(HttpStatusCode.Forbidden) - // Checking exchange debt constraint. client.post("/accounts/foo/taler-wire-gateway/transfer") { basicAuth("foo", "pw") @@ -184,12 +197,7 @@ class TalerApiTest { */ @Test fun historyIncoming() { - val db = initDb() - val ctx = getTestContext() - assertNotNull(db.customerCreate(customerFoo)) - assertNotNull(db.bankAccountCreate(bankAccountFoo)) - assertNotNull(db.customerCreate(customerBar)) - assertNotNull(db.bankAccountCreate(bankAccountBar)) + val (db, ctx) = commonSetup() // Give Foo reasonable debt allowance: assert( db.bankAccountSetMaxDebt( @@ -204,6 +212,8 @@ class TalerApiTest { corebankWebApp(db, ctx) } + authRoutine(client, "/accounts/foo/taler-wire-gateway/history/incoming", HttpMethod.Get) + // Check error when no transactions client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=7") { basicAuth("bar", "secret") @@ -283,12 +293,7 @@ class TalerApiTest { // Testing the /admin/add-incoming call from the TWG API. @Test fun addIncoming() { - val db = initDb() - val ctx = getTestContext() - assertNotNull(db.customerCreate(customerFoo)) - assertNotNull(db.bankAccountCreate(bankAccountFoo)) - assertNotNull(db.customerCreate(customerBar)) - assertNotNull(db.bankAccountCreate(bankAccountBar)) + val (db, ctx) = commonSetup() // Give Bar reasonable debt allowance: assert(db.bankAccountSetMaxDebt( 2L, @@ -298,6 +303,9 @@ class TalerApiTest { application { corebankWebApp(db, ctx) } + + authRoutine(client, "/accounts/foo/taler-wire-gateway/admin/add-incoming") + val valid_req = json { "amount" to "KUDOS:44" "reserve_pub" to randEddsaPublicKey() @@ -428,13 +436,7 @@ class TalerApiTest { // Testing withdrawal confirmation @Test fun withdrawalConfirmation() { - val db = initDb() - val ctx = getTestContext() - // Creating Foo as the wallet owner and Bar as the exchange. - assertNotNull(db.customerCreate(customerFoo)) - assertNotNull(db.bankAccountCreate(bankAccountFoo)) - assertNotNull(db.customerCreate(customerBar)) - assertNotNull(db.bankAccountCreate(bankAccountBar)) + val (db, ctx) = commonSetup() // Artificially making a withdrawal operation for Foo. val uuid = UUID.randomUUID()