summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine A <>2024-03-20 16:31:41 +0100
committerAntoine A <>2024-03-20 16:31:41 +0100
commit97963f2b5c5fc520705a5f2f6283d6483b7585d3 (patch)
tree2d1168a57cd18462ed41a9403ac29c7d51468db3
parente732b8c1c839c57dd6860d2d478f0fa39ed0cd5e (diff)
downloadlibeufin-97963f2b5c5fc520705a5f2f6283d6483b7585d3.tar.gz
libeufin-97963f2b5c5fc520705a5f2f6283d6483b7585d3.tar.bz2
libeufin-97963f2b5c5fc520705a5f2f6283d6483b7585d3.zip
Make soft deleted account's information and operations accessible to admin
-rw-r--r--API_CHANGES.md2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Constants.kt2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt11
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt4
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt18
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt6
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt4
-rw-r--r--bank/src/test/kotlin/CoreBankApiTest.kt138
9 files changed, 141 insertions, 46 deletions
diff --git a/API_CHANGES.md b/API_CHANGES.md
index da085bb7..ff80790f 100644
--- a/API_CHANGES.md
+++ b/API_CHANGES.md
@@ -47,6 +47,8 @@ This files contains all the API changes for the current release:
- GET /public-accounts: add row_id field
- GET /config: new bank_name field for the bank name
- POST /accounts/USERNAME/transactions: new request_uid field for idempotency and new idempotency error
+- GET /accounts: new status field
+- GET /accounts/USERNAME: new status field
## bank cli
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 } },