commit 10bc544c731291090683bc67e421c1740f6dc269
parent b92a628982bde6582269734713183f3760e1f5e6
Author: ms <ms@taler.net>
Date: Wed, 22 Mar 2023 14:27:06 +0100
pointing demobanks
Avoid relying on the default demobank along the
HTTP handlers, to honor the demobanks multitenancy.
This change aims to also make the code compatible
with DD38. Polishing (Access API) access control
to bank accounts too.
Diffstat:
8 files changed, 147 insertions(+), 118 deletions(-)
diff --git a/nexus/src/test/kotlin/SandboxBankAccountTest.kt b/nexus/src/test/kotlin/SandboxBankAccountTest.kt
@@ -25,7 +25,7 @@ class SandboxBankAccountTest {
* the payment is still pending (= not booked), the pending
* transactions must be included in the calculation.
*/
- var bankBalance = getBalance("admin", true)
+ var bankBalance = getBalance("admin")
assert(bankBalance == parseDecimal("-1"))
wireTransfer(
"foo",
@@ -34,7 +34,7 @@ class SandboxBankAccountTest {
"Show up in logging!",
"TESTKUDOS:5"
)
- bankBalance = getBalance("admin", true)
+ bankBalance = getBalance("admin")
assert(bankBalance == parseDecimal("4"))
// Trigger Insufficient funds case for users.
try {
@@ -63,9 +63,9 @@ class SandboxBankAccountTest {
assert(e.statusCode == HttpStatusCode.PreconditionFailed)
}
// Check balance didn't change for both parties.
- bankBalance = getBalance("admin", true)
+ bankBalance = getBalance("admin")
assert(bankBalance == parseDecimal("4"))
- val fooBalance = getBalance("foo", true)
+ val fooBalance = getBalance("foo")
assert(fooBalance == parseDecimal("-4"))
}
}
diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
@@ -44,7 +44,6 @@ class SandboxCircuitApiTest {
expectSuccess = true
basicAuth("foo", "foo")
}
- println(R.bodyAsText())
val mapper = ObjectMapper()
val respJson = mapper.readTree(R.bodyAsText())
val creditAmount = respJson.get("amount_credit").asText()
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
@@ -217,7 +217,7 @@ fun circuitApi(circuitRoute: Route) {
// Abort a cash-out operation.
circuitRoute.post("/cashouts/{uuid}/abort") {
call.request.basicAuth() // both admin and author allowed
- val arg = call.getUriComponent("uuid")
+ val arg = call.expectUriComponent("uuid")
// Parse and check the UUID.
val maybeUuid = parseUuid(arg)
val maybeOperation = transaction {
@@ -244,7 +244,7 @@ fun circuitApi(circuitRoute: Route) {
if (user == "admin" || user == "bank")
throw conflict("Institutional user '$user' shouldn't confirm any cash-out.")
// Get the operation identifier.
- val operationUuid = parseUuid(call.getUriComponent("uuid"))
+ val operationUuid = parseUuid(call.expectUriComponent("uuid"))
val op = transaction {
CashoutOperationEntity.find {
uuid eq operationUuid
@@ -302,7 +302,7 @@ fun circuitApi(circuitRoute: Route) {
// Retrieve the status of a cash-out operation.
circuitRoute.get("/cashouts/{uuid}") {
call.request.basicAuth() // both admin and author
- val operationUuid = call.getUriComponent("uuid")
+ val operationUuid = call.expectUriComponent("uuid")
// Parse and check the UUID.
val maybeUuid = parseUuid(operationUuid)
// Get the operation from the database.
@@ -449,7 +449,11 @@ fun circuitApi(circuitRoute: Route) {
" but ${amountCredit.amount} was specified.")
}
// check that the balance is sufficient
- val balance = getBalance(user, withPending = true)
+ val balance = getBalance(
+ user,
+ demobank.name,
+ withPending = true
+ )
val balanceCheck = balance - amountDebitAsNumber
if (balanceCheck < BigDecimal.ZERO && balanceCheck.abs() > BigDecimal(demobank.config.usersDebtLimit))
throw SandboxError(
@@ -547,7 +551,7 @@ fun circuitApi(circuitRoute: Route) {
// Get Circuit-relevant account data.
circuitRoute.get("/accounts/{resourceName}") {
val username = call.request.basicAuth()
- val resourceName = call.getUriComponent("resourceName")
+ val resourceName = call.expectUriComponent("resourceName")
throwIfInstitutionalName(resourceName)
if (!allowOwnerOrAdmin(username, resourceName)) throw forbidden(
"User $username has no rights over $resourceName"
@@ -593,6 +597,7 @@ fun circuitApi(circuitRoute: Route) {
"%${maybeFilter}%"
} else "%"
val customers = mutableListOf<Any>()
+ val demobank = ensureDemobank(call)
transaction {
DemobankCustomerEntity.find{
// like() is case insensitive.
@@ -602,10 +607,13 @@ fun circuitApi(circuitRoute: Route) {
val username = it.username
val name = it.name
val balance = getBalanceForJson(
- getBalance(it.username),
- getDefaultDemobank().config.currency
+ getBalance(it.username, demobank.name),
+ demobank.config.currency
+ )
+ val debitThreshold = getMaxDebitForUser(
+ it.username,
+ demobank.name
)
- val debitThreshold = getMaxDebitForUser(it.username)
})
}
}
@@ -620,7 +628,7 @@ fun circuitApi(circuitRoute: Route) {
// Change password.
circuitRoute.patch("/accounts/{customerUsername}/auth") {
val username = call.request.basicAuth()
- val customerUsername = call.getUriComponent("customerUsername")
+ val customerUsername = call.expectUriComponent("customerUsername")
throwIfInstitutionalName(customerUsername)
if (!allowOwnerOrAdmin(username, customerUsername)) throw forbidden(
"User $username has no rights over $customerUsername"
@@ -644,7 +652,7 @@ fun circuitApi(circuitRoute: Route) {
val username = call.request.basicAuth()
if (username == null)
throw internalServerError("Authentication disabled, don't have a default for this request.")
- val resourceName = call.getUriComponent("resourceName")
+ val resourceName = call.expectUriComponent("resourceName")
throwIfInstitutionalName(resourceName)
if(!allowOwnerOrAdmin(username, resourceName)) throw forbidden(
"User $username has no rights over $resourceName"
@@ -719,7 +727,8 @@ fun circuitApi(circuitRoute: Route) {
username = req.username,
password = req.password,
name = req.name,
- iban = req.internal_iban
+ iban = req.internal_iban,
+ demobank = ensureDemobank(call).name
)
newAccount.customer.phone = req.contact_data.phone
newAccount.customer.email = req.contact_data.email
@@ -736,7 +745,7 @@ fun circuitApi(circuitRoute: Route) {
// Only Admin and only when balance is zero.
circuitRoute.delete("/accounts/{resourceName}") {
call.request.basicAuth(onlyAdmin = true)
- val resourceName = call.getUriComponent("resourceName")
+ val resourceName = call.expectUriComponent("resourceName")
throwIfInstitutionalName(resourceName)
val customer = getCustomer(resourceName)
val bankAccount = getBankAccountFromLabel(
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
@@ -281,7 +281,8 @@ fun buildCamtString(
subscriberIban: String,
history: MutableList<RawPayment>,
balancePrcd: BigDecimal, // Balance up to freshHistory (excluded).
- balanceClbd: BigDecimal
+ balanceClbd: BigDecimal,
+ currency: String
): SandboxCamt {
/**
* ID types required:
@@ -300,7 +301,6 @@ fun buildCamtString(
val zonedDateTime = camtCreationTime.toZonedString()
val creationTimeMillis = camtCreationTime.toInstant().toEpochMilli()
val messageId = "sandbox-${creationTimeMillis / 1000}-${getRandomString(10)}"
- val currency = getDefaultDemobank().config.currency
val camtMessage = constructXml(indent = true) {
root("Document") {
@@ -561,7 +561,8 @@ private fun constructCamtResponse(
bankAccount.iban,
history,
balancePrcd = prcdBalance,
- balanceClbd = clbdBalance
+ balanceClbd = clbdBalance,
+ bankAccount.demoBank.config.currency
)
val paymentsList: String = if (logger.isDebugEnabled) {
var ret = " It includes the payments:"
@@ -713,8 +714,9 @@ private fun parsePain001(paymentRequest: String): PainParseResult {
* payments outside of the running Sandbox and (2) may ease
* tests where the preparation logic can skip creating also
* the receiver account. */
-private fun handleCct(paymentRequest: String,
- requestingSubscriber: EbicsSubscriberEntity
+private fun handleCct(
+ paymentRequest: String,
+ requestingSubscriber: EbicsSubscriberEntity
) {
val parseResult = parsePain001(paymentRequest)
logger.debug("Handling Pain.001: ${parseResult.pmtInfId}, " +
@@ -752,7 +754,7 @@ private fun handleCct(paymentRequest: String,
logger.warn("Although PAIN validated, BigDecimal didn't parse its amount (${parseResult.amount})!")
throw EbicsProcessingError("The CCT request contains an invalid amount: ${parseResult.amount}")
}
- if (maybeDebit(bankAccount.label, maybeAmount))
+ if (maybeDebit(bankAccount.label, maybeAmount, bankAccount.demoBank.name))
throw EbicsAmountCheckError("The requested amount (${parseResult.amount}) would exceed the debit threshold")
// Get the two parties.
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
@@ -82,8 +82,8 @@ fun insertNewAccount(username: String,
password: String,
name: String? = null, // tests do not usually give one.
iban: String? = null,
- isPublic: Boolean = false,
- demobank: String = "default"): AccountPair {
+ demobank: String = "default",
+ isPublic: Boolean = false): AccountPair {
requireValidResourceName(username)
// Forbid institutional usernames.
if (username == "bank" || username == "admin") {
@@ -163,7 +163,7 @@ fun allowOwnerOrAdmin(username: String?, bankAccountLabel: String): Boolean {
*
* Return:
* - null if the authentication is disabled (during tests, for example).
- * This facilitates tests because allows requests to lack entirely a
+ * This facilitates tests because allows requests to lack entirely an
* Authorization header.
* - the username of the authenticated user
* - throw exception when the authentication fails
@@ -365,10 +365,12 @@ fun getBankAccountFromLabel(
withBankFault
)
}
+
+// Get bank account DAO, given its name and demobank.
fun getBankAccountFromLabel(
label: String,
demobank: DemobankConfigEntity,
- withBankFault: Boolean = false
+ withBankFault: Boolean = false // documented along the other same-named function.
): BankAccountEntity {
val maybeBankAccount = transaction {
BankAccountEntity.find(
@@ -408,7 +410,7 @@ fun BankAccountEntity.bonus(amount: String) {
}
fun ensureDemobank(call: ApplicationCall): DemobankConfigEntity {
- return ensureDemobank(call.getUriComponent("demobankid"))
+ return ensureDemobank(call.expectUriComponent("demobankid"))
}
fun ensureDemobank(name: String): DemobankConfigEntity {
@@ -444,22 +446,6 @@ fun getEbicsSubscriberFromDetails(userID: String, partnerID: String, hostID: Str
}
/**
- * 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 = getBankAccountFromLabel(accountAccessed, demobank)
- if (WITH_AUTH && (bankAccount.owner != username && username != "admin"))
- throw forbidden("Customer '$username' cannot access bank account '$accountAccessed'")
- return bankAccount
-}
-
-/**
* Compress, encrypt, encode a EBICS payload. The payload
* is assumed to be a Zip archive with only one entry.
* Return the customer key (second element) along the data.
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -48,7 +48,6 @@ import io.ktor.server.plugins.callloging.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.util.date.*
import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.Logger
@@ -61,10 +60,6 @@ import java.math.BigDecimal
import java.net.URL
import java.security.interfaces.RSAPublicKey
import javax.xml.bind.JAXBContext
-import kotlin.reflect.KProperty0
-import kotlin.reflect.KProperty1
-import kotlin.reflect.full.declaredMemberProperties
-import kotlin.reflect.full.declaredMembers
import kotlin.system.exitProcess
val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox")
@@ -269,7 +264,8 @@ class Camt053Tick : CliktCommand(
accountIter.iban,
newStatements[accountIter.label]!!,
balanceClbd = balanceClbd,
- balancePrcd = lastBalance
+ balancePrcd = lastBalance,
+ currency = accountIter.demoBank.config.currency
)
BankAccountStatementEntity.new {
statementId = camtData.messageId
@@ -717,7 +713,7 @@ val sandboxApp: Application.() -> Unit = {
// Information about one bank account.
get("/admin/bank-accounts/{label}") {
val username = call.request.basicAuth()
- val label = call.getUriComponent("label")
+ val label = call.expectUriComponent("label")
val ret = transaction {
val demobank = getDefaultDemobank()
val bankAccount = getBankAccountFromLabel(label, demobank)
@@ -1069,9 +1065,7 @@ val sandboxApp: Application.() -> Unit = {
}
// Process one EBICS request
post("/ebicsweb") {
- try {
- call.ebicsweb()
- }
+ try { call.ebicsweb() }
/**
* The catch blocks try to extract a EBICS error message from the
* exception type being handled. NOT logging under each catch block
@@ -1139,17 +1133,13 @@ val sandboxApp: Application.() -> Unit = {
// NOTE: TWG assumes that username == bank account label.
route("/taler-wire-gateway") {
post("/{exchangeUsername}/admin/add-incoming") {
- val username = call.getUriComponent("exchangeUsername")
+ val username = call.expectUriComponent("exchangeUsername")
val usernameAuth = call.request.basicAuth()
- if (username != usernameAuth) {
- throw forbidden(
- "Bank account name and username differ: $username vs $usernameAuth"
- )
- }
+ if (username != usernameAuth)
+ throw forbidden("Bank account name and username differ: $username vs $usernameAuth")
logger.debug("TWG add-incoming passed authentication")
- val body = try {
- call.receive<TWGAdminAddIncoming>()
- } catch (e: Exception) {
+ val body = try { call.receive<TWGAdminAddIncoming>() }
+ catch (e: Exception) {
logger.error("/admin/add-incoming failed at parsing the request body")
throw SandboxError(
HttpStatusCode.BadRequest,
@@ -1239,7 +1229,7 @@ val sandboxApp: Application.() -> Unit = {
)
}
val demobank = ensureDemobank(call)
- var captcha_page = demobank.config.captchaUrl
+ val captcha_page = demobank.config.captchaUrl
if (captcha_page == null) logger.warn("CAPTCHA URL not found")
val ret = TalerWithdrawalStatus(
selection_done = maybeWithdrawalOp.selectionDone,
@@ -1259,7 +1249,16 @@ val sandboxApp: Application.() -> Unit = {
// Talk to Web UI.
route("/access-api") {
post("/accounts/{account_name}/transactions") {
- val bankAccount = getBankAccountWithAuth(call)
+ val username = call.request.basicAuth()
+ val demobank = ensureDemobank(call)
+ val bankAccount = getBankAccountFromLabel(
+ call.expectUriComponent("account_name"),
+ demobank
+ )
+ // note: admin has no rights to create transactions on non-admin accounts.
+ val authGranted: Boolean = !WITH_AUTH
+ if (!authGranted && username != bankAccount.label)
+ throw unauthorized("Username '$username' has no rights over bank account ${bankAccount.label}")
val req = call.receive<NewTransactionReq>()
val payto = parsePayto(req.paytoUri)
val amount: String? = payto.amount ?: req.amount
@@ -1283,8 +1282,7 @@ val sandboxApp: Application.() -> Unit = {
}
// Information about one withdrawal.
get("/accounts/{account_name}/withdrawals/{withdrawal_id}") {
- val op = getWithdrawalOperation(call.getUriComponent("withdrawal_id"))
- ensureDemobank(call)
+ val op = getWithdrawalOperation(call.expectUriComponent("withdrawal_id"))
if (!op.selectionDone && op.reservePub != null) throw internalServerError(
"Unselected withdrawal has a reserve public key",
LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE
@@ -1302,29 +1300,30 @@ val sandboxApp: Application.() -> Unit = {
// Create a new withdrawal operation.
post("/accounts/{account_name}/withdrawals") {
var username = call.request.basicAuth()
- if (username == null && (!WITH_AUTH)) {
- logger.info("Authentication is disabled to facilitate tests, defaulting to 'admin' username")
- username = "admin"
- }
val demobank = ensureDemobank(call)
/**
* Check here if the user has the right over the claimed bank account. After
* this check, the withdrawal operation will be allowed only by providing its
* UID. */
val maybeOwnedAccount = getBankAccountFromLabel(
- call.getUriComponent("account_name"),
+ call.expectUriComponent("account_name"),
demobank
)
- if (maybeOwnedAccount.owner != username && WITH_AUTH) throw unauthorized(
- "Customer '$username' has no rights over bank account '${maybeOwnedAccount.label}'"
- )
+ val authGranted = !WITH_AUTH // note: admin not allowed on non-admin accounts
+ if (!authGranted && maybeOwnedAccount.owner != username)
+ throw unauthorized("Customer '$username' has no rights over bank account '${maybeOwnedAccount.label}'")
val req = call.receive<WithdrawalRequest>()
// Check for currency consistency
val amount = parseAmount(req.amount)
if (amount.currency != demobank.config.currency)
throw badRequest("Currency ${amount.currency} differs from Demobank's: ${demobank.config.currency}")
// Check funds are sufficient.
- if (maybeDebit(maybeOwnedAccount.label, BigDecimal(amount.amount))) {
+ if (
+ maybeDebit(
+ maybeOwnedAccount.label,
+ BigDecimal(amount.amount),
+ transaction { maybeOwnedAccount.demoBank.name }
+ )) {
logger.error("Account ${maybeOwnedAccount.label} would surpass debit threshold. Not withdrawing")
throw SandboxError(HttpStatusCode.Forbidden, "Insufficient funds")
}
@@ -1372,7 +1371,7 @@ val sandboxApp: Application.() -> Unit = {
}
// Confirm a withdrawal: no basic auth, because the ID should be unguessable.
post("/accounts/{account_name}/withdrawals/{withdrawal_id}/confirm") {
- val withdrawalId = call.getUriComponent("withdrawal_id")
+ val withdrawalId = call.expectUriComponent("withdrawal_id")
transaction {
val wo = getWithdrawalOperation(withdrawalId)
if (wo.aborted) throw SandboxError(
@@ -1415,7 +1414,7 @@ val sandboxApp: Application.() -> Unit = {
return@post
}
post("/accounts/{account_name}/withdrawals/{withdrawal_id}/abort") {
- val withdrawalId = call.getUriComponent("withdrawal_id")
+ val withdrawalId = call.expectUriComponent("withdrawal_id")
val operation = getWithdrawalOperation(withdrawalId)
if (operation.confirmationDone) throw conflict("Cannot abort paid withdrawal.")
transaction { operation.aborted = true }
@@ -1425,16 +1424,12 @@ val sandboxApp: Application.() -> Unit = {
// Bank account basic information.
get("/accounts/{account_name}") {
val username = call.request.basicAuth()
- val accountAccessed = call.getUriComponent("account_name")
+ val accountAccessed = call.expectUriComponent("account_name")
val demobank = ensureDemobank(call)
val bankAccount = getBankAccountFromLabel(accountAccessed, demobank)
- // Check rights.
- if (
- WITH_AUTH
- && (bankAccount.owner != username && username != "admin")
- ) throw forbidden(
- "Customer '$username' cannot access bank account '$accountAccessed'"
- )
+ val authGranted = !WITH_AUTH || bankAccount.isPublic || username == "admin"
+ if (!authGranted && bankAccount.owner != username)
+ throw forbidden("Customer '$username' cannot access bank account '$accountAccessed'")
val balance = getBalance(bankAccount, withPending = true)
call.respond(object {
val balance = object {
@@ -1450,21 +1445,23 @@ val sandboxApp: Application.() -> Unit = {
val iban = bankAccount.iban
// The Elvis operator helps the --no-auth case,
// where username would be empty
- val debitThreshold = getMaxDebitForUser(username ?: "admin").toString()
+ val debitThreshold = getMaxDebitForUser(
+ username = username ?: "admin",
+ demobankName = demobank.name
+ ).toString()
})
return@get
}
get("/accounts/{account_name}/transactions/{tId}") {
+ val username = call.request.basicAuth()
val demobank = ensureDemobank(call)
val bankAccount = getBankAccountFromLabel(
- call.getUriComponent("account_name"),
+ call.expectUriComponent("account_name"),
demobank
)
- val authOk: Boolean = bankAccount.isPublic || (!WITH_AUTH)
- if (!authOk && (call.request.basicAuth() != bankAccount.owner)) throw forbidden(
- "Cannot access bank account ${bankAccount.label}"
- )
- // Flow here == Right on the bank account.
+ val authGranted: Boolean = bankAccount.isPublic || !WITH_AUTH || username == "admin"
+ if (!authGranted && username != bankAccount.owner)
+ throw forbidden("Cannot access bank account ${bankAccount.label}")
val tId = call.parameters["tId"] ?: throw badRequest("URI didn't contain the transaction ID")
val tx: BankAccountTransactionEntity? = transaction {
BankAccountTransactionEntity.find {
@@ -1476,16 +1473,15 @@ val sandboxApp: Application.() -> Unit = {
return@get
}
get("/accounts/{account_name}/transactions") {
+ val username = call.request.basicAuth()
val demobank = ensureDemobank(call)
val bankAccount = getBankAccountFromLabel(
- call.getUriComponent("account_name"),
+ call.expectUriComponent("account_name"),
demobank
)
- val authOk: Boolean = bankAccount.isPublic || (!WITH_AUTH)
- if (!authOk && (call.request.basicAuth() != bankAccount.owner)) throw forbidden(
- "Cannot access bank account ${bankAccount.label}"
- )
-
+ val authGranted: Boolean = bankAccount.isPublic || !WITH_AUTH || username == "admin"
+ if (!authGranted && bankAccount.owner != username)
+ throw forbidden("Cannot access bank account ${bankAccount.label}")
val page: Int = Integer.decode(call.request.queryParameters["page"] ?: "0")
val size: Int = Integer.decode(call.request.queryParameters["size"] ?: "5")
@@ -1535,7 +1531,10 @@ val sandboxApp: Application.() -> Unit = {
BankAccountsTable.demoBank eq demobank.id
)
}.forEach {
- val balanceIter = getBalance(it, withPending = true)
+ val balanceIter = getBalance(
+ it,
+ withPending = true,
+ )
ret.publicAccounts.add(
PublicAccountInfo(
balance = "${demobank.config.currency}:$balanceIter",
@@ -1549,10 +1548,21 @@ val sandboxApp: Application.() -> Unit = {
return@get
}
delete("accounts/{account_name}") {
- // Check demobank was created.
- ensureDemobank(call)
+ val username = call.request.basicAuth()
+ val demobank = ensureDemobank(call)
+ val authGranted = !WITH_AUTH || username == "admin"
+ val bankAccountLabel = call.expectUriComponent("account_name")
+ /**
+ * This helper fails if the demobank that is mentioned in the URI
+ * is not hosting the account to be deleted.
+ */
+ val bankAccount = getBankAccountFromLabel(
+ bankAccountLabel,
+ demobank
+ )
+ if (!authGranted && username != bankAccount.owner)
+ throw unauthorized("User '$username' has no rights to delete bank account '$bankAccountLabel'")
transaction {
- val bankAccount = getBankAccountWithAuth(call)
val customerAccount = getCustomer(bankAccount.owner)
bankAccount.delete()
customerAccount.delete()
@@ -1572,11 +1582,12 @@ val sandboxApp: Application.() -> Unit = {
}
val req = call.receive<CustomerRegistration>()
val newAccount = insertNewAccount(
- req.username,
- req.password,
- name = req.name,
- iban = req.iban,
- isPublic = req.isPublic
+ req.username,
+ req.password,
+ name = req.name,
+ iban = req.iban,
+ demobank = demobank.name,
+ isPublic = req.isPublic
)
val balance = getBalance(newAccount.bankAccount, withPending = true)
call.respond(object {
@@ -1587,7 +1598,10 @@ val sandboxApp: Application.() -> Unit = {
receiverName = getPersonNameFromCustomer(req.username)
)
val iban = newAccount.bankAccount.iban
- val debitThreshold = getMaxDebitForUser(req.username).toString()
+ val debitThreshold = getMaxDebitForUser(
+ req.username,
+ demobank.name
+ ).toString()
})
return@post
}
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
@@ -19,7 +19,7 @@ fun maybeDebit(
"Demobank '${demobankName}' not found when trying to check the debit threshold" +
" for user $accountLabel"
)
- val balance = getBalance(accountLabel, withPending = true)
+ val balance = getBalance(accountLabel, demobankName, withPending = true)
val maxDebt = if (accountLabel == "admin") {
demobank.config.bankDebtLimit
} else demobank.config.usersDebtLimit
@@ -32,8 +32,13 @@ fun maybeDebit(
return false
}
-fun getMaxDebitForUser(username: String): Int {
- val bank = getDefaultDemobank()
+fun getMaxDebitForUser(
+ username: String,
+ demobankName: String = "default"
+): Int {
+ val bank = getDemobank(demobankName) ?: throw internalServerError(
+ "demobank $demobankName not found"
+ )
if (username == "admin") return bank.config.bankDebtLimit
return bank.config.usersDebtLimit
}
@@ -50,6 +55,9 @@ fun getBalanceForJson(value: BigDecimal, currency: String): BalanceJson {
* last statement. If the bank account does not have any statement
* yet, then zero is returned. When 'withPending' is true, it adds
* the pending transactions to it.
+ *
+ * Note: because transactions are searched after the bank accounts
+ * (numeric) id, the research in the database is not ambiguous.
*/
fun getBalance(
bankAccount: BankAccountEntity,
@@ -92,10 +100,16 @@ fun getBalance(
return lastBalance
}
-// Wrapper offering to get bank accounts from a string.
-fun getBalance(accountLabel: String, withPending: Boolean = true): BigDecimal {
- val defaultDemobank = getDefaultDemobank()
- val account = getBankAccountFromLabel(accountLabel, defaultDemobank)
+// Gets the balance of 'accountLabel', which is hosted at 'demobankName'.
+fun getBalance(accountLabel: String,
+ demobankName: String = "default",
+ withPending: Boolean = true
+): BigDecimal {
+ val demobank = getDemobank(demobankName) ?: throw SandboxError(
+ HttpStatusCode.InternalServerError,
+ "Demobank '$demobankName' not found"
+ )
+ val account = getBankAccountFromLabel(accountLabel, demobank)
return getBalance(account, withPending)
}
@@ -150,7 +164,12 @@ fun wireTransfer(
" Only ${demobank.config.currency} allowed."
)
// Check funds are sufficient.
- if (maybeDebit(debitAccount.label, amountAsNumber)) {
+ if (
+ maybeDebit(
+ debitAccount.label,
+ amountAsNumber,
+ demobank.name
+ )) {
logger.error("Account ${debitAccount.label} would surpass debit threshold. Rollback wire transfer")
throw SandboxError(HttpStatusCode.PreconditionFailed, "Insufficient funds")
}
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
@@ -118,7 +118,7 @@ fun ApplicationRequest.getBaseUrl(): String {
* Get the URI (path's) component or throw Internal server error.
* @param component the name of the URI component to return.
*/
-fun ApplicationCall.getUriComponent(name: String): String {
+fun ApplicationCall.expectUriComponent(name: String): String {
val ret: String? = this.parameters[name]
if (ret == null) throw badRequest("Component $name not found in URI")
return ret