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:
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()