diff options
Diffstat (limited to 'bank/src')
8 files changed, 139 insertions, 46 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt index 7a192308..15fe5be1 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt @@ -40,7 +40,7 @@ const val IBAN_ALLOCATION_RETRY_COUNTER: Int = 5 const val MAX_BODY_LENGTH: Long = 4 * 1024 // 4kB // API version -const val COREBANK_API_VERSION: String = "4:4:0" +const val COREBANK_API_VERSION: String = "4:5:0" const val CONVERSION_API_VERSION: String = "0:0:0" const val INTEGRATION_API_VERSION: String = "2:0:2" const val WIRE_GATEWAY_API_VERSION: String = "0:2:0" diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt index ce459baa..b9bfcbec 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -55,6 +55,11 @@ enum class WithdrawalStatus { confirmed } +enum class AccountStatus { + active, + deleted +} + enum class RoundingMode { zero, up, @@ -350,7 +355,8 @@ data class AccountMinimalData( val debit_threshold: TalerAmount, val is_public: Boolean, val is_taler_exchange: Boolean, - val row_id: Long + val row_id: Long, + val status: AccountStatus ) /** @@ -374,7 +380,8 @@ data class AccountData( val cashout_payto_uri: String? = null, val tan_channel: TanChannel? = null, val is_public: Boolean, - val is_taler_exchange: Boolean + val is_taler_exchange: Boolean, + val status: AccountStatus ) @Serializable diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt index a3bb8265..45689bf0 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt @@ -422,7 +422,7 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { } private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { - auth(db, TokenScope.readonly) { + auth(db, TokenScope.readonly, allowAdmin = true) { get("/accounts/{USERNAME}/transactions") { val params = HistoryParams.extract(call.request.queryParameters) val bankAccount = call.bankInfo(db, ctx.payto) @@ -618,7 +618,7 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) = conditio } } } - auth(db, TokenScope.readonly) { + auth(db, TokenScope.readonly, allowAdmin = true) { get("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}") { val id = call.longPath("CASHOUT_ID") val cashout = db.cashout.get(id, username) ?: throw notFound( diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt index 7781d807..a3a5d8e5 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -436,7 +436,7 @@ class AccountDAO(private val db: Database) { ,is_taler_exchange FROM bank_accounts JOIN customers ON customer_id=owning_customer_id - WHERE login=? AND deleted_at IS NULL + WHERE login=? """) stmt.setString(1, login) stmt.oneOrNull { @@ -465,10 +465,14 @@ class AccountDAO(private val db: Database) { ,(max_debt).frac AS max_debt_frac ,is_public ,is_taler_exchange + ,CASE + WHEN deleted_at IS NOT NULL THEN 'deleted' + ELSE 'active' + END as status FROM customers JOIN bank_accounts ON customer_id=owning_customer_id - WHERE login=? AND deleted_at IS NULL + WHERE login=? """) stmt.setString(1, login) stmt.oneOrNull { @@ -492,7 +496,8 @@ class AccountDAO(private val db: Database) { ), debit_threshold = it.getAmount("max_debt", db.bankCurrency), is_public = it.getBoolean("is_public"), - is_taler_exchange = it.getBoolean("is_taler_exchange") + is_taler_exchange = it.getBoolean("is_taler_exchange"), + status = AccountStatus.valueOf(it.getString("status")) ) } } @@ -555,9 +560,13 @@ class AccountDAO(private val db: Database) { ,is_taler_exchange ,internal_payto_uri ,bank_account_id + ,CASE + WHEN deleted_at IS NOT NULL THEN 'deleted' + ELSE 'active' + END as status FROM bank_accounts JOIN customers ON owning_customer_id = customer_id - WHERE name LIKE ? AND deleted_at IS NULL AND + WHERE name LIKE ? AND """, { setString(1, params.loginFilter) @@ -580,6 +589,7 @@ class AccountDAO(private val db: Database) { is_public = it.getBoolean("is_public"), is_taler_exchange = it.getBoolean("is_taler_exchange"), payto_uri = it.getBankPayto("internal_payto_uri", "name", ctx), + status = AccountStatus.valueOf(it.getString("status")) ) } }
\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt index 45da0854..958d2b33 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt @@ -104,7 +104,7 @@ class CashoutDAO(private val db: Database) { JOIN bank_accounts ON bank_account=bank_account_id JOIN customers ON owning_customer_id=customer_id LEFT JOIN bank_account_transactions ON local_transaction=bank_transaction_id - WHERE cashout_id=? AND login=? AND deleted_at IS NULL + WHERE cashout_id=? AND login=? """) stmt.setLong(1, id) stmt.setString(2, login) @@ -134,7 +134,7 @@ class CashoutDAO(private val db: Database) { FROM cashout_operations JOIN bank_accounts ON bank_account=bank_account_id JOIN customers ON owning_customer_id=customer_id - WHERE deleted_at IS NULL AND + WHERE """) { GlobalCashoutInfo( cashout_id = it.getLong("cashout_id"), @@ -150,7 +150,7 @@ class CashoutDAO(private val db: Database) { FROM cashout_operations JOIN bank_accounts ON bank_account=bank_account_id JOIN customers ON owning_customer_id=customer_id - WHERE login = ? AND deleted_at IS NULL AND + WHERE login = ? AND """, bind = { setString(1, login) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt index e82f0ba2..cf5ef9bc 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt @@ -165,7 +165,7 @@ class TransactionDAO(private val db: Database) { FROM bank_account_transactions JOIN bank_accounts ON bank_account_transactions.bank_account_id=bank_accounts.bank_account_id JOIN customers ON customer_id=owning_customer_id - WHERE bank_transaction_id=? AND login=? AND deleted_at IS NULL + WHERE bank_transaction_id=? AND login=? """) stmt.setLong(1, rowId) stmt.setString(2, login) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt index 3a41c0d8..4b069339 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt @@ -189,7 +189,7 @@ class WithdrawalDAO(private val db: Database) { FROM taler_withdrawal_operations JOIN bank_accounts ON wallet_bank_account=bank_account_id JOIN customers ON customer_id=owning_customer_id - WHERE withdrawal_uuid=? AND deleted_at IS NULL + WHERE withdrawal_uuid=? """) stmt.setObject(1, uuid) stmt.oneOrNull { it.getString(1) } @@ -250,7 +250,7 @@ class WithdrawalDAO(private val db: Database) { FROM taler_withdrawal_operations JOIN bank_accounts ON wallet_bank_account=bank_account_id JOIN customers ON customer_id=owning_customer_id - WHERE withdrawal_uuid=? AND deleted_at IS NULL + WHERE withdrawal_uuid=? """) stmt.setObject(1, uuid) stmt.oneOrNull { diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt index 0bd13207..275ed911 100644 --- a/bank/src/test/kotlin/CoreBankApiTest.kt +++ b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -425,11 +425,82 @@ class CoreBankAccountsApiTest { .assertChallenge() .assertNoContent() // Account no longer exists + client.deleteA("/accounts/john").assertUnauthorized() client.delete("/accounts/john") { pwAuth("admin") }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) } + + + @Test + fun softDelete() = bankSetup { db -> + // Create all kind of operations + val token = client.postA("/accounts/customer/token") { + json { "scope" to "readonly" } + }.assertOkJson<TokenSuccessResponse>().access_token + val tx_id = client.postA("/accounts/customer/transactions") { + json { + "payto_uri" to "$exchangePayto?message=payout" + "amount" to "KUDOS:0.3" + } + }.assertOkJson<TransactionCreateResponse>().row_id + val withdrawal_id = client.postA("/accounts/customer/withdrawals") { + json { "amount" to "KUDOS:9.0" } + }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id + fillCashoutInfo("customer") + val cashout_id = client.postA("/accounts/customer/cashouts") { + json { + "request_uid" to ShortHashCode.rand() + "amount_debit" to "KUDOS:1" + "amount_credit" to convert("KUDOS:1") + } + }.assertOkJson<CashoutResponse>().cashout_id + fillTanInfo("customer") + val tan_id = client.postA("/accounts/customer/transactions") { + json { + "payto_uri" to "$exchangePayto?message=payout" + "amount" to "KUDOS:0.3" + } + }.assertAcceptedJson<TanChallenge>().challenge_id + + // Delete account + tx("merchant", "KUDOS:1", "customer") + assertBalance("customer", "+KUDOS:0") + client.deleteA("/accounts/customer") + .assertChallenge() + .assertNoContent() + + // Check account can no longer login + client.delete("/accounts/customer/token") { + headers["Authorization"] = "Bearer secret-token:$token" + }.assertUnauthorized() + client.getA("/accounts/customer/transactions/$tx_id").assertUnauthorized() + client.getA("/accounts/customer/cashouts/$cashout_id").assertUnauthorized() + client.postA("/accounts/customer/withdrawals/$withdrawal_id/confirm").assertUnauthorized() + + // But admin can still see existing operations + client.get("/accounts/customer/transactions/$tx_id") { + pwAuth("admin") + }.assertOkJson<BankAccountTransactionInfo>() + client.get("/accounts/customer/cashouts/$cashout_id") { + pwAuth("admin") + }.assertOkJson<CashoutStatusResponse>() + client.get("/withdrawals/$withdrawal_id") + .assertOkJson<WithdrawalPublicInfo>() + + // GC + db.gc.collect(Instant.now(), Duration.ZERO, Duration.ZERO, Duration.ZERO) + client.get("/accounts/customer/transactions/$tx_id") { + pwAuth("admin") + }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + client.get("/accounts/customer/cashouts/$cashout_id") { + pwAuth("admin") + }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + client.get("/withdrawals/$withdrawal_id") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + } + // Test admin-only account deletion @Test fun deleteRestricted() = bankSetup(conf = "test_restrict.conf") { _ -> @@ -686,19 +757,32 @@ class CoreBankAccountsApiTest { // GET /public-accounts and GET /accounts @Test - fun list() = bankSetup(conf = "test_no_conversion.conf") { _ -> + fun list() = bankSetup(conf = "test_no_conversion.conf") { db -> authRoutine(HttpMethod.Get, "/accounts", requireAdmin = true) // Remove default accounts - listOf("merchant", "exchange", "customer").forEach { + val defaultAccounts = listOf("merchant", "exchange", "customer") + defaultAccounts.forEach { client.delete("/accounts/$it") { pwAuth("admin") }.assertNoContent() } + client.get("/accounts") { + pwAuth("admin") + }.assertOkJson<ListBankAccountsResponse> { + for (account in it.accounts) { + if (defaultAccounts.contains(account.username)) { + assertEquals(AccountStatus.deleted, account.status) + } else { + assertEquals(AccountStatus.active, account.status) + } + } + } + db.gc.collect(Instant.now(), Duration.ZERO, Duration.ZERO, Duration.ZERO) // Check error when no public accounts client.get("/public-accounts").assertNoContent() client.get("/accounts") { pwAuth("admin") - }.assertOk() + }.assertOkJson<ListBankAccountsResponse>() // Gen some public and private accounts repeat(5) { @@ -712,22 +796,18 @@ class CoreBankAccountsApiTest { }.assertOk() } // All public - client.get("/public-accounts").run { - assertOk() - val obj = json<PublicAccountsResponse>() - assertEquals(3, obj.public_accounts.size) - obj.public_accounts.forEach { + client.get("/public-accounts").assertOkJson<PublicAccountsResponse> { + assertEquals(3, it.public_accounts.size) + it.public_accounts.forEach { assertEquals(0, it.username.toInt() % 2) } } // All accounts client.get("/accounts?delta=10"){ pwAuth("admin") - }.run { - assertOk() - val obj = json<ListBankAccountsResponse>() - assertEquals(6, obj.accounts.size) - obj.accounts.forEachIndexed { idx, it -> + }.assertOkJson<ListBankAccountsResponse> { + assertEquals(6, it.accounts.size) + it.accounts.forEachIndexed { idx, it -> if (idx == 0) { assertEquals("admin", it.username) } else { @@ -738,11 +818,9 @@ class CoreBankAccountsApiTest { // Filtering client.get("/accounts?filter_name=3"){ pwAuth("admin") - }.run { - assertOk() - val obj = json<ListBankAccountsResponse>() - assertEquals(1, obj.accounts.size) - assertEquals("3", obj.accounts[0].username) + }.assertOkJson<ListBankAccountsResponse> { + assertEquals(1, it.accounts.size) + assertEquals("3", it.accounts[0].username) } } @@ -761,7 +839,7 @@ class CoreBankTransactionsApiTest { // GET /transactions @Test fun history() = bankSetup { _ -> - authRoutine(HttpMethod.Get, "/accounts/merchant/transactions") + authRoutine(HttpMethod.Get, "/accounts/merchant/transactions", allowAdmin = true) historyRoutine<BankAccountTransactionsResponse>( url = "/accounts/customer/transactions", ids = { it.transactions.map { it.row_id } }, @@ -799,7 +877,7 @@ class CoreBankTransactionsApiTest { // GET /transactions/T_ID @Test fun testById() = bankSetup { _ -> - authRoutine(HttpMethod.Get, "/accounts/merchant/transactions/42") + authRoutine(HttpMethod.Get, "/accounts/merchant/transactions/42", allowAdmin = true) // Create transaction tx("merchant", "KUDOS:0.3", "exchange", "tx") @@ -1043,9 +1121,7 @@ class CoreBankWithdrawalApiTest { client.postA("/accounts/merchant/withdrawals") { json { "amount" to amount} }.assertOkJson<BankAccountCreateWithdrawalResponse> { - client.get("/withdrawals/${it.withdrawal_id}") { - pwAuth("merchant") - }.assertOkJson<WithdrawalPublicInfo> { + client.get("/withdrawals/${it.withdrawal_id}").assertOkJson<WithdrawalPublicInfo> { assertEquals(amount, it.amount) } } @@ -1069,7 +1145,7 @@ class CoreBankWithdrawalApiTest { client.postA("/accounts/merchant/withdrawals") { json { "amount" to "KUDOS:1" } }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.taler_withdraw_uri.split("/").last() + val uuid = it.withdrawal_id // Check err client.postA("/accounts/merchant/withdrawals/$uuid/confirm") @@ -1080,7 +1156,7 @@ class CoreBankWithdrawalApiTest { client.postA("/accounts/merchant/withdrawals") { json { "amount" to "KUDOS:1" } }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.taler_withdraw_uri.split("/").last() + val uuid = it.withdrawal_id withdrawalSelect(uuid) // Check OK @@ -1093,7 +1169,7 @@ class CoreBankWithdrawalApiTest { client.postA("/accounts/merchant/withdrawals") { json { "amount" to "KUDOS:1" } }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.taler_withdraw_uri.split("/").last() + val uuid = it.withdrawal_id withdrawalSelect(uuid) client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() @@ -1106,7 +1182,7 @@ class CoreBankWithdrawalApiTest { client.postA("/accounts/merchant/withdrawals") { json { "amount" to "KUDOS:5" } }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.taler_withdraw_uri.split("/").last() + val uuid = it.withdrawal_id withdrawalSelect(uuid) // Send too much money @@ -1122,7 +1198,7 @@ class CoreBankWithdrawalApiTest { client.postA("/accounts/customer/withdrawals") { json { "amount" to "KUDOS:1" } }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.taler_withdraw_uri.split("/").last() + val uuid = it.withdrawal_id withdrawalSelect(uuid) // Check error @@ -1144,7 +1220,7 @@ class CoreBankWithdrawalApiTest { client.postA("/accounts/merchant/withdrawals") { json { "amount" to "KUDOS:1" } }.assertOkJson<BankAccountCreateWithdrawalResponse> { - val uuid = it.taler_withdraw_uri.split("/").last() + val uuid = it.withdrawal_id withdrawalSelect(uuid) client.postA("/accounts/merchant/withdrawals/$uuid/confirm") @@ -1244,7 +1320,7 @@ class CoreBankCashoutApiTest { // GET /accounts/{USERNAME}/cashouts/{CASHOUT_ID} @Test fun get() = bankSetup { _ -> - authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts/42") + authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts/42", allowAdmin = true) fillCashoutInfo("customer") val amountDebit = TalerAmount("KUDOS:1.5") @@ -1292,7 +1368,7 @@ class CoreBankCashoutApiTest { // GET /accounts/{USERNAME}/cashouts @Test fun history() = bankSetup { _ -> - authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts") + authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts", allowAdmin = true) historyRoutine<Cashouts>( url = "/accounts/customer/cashouts", ids = { it.cashouts.map { it.cashout_id } }, |