[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated: Taler withdrawal: create and abort.
From: |
gnunet |
Subject: |
[libeufin] branch master updated: Taler withdrawal: create and abort. |
Date: |
Tue, 19 Sep 2023 18:30:42 +0200 |
This is an automated email from the git hooks/post-receive script.
ms pushed a commit to branch master
in repository libeufin.
The following commit(s) were added to refs/heads/master by this push:
new a0315990 Taler withdrawal: create and abort.
a0315990 is described below
commit a03159906df6342432c238a6f7956a4872498443
Author: MS <ms@taler.net>
AuthorDate: Tue Sep 19 18:30:22 2023 +0200
Taler withdrawal: create and abort.
---
.../src/main/kotlin/tech/libeufin/bank/Database.kt | 23 ++++-
.../kotlin/tech/libeufin/bank/talerWebHandlers.kt | 86 ++++++++++++----
bank/src/main/kotlin/tech/libeufin/bank/types.kt | 23 +++++
bank/src/test/kotlin/TalerApiTest.kt | 114 +++++++++++++++++++++
bank/src/test/kotlin/TalerTest.kt | 34 ------
5 files changed, 227 insertions(+), 53 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 64fe9afa..4815aa61 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -144,7 +144,7 @@ class Database(private val dbConfig: String) {
}
res.use {
if (!it.next())
- throw internalServerError("SQL RETURNING gave nothing.")
+ throw internalServerError("SQL RETURNING gave no customer_id.")
return it.getLong("customer_id")
}
}
@@ -615,6 +615,27 @@ class Database(private val dbConfig: String) {
}
}
+ /**
+ * Aborts one Taler withdrawal, only if it wasn't previously
+ * confirmed. It returns false if the UPDATE didn't succeed.
+ */
+ fun talerWithdrawalAbort(opUUID: UUID): Boolean {
+ reconnect()
+ val stmt = prepare("""
+ UPDATE taler_withdrawal_operations
+ SET aborted = true
+ WHERE withdrawal_uuid=? AND selection_done = false
+ RETURNING taler_withdrawal_id
+ """
+ )
+ stmt.setObject(1, opUUID)
+ val res = stmt.executeQuery()
+ res.use {
+ if (!it.next()) return false
+ }
+ return true
+ }
+
// Values coming from the wallet.
fun talerWithdrawalSetDetails(
opUUID: UUID,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
index d3bc8e71..e680d7df 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
@@ -24,6 +24,7 @@
package tech.libeufin.bank
+import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.server.request.*
@@ -34,6 +35,27 @@ import net.taler.wallet.crypto.Base32Crockford
import tech.libeufin.util.getBaseUrl
import java.util.*
+/**
+ * This handler factors out the checking of the query param
+ * and the retrieval of the related withdrawal database row.
+ * It throws 404 if the operation is not found, and throws 400
+ * if the query param doesn't parse into an UUID.
+ */
+private fun getWithdrawal(opIdParam: String): TalerWithdrawalOperation {
+ val opId = try {
+ UUID.fromString(opIdParam)
+ } catch (e: Exception) {
+ logger.error(e.message)
+ throw badRequest("withdrawal_id query parameter was malformed")
+ }
+ val op = db.talerWithdrawalGet(opId)
+ ?: throw notFound(
+ hint = "Withdrawal operation ${opIdParam} not found",
+ talerEc = TalerErrorCode.TALER_EC_END
+ )
+ return op
+}
+
fun Routing.talerWebHandlers() {
post("/accounts/{USERNAME}/withdrawals") {
val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
@@ -76,25 +98,13 @@ fun Routing.talerWebHandlers() {
))
return@post
}
- get("/accounts/{USERNAME}/withdrawals/{W_ID}") {
+ get("/accounts/{USERNAME}/withdrawals/{withdrawal_id}") {
val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
val accountName = call.expectUriComponent("USERNAME")
// Admin allowed to see the details
if (c.login != accountName && c.login != "admin") throw forbidden()
// Permissions passed, get the information.
- val opIdParam: String = call.request.queryParameters.get("W_ID") ?:
throw
- MissingRequestParameterException("withdrawal_id")
- val opId = try {
- UUID.fromString(opIdParam)
- } catch (e: Exception) {
- logger.error(e.message)
- throw badRequest("withdrawal_id query parameter was malformed")
- }
- val op = db.talerWithdrawalGet(opId)
- ?: throw notFound(
- hint = "Withdrawal operation ${opIdParam} not found",
- talerEc = TalerErrorCode.TALER_EC_END
- )
+ val op = getWithdrawal(call.expectUriComponent("withdrawal_id"))
call.respond(BankAccountGetWithdrawalResponse(
amount = op.amount.toString(),
aborted = op.aborted,
@@ -107,11 +117,51 @@ fun Routing.talerWebHandlers() {
))
return@get
}
- post("/accounts/{USERNAME}/withdrawals/abort") {
- throw NotImplementedError()
+ post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/abort") {
+ val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+ // Admin allowed to abort.
+ if (!call.getResourceName("USERNAME").canI(c)) throw forbidden()
+ val op = getWithdrawal(call.expectUriComponent("withdrawal_id"))
+ // Idempotency:
+ if (op.aborted) {
+ call.respondText("{}", ContentType.Application.Json)
+ return@post
+ }
+ // Op is found, it'll now fail only if previously confirmed (DB
checks).
+ if (!db.talerWithdrawalAbort(op.withdrawalUuid)) throw conflict(
+ hint = "Cannot abort confirmed withdrawal",
+ talerEc = TalerErrorCode.TALER_EC_END
+ )
+ call.respondText("{}", ContentType.Application.Json)
+ return@post
}
- post("/accounts/{USERNAME}/withdrawals/confirm") {
- throw NotImplementedError()
+ post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") {
+ val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+ // No admin allowed.
+ if(!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw
forbidden()
+ val op = getWithdrawal(call.expectUriComponent("withdrawal_id"))
+ // Checking idempotency:
+ if (op.confirmationDone) {
+ call.respondText("{}", ContentType.Application.Json)
+ return@post
+ }
+ if (op.aborted)
+ throw conflict(
+ hint = "Cannot confirm an aborted withdrawal",
+ talerEc = TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
+ )
+ // Checking that reserve GOT indeed selected.
+ if (!op.selectionDone)
+ throw LibeufinBankException(
+ httpStatus = HttpStatusCode.UnprocessableEntity,
+ talerError = TalerError(
+ hint = "Cannot confirm an unselected withdrawal",
+ code = TalerErrorCode.TALER_EC_END.code
+ ))
+ /* Confirmation conditions are all met, now put the operation
+ * to the selected state _and_ wire the funds to the exchange.
+ */
+ throw NotImplementedError("Need a database transaction now?")
}
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
index cfe58476..41064863 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
@@ -20,6 +20,7 @@
package tech.libeufin.bank
import io.ktor.http.*
+import io.ktor.server.application.*
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.util.*
@@ -406,3 +407,25 @@ data class BankAccountGetWithdrawalResponse(
val selected_reserve_pub: String? = null,
val selected_exchange_account: String? = null
)
+
+typealias ResourceName = String
+
+
+// Checks if the input Customer has the rights over ResourceName
+fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean {
+ if (c.login == this) return true
+ if (c.login == "admin" && withAdmin) return true
+ return false
+}
+
+/**
+ * Factors out the retrieval of the resource name from
+ * the URI. The resource looked for defaults to "USERNAME"
+ * as this is frequently mentioned resource along the endpoints.
+ *
+ * This helper is recommended because it returns a ResourceName
+ * type that then offers the ".canI()" helper to check if the user
+ * has the rights on the resource.
+ */
+fun ApplicationCall.getResourceName(param: String): ResourceName =
+ this.expectUriComponent(param)
\ No newline at end of file
diff --git a/bank/src/test/kotlin/TalerApiTest.kt
b/bank/src/test/kotlin/TalerApiTest.kt
new file mode 100644
index 00000000..d5334c98
--- /dev/null
+++ b/bank/src/test/kotlin/TalerApiTest.kt
@@ -0,0 +1,114 @@
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.server.testing.*
+import kotlinx.serialization.json.Json
+import org.junit.Ignore
+import org.junit.Test
+import tech.libeufin.bank.*
+import tech.libeufin.util.CryptoUtil
+import java.util.*
+
+class TalerApiTest {
+ private val customerFoo = Customer(
+ login = "foo",
+ passwordHash = CryptoUtil.hashpw("pw"),
+ name = "Foo",
+ phone = "+00",
+ email = "foo@b.ar",
+ cashoutPayto = "payto://external-IBAN",
+ cashoutCurrency = "KUDOS"
+ )
+ private val bankAccountFoo = BankAccount(
+ internalPaytoUri = "FOO-IBAN-XYZ",
+ lastNexusFetchRowId = 1L,
+ owningCustomerId = 1L,
+ hasDebt = false,
+ maxDebt = TalerAmount(10, 1, "KUDOS")
+ )
+ // Testing withdrawal abort
+ @Test
+ fun withdrawalAbort() {
+ val db = initDb()
+ val uuid = UUID.randomUUID()
+ assert(db.customerCreate(customerFoo) != null)
+ assert(db.bankAccountCreate(bankAccountFoo))
+ // insert new.
+ assert(db.talerWithdrawalCreate(
+ opUUID = uuid,
+ walletBankAccount = 1L,
+ amount = TalerAmount(1, 0)
+ ))
+ val op = db.talerWithdrawalGet(uuid)
+ assert(op?.aborted == false)
+ testApplication {
+ application(webApp)
+ client.post("/accounts/foo/withdrawals/${uuid}/abort") {
+ expectSuccess = true
+ basicAuth("foo", "pw")
+ }
+ }
+ val opAbo = db.talerWithdrawalGet(uuid)
+ assert(opAbo?.aborted == true)
+ }
+ // Testing withdrawal creation
+ @Test
+ fun withdrawalCreation() {
+ val db = initDb()
+ assert(db.customerCreate(customerFoo) != null)
+ assert(db.bankAccountCreate(bankAccountFoo))
+ testApplication {
+ application(webApp)
+ // Creating the withdrawal as if the SPA did it.
+ val r = client.post("/accounts/foo/withdrawals") {
+ basicAuth("foo", "pw")
+ contentType(ContentType.Application.Json)
+ expectSuccess = true
+ setBody("""
+ {"amount": "KUDOS:9"}
+ """.trimIndent())
+ }
+ val opId =
Json.decodeFromString<BankAccountCreateWithdrawalResponse>(r.bodyAsText())
+ // Getting the withdrawal from the bank. Throws (failing the
test) if not found.
+ client.get("/accounts/foo/withdrawals/${opId.withdrawal_id}") {
+ expectSuccess = true
+ basicAuth("foo", "pw")
+ }
+ }
+ }
+ // Testing withdrawal confirmation
+ @Ignore
+ fun withdrawalConfirmation() {
+ assert(false)
+ }
+ // Testing the generation of taler://withdraw-URIs.
+ @Test
+ fun testWithdrawUri() {
+ // Checking the taler+http://-style.
+ val withHttp = getTalerWithdrawUri(
+ "http://example.com",
+ "my-id"
+ )
+ assert(withHttp ==
"taler+http://withdraw/example.com/taler-integration/my-id")
+ // Checking the taler://-style
+ val onlyTaler = getTalerWithdrawUri(
+ "https://example.com/",
+ "my-id"
+ )
+ // Note: this tests as well that no double slashes belong to the result
+ assert(onlyTaler ==
"taler://withdraw/example.com/taler-integration/my-id")
+ // Checking the removal of subsequent slashes
+ val manySlashes = getTalerWithdrawUri(
+ "https://www.example.com//////",
+ "my-id"
+ )
+ assert(manySlashes ==
"taler://withdraw/www.example.com/taler-integration/my-id")
+ // Checking with specified port number
+ val withPort = getTalerWithdrawUri(
+ "https://www.example.com:9876",
+ "my-id"
+ )
+ assert(withPort ==
"taler://withdraw/www.example.com:9876/taler-integration/my-id")
+ }
+}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/TalerTest.kt
b/bank/src/test/kotlin/TalerTest.kt
deleted file mode 100644
index 2aa90e2d..00000000
--- a/bank/src/test/kotlin/TalerTest.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-import org.junit.Test
-import tech.libeufin.bank.getTalerWithdrawUri
-
-class TalerTest {
- // Testing the generation of taler://withdraw-URIs.
- @Test
- fun testWithdrawUri() {
- // Checking the taler+http://-style.
- val withHttp = getTalerWithdrawUri(
- "http://example.com",
- "my-id"
- )
- assert(withHttp ==
"taler+http://withdraw/example.com/taler-integration/my-id")
- // Checking the taler://-style
- val onlyTaler = getTalerWithdrawUri(
- "https://example.com/",
- "my-id"
- )
- // Note: this tests as well that no double slashes belong to the result
- assert(onlyTaler ==
"taler://withdraw/example.com/taler-integration/my-id")
- // Checking the removal of subsequent slashes
- val manySlashes = getTalerWithdrawUri(
- "https://www.example.com//////",
- "my-id"
- )
- assert(manySlashes ==
"taler://withdraw/www.example.com/taler-integration/my-id")
- // Checking with specified port number
- val withPort = getTalerWithdrawUri(
- "https://www.example.com:9876",
- "my-id"
- )
- assert(withPort ==
"taler://withdraw/www.example.com:9876/taler-integration/my-id")
- }
-}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [libeufin] branch master updated: Taler withdrawal: create and abort.,
gnunet <=