libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit cb7094fe27afb1ea1ecd4fcbda3d075c05f7aa16
parent 61c49361c004bfe59a05e00cc51aae43af213193
Author: Antoine A <>
Date:   Mon, 13 Nov 2023 18:47:44 +0000

Improve accounts API

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 38++++++++++++++++++++++++++------------
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 3++-
Mbank/src/main/kotlin/tech/libeufin/bank/db/Database.kt | 40+++++++++++++++++++++++++++++++---------
Mbank/src/test/kotlin/CoreBankApiTest.kt | 58++++++++++++++++++++++++++++++++++++++++++++++------------
Mutil/src/main/kotlin/TalerErrorCode.kt | 48++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 154 insertions(+), 35 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -221,7 +221,10 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { req.debit_threshold?.run { ctx.checkRegionalCurrency(this) } if (req.is_taler_exchange != null && username == "admin") - throw forbidden("admin account cannot be an exchange") + throw conflict( + "admin account cannot be an exchange", + TalerErrorCode.BANK_PATCH_ADMIN_EXCHANGE + ) val res = db.accountReconfig( login = username, @@ -239,27 +242,38 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { "Account '$username' not found", TalerErrorCode.BANK_UNKNOWN_ACCOUNT ) - CustomerPatchResult.CONFLICT_LEGAL_NAME -> - throw forbidden("non-admin user cannot change their legal name") - CustomerPatchResult.CONFLICT_DEBT_LIMIT -> - throw forbidden("non-admin user cannot change their debt limit") + CustomerPatchResult.CONFLICT_LEGAL_NAME -> throw conflict( + "non-admin user cannot change their legal name", + TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME + ) + CustomerPatchResult.CONFLICT_DEBT_LIMIT -> throw conflict( + "non-admin user cannot change their debt limit", + TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT + ) } } - } - auth(db, TokenScope.readwrite) { patch("/accounts/{USERNAME}/auth") { val req = call.receive<AccountPasswordChange>() - val hashedPassword = CryptoUtil.hashpw(req.new_password) - if (!db.customerChangePassword(username, hashedPassword)) - throw notFound( + if (!isAdmin && req.old_password == null) { + throw conflict( + "non-admin user cannot change password without providing old password", + TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD + ) + } + when (db.accountReconfigPassword(username, req.new_password, req.old_password)) { + CustomerPatchAuthResult.SUCCESS -> call.respond(HttpStatusCode.NoContent) + CustomerPatchAuthResult.ACCOUNT_NOT_FOUND -> throw notFound( "Account '$username' not found", TalerErrorCode.BANK_UNKNOWN_ACCOUNT ) - call.respond(HttpStatusCode.NoContent) + CustomerPatchAuthResult.CONFLICT_BAD_PASSWORD -> throw conflict( + "old password does not match", + TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD + ) + } } } get("/public-accounts") { - // no authentication here. val publicAccounts = db.accountsGetPublic(ctx.currency) if (publicAccounts.isEmpty()) { call.respond(HttpStatusCode.NoContent) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -331,7 +331,7 @@ class ChangePw : CliktCommand("Change account password", name = "passwd") { if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper exitProcess(1) - if (!db.customerChangePassword(account, CryptoUtil.hashpw(password))) { + if (db.accountReconfigPassword(account, password, null) != CustomerPatchAuthResult.SUCCESS) { println("password change failed") exitProcess(1) } else { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -609,5 +609,6 @@ data class PublicAccount( */ @Serializable data class AccountPasswordChange( - val new_password: String + val new_password: String, + val old_password: String? = null ) \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt @@ -110,15 +110,6 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val } } - suspend fun customerChangePassword(customerName: String, passwordHash: String): Boolean = conn { conn -> - val stmt = conn.prepareStatement(""" - UPDATE customers SET password_hash=? where login=? - """) - stmt.setString(1, passwordHash) - stmt.setString(2, customerName) - stmt.executeUpdateCheck() - } - suspend fun customerPasswordHashFromLogin(login: String): String? = conn { conn -> val stmt = conn.prepareStatement(""" SELECT password_hash FROM customers WHERE login=? @@ -438,6 +429,30 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val } } + suspend fun accountReconfigPassword(login: String, newPw: String, oldPw: String?): CustomerPatchAuthResult = conn { + it.transaction { conn -> + val currentPwh = conn.prepareStatement(""" + SELECT password_hash FROM customers WHERE login=? + """).run { + setString(1, login) + oneOrNull { it.getString(1) } + } + if (currentPwh == null) { + CustomerPatchAuthResult.ACCOUNT_NOT_FOUND + } else if (oldPw != null && !CryptoUtil.checkpw(oldPw, currentPwh)) { + CustomerPatchAuthResult.CONFLICT_BAD_PASSWORD + } else { + val stmt = conn.prepareStatement(""" + UPDATE customers SET password_hash=? where login=? + """) + stmt.setString(1, CryptoUtil.hashpw(newPw)) + stmt.setString(2, login) + stmt.executeUpdateCheck() + CustomerPatchAuthResult.SUCCESS + } + } + } + /** * Gets the list of public accounts in the system. * internalCurrency is the bank's currency and loginFilter is @@ -943,6 +958,13 @@ enum class CustomerPatchResult { SUCCESS } +/** Result status of customer account auth patch */ +enum class CustomerPatchAuthResult { + ACCOUNT_NOT_FOUND, + CONFLICT_BAD_PASSWORD, + SUCCESS +} + /** Result status of customer account deletion */ enum class CustomerDeletionResult { SUCCESS, diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -352,12 +352,12 @@ class CoreBankAccountsMgmtApiTest { jsonBody(req) }.assertNoContent() - suspend fun checkAdminOnly(req: JsonElement) { + suspend fun checkAdminOnly(req: JsonElement, error: TalerErrorCode) { // Checking ordinary user doesn't get to patch client.patch("/accounts/merchant") { basicAuth("merchant", "merchant-password") jsonBody(req) - }.assertForbidden() + }.assertConflict(error) // Finally checking that admin does get to patch client.patch("/accounts/merchant") { basicAuth("admin", "admin-password") @@ -365,14 +365,20 @@ class CoreBankAccountsMgmtApiTest { }.assertNoContent() } - checkAdminOnly(json(req) { "name" to "Another Foo" }) - checkAdminOnly(json(req) { "debit_threshold" to "KUDOS:100" }) + checkAdminOnly( + json(req) { "name" to "Another Foo" }, + TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME + ) + checkAdminOnly( + json(req) { "debit_threshold" to "KUDOS:100" }, + TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT + ) // Check admin account cannot be exchange client.patch("/accounts/admin") { basicAuth("admin", "admin-password") jsonBody { "is_taler_exchange" to true } - }.assertForbidden() + }.assertConflict(TalerErrorCode.BANK_PATCH_ADMIN_EXCHANGE) // But we can change its debt limit client.patch("/accounts/admin") { basicAuth("admin", "admin-password") @@ -402,21 +408,49 @@ class CoreBankAccountsMgmtApiTest { @Test fun passwordChangeTest() = bankSetup { _ -> // Changing the password. - client.patch("/accounts/merchant/auth") { - basicAuth("merchant", "merchant-password") + client.patch("/accounts/customer/auth") { + basicAuth("customer", "customer-password") jsonBody { + "old_password" to "customer-password" "new_password" to "new-password" } }.assertNoContent() // Previous password should fail. - client.patch("/accounts/merchant/auth") { - basicAuth("merchant", "merchant-password") + client.patch("/accounts/customer/auth") { + basicAuth("customer", "customer-password") }.assertUnauthorized() // New password should succeed. - client.patch("/accounts/merchant/auth") { - basicAuth("merchant", "new-password") + client.patch("/accounts/customer/auth") { + basicAuth("customer", "new-password") jsonBody { - "new_password" to "merchant-password" + "old_password" to "new-password" + "new_password" to "customer-password" + } + }.assertNoContent() + + + // Check require test old password + client.patch("/accounts/customer/auth") { + basicAuth("customer", "customer-password") + jsonBody { + "old_password" to "bad-password" + "new_password" to "new-password" + } + }.assertConflict(TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD) + + // Check require old password for user + client.patch("/accounts/customer/auth") { + basicAuth("customer", "customer-password") + jsonBody { + "new_password" to "new-password" + } + }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD) + + // Check admin + client.patch("/accounts/customer/auth") { + basicAuth("admin", "admin-password") + jsonBody { + "new_password" to "new-password" } }.assertNoContent() } diff --git a/util/src/main/kotlin/TalerErrorCode.kt b/util/src/main/kotlin/TalerErrorCode.kt @@ -666,6 +666,14 @@ enum class TalerErrorCode(val code: Int) { /** + * The exchange failed to start a KYC attribute conversion helper process. It is likely configured incorrectly. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_KYC_CONVERTER_FAILED(1037), + + + /** * The exchange did not find information about the specified transaction in the database. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * (A value of 0 indicates that the error is generated client-side). @@ -3426,6 +3434,46 @@ enum class TalerErrorCode(val code: Int) { /** + * A non-admin user has tried to change their legal name. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_PATCH_LEGAL_NAME(5135), + + + /** + * A non-admin user has tried to change their debt limit. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_PATCH_DEBT_LIMIT(5136), + + + /** + * A non-admin user has tried to change their password whihout providing the current one. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD(5137), + + + /** + * Provided old password does not match current password. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_PATCH_BAD_OLD_PASSWORD(5138), + + + /** + * An admin user has tried to become an exchange. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_PATCH_ADMIN_EXCHANGE(5139), + + + /** * The sync service failed find the account in its database. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * (A value of 0 indicates that the error is generated client-side).