libeufin

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

commit a917619a31aa0abbedfa36def0def2b38bae3d43
parent 677d30af69503e7bb0a895a9dec8f2694b763e1f
Author: MS <ms@taler.net>
Date:   Fri, 29 Sep 2023 10:50:32 +0200

Addressing FIXMEs.

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt | 8++------
Mbank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt | 32++++++++++++++++++--------------
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 4++--
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 17+++--------------
Mbank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt | 6+++---
Mbank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 3+--
Mbank/src/test/kotlin/TalerApiTest.kt | 4++--
Mutil/src/main/kotlin/TalerErrorCode.kt | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
8 files changed, 92 insertions(+), 62 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt @@ -31,12 +31,10 @@ import java.util.* /** * Allowed lengths for fractional digits in amounts. */ -enum class FracDigits(howMany: Int) { - TWO(2), - EIGHT(8) +enum class FracDigits { + TWO, EIGHT } - /** * Timestamp containing the number of seconds since epoch. */ @@ -437,8 +435,6 @@ typealias ResourceName = String /** * Checks if the input Customer has the rights over ResourceName. - * FIXME: myAuth() gives null on failures, but this gives false. - * Should they return the same, for consistency? */ fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean { if (c.login == this) return true diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt @@ -33,7 +33,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { call.authenticateBankRequest(db, TokenScope.refreshable) ?: throw unauthorized("Authentication failed") val endpointOwner = call.maybeUriComponent("USERNAME") if (customer.login != endpointOwner) throw forbidden( - "User has no rights on this enpoint", TalerErrorCode.TALER_EC_END // FIXME: need generic forbidden + "User has no rights on this enpoint", + TalerErrorCode.TALER_EC_GENERIC_FORBIDDEN ) val maybeAuthToken = call.getAuthToken() val req = call.receive<TokenRequest>() @@ -93,16 +94,18 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { if (ctx.restrictRegistration) { val customer: Customer? = call.authenticateBankRequest(db, TokenScope.readwrite) if (customer == null || customer.login != "admin") throw LibeufinBankException( - httpStatus = HttpStatusCode.Unauthorized, talerError = TalerError( - code = TalerErrorCode.TALER_EC_BANK_LOGIN_FAILED.code, + httpStatus = HttpStatusCode.Unauthorized, + talerError = TalerError( + code = TalerErrorCode.TALER_EC_GENERIC_UNAUTHORIZED.code, hint = "Either 'admin' not authenticated or an ordinary user tried this operation." ) ) } // auth passed, proceed with activity. val req = call.receive<RegisterAccountRequest>() // Prohibit reserved usernames: if (req.username == "admin" || req.username == "bank") throw LibeufinBankException( - httpStatus = HttpStatusCode.Conflict, talerError = TalerError( - code = GENERIC_UNDEFINED, // FIXME: this waits GANA. + httpStatus = HttpStatusCode.Conflict, + talerError = TalerError( + code = TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT.code, hint = "Username '${req.username}' is reserved." ) ) // Checking idempotency. @@ -242,7 +245,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { ) ) throw forbidden( hint = "Insufficient funds to withdraw with Taler", - talerErrorCode = TalerErrorCode.TALER_EC_NONE // FIXME: need EC. + talerErrorCode = TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT ) // Auth and funds passed, create the operation now! val opId = UUID.randomUUID() if (!db.talerWithdrawalCreate( @@ -308,7 +311,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { WithdrawalConfirmationResult.BALANCE_INSUFFICIENT -> throw conflict( "Insufficient funds", - TalerErrorCode.TALER_EC_END // FIXME: define EC for this. + TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT ) WithdrawalConfirmationResult.OP_NOT_FOUND -> /** @@ -324,7 +327,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { * bank account got removed before this confirmation. */ throw conflict( - hint = "Exchange to withdraw from not found", talerEc = TalerErrorCode.TALER_EC_END // FIXME + hint = "Exchange to withdraw from not found", + talerEc = TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT ) WithdrawalConfirmationResult.CONFLICT -> throw internalServerError("Bank didn't check for idempotency") @@ -380,7 +384,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { ?: throw internalServerError("Debtor database ID not found") // This performs already a SELECT on the bank account, like the wire transfer will do as well later! logger.info("creditor payto: $paytoWithoutParams") val creditorCustomerData = db.bankAccountGetFromInternalPayto(paytoWithoutParams) ?: throw notFound( - "Creditor account not found", TalerErrorCode.TALER_EC_END // FIXME: define this EC. + "Creditor account not found", + TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT ) if (txData.amount.currency != ctx.currency) throw badRequest( "Wrong currency: ${txData.amount.currency}", talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH @@ -395,13 +400,11 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { val res = db.bankTransactionCreate(dbInstructions) when (res) { Database.BankTransactionResult.CONFLICT -> throw conflict( - "Insufficient funds", TalerErrorCode.TALER_EC_END // FIXME: need bank 'insufficient funds' EC. + "Insufficient funds", + TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT ) - Database.BankTransactionResult.NO_CREDITOR -> throw internalServerError("Creditor not found despite previous checks.") - Database.BankTransactionResult.NO_DEBTOR -> throw internalServerError("Debtor not found despite the request was authenticated.") - Database.BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK) } return@post @@ -420,7 +423,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { } val customerRowId = c.dbRowId ?: throw internalServerError("Authenticated client lacks database entry") val tx = db.bankTransactionGetFromInternalId(txRowId) ?: throw notFound( - "Bank transaction '$tId' not found", TalerErrorCode.TALER_EC_NONE // FIXME: need def. + "Bank transaction '$tId' not found", + TalerErrorCode.TALER_EC_BANK_TRANSACTION_NOT_FOUND ) val customerBankAccount = db.bankAccountGetFromOwnerId(customerRowId) ?: throw internalServerError("Customer '${c.login}' lacks bank account.") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -371,7 +371,6 @@ class Database(private val dbConfig: String, private val bankCurrency: String) { throw internalServerError( "Do not pass a balance upon bank account creation, do a wire transfer instead." ) - // FIXME: likely to be changed to only do internal_payto_uri val stmt = prepare(""" INSERT INTO bank_accounts (internal_payto_uri @@ -1116,6 +1115,8 @@ class Database(private val dbConfig: String, private val bankCurrency: String) { val txResult: BankTransactionResult, /** * bank transaction that backs this Taler transfer request. + * This is the debit transactions associated to the exchange + * bank account. */ val txRowId: Long? = null ) @@ -1139,7 +1140,6 @@ class Database(private val dbConfig: String, private val bankCurrency: String) { endToEndId: String = "not used", ): TalerTransferCreationResult { reconnect() - // FIXME: future versions should return the exchange's latest bank transaction ID val stmt = prepare(""" SELECT out_exchange_balance_insufficient diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -275,7 +275,7 @@ val corebankDecompressionPlugin = createApplicationPlugin("RequestingBodyDecompr logger.error("Deflated request failed to inflate: ${e.message}") throw badRequest( hint = "Could not inflate request", - talerErrorCode = TalerErrorCode.TALER_EC_END // FIXME: provide dedicated EC. + talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_COMPRESSION_INVALID ) } brc @@ -284,7 +284,6 @@ val corebankDecompressionPlugin = createApplicationPlugin("RequestingBodyDecompr } } - /** * Set up web server handlers for the Taler corebank API. */ @@ -309,6 +308,7 @@ fun Application.corebankWebApp(db: Database, ctx: BankApplicationContext) { install(IgnoreTrailingSlash) install(ContentNegotiation) { json(Json { + @OptIn(ExperimentalSerializationApi::class) explicitNulls = false encodeDefaults = true prettyPrint = true @@ -443,17 +443,6 @@ fun durationFromPretty(s: String): Long { return durationUs } -/** - * FIXME: Introduce a datatype for this instead of using Long - */ -fun TalerConfig.requireValueDuration(section: String, option: String): Long { - val durationStr = lookupValueString(section, option) - if (durationStr == null) { - throw TalerConfigError("expected duration for section $section, option $option, but config value is empty") - } - return durationFromPretty(durationStr) -} - fun TalerConfig.requireValueAmount(section: String, option: String, currency: String): TalerAmount { val amountStr = lookupValueString(section, option) if (amountStr == null) { @@ -573,7 +562,7 @@ class ChangePw : CliktCommand("Change account password", name = "passwd") { val config = TalerConfig.load(this.configFile) val ctx = readBankApplicationContextFromConfig(config) val dbConnStr = config.requireValueString("libeufin-bankdb-postgres", "config") - val servePortLong = config.requireValueNumber("libeufin-bank", "port") + config.requireValueNumber("libeufin-bank", "port") val db = Database(dbConnStr, ctx.currency) if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper exitProcess(1) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt @@ -93,7 +93,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) } throw conflict( hint = "request_uid used already", - talerEc = TalerErrorCode.TALER_EC_END // FIXME: need appropriate Taler EC. + talerEc = TalerErrorCode.TALER_EC_BANK_TRANSFER_REQUEST_UID_REUSED ) } // Legitimate request, go on. @@ -111,12 +111,12 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) if (dbRes.txResult == Database.BankTransactionResult.CONFLICT) throw conflict( "Insufficient balance for exchange", - TalerErrorCode.TALER_EC_END // FIXME + TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT ) if (dbRes.txResult == Database.BankTransactionResult.NO_CREDITOR) throw notFound( "Creditor account was not found", - TalerErrorCode.TALER_EC_END // FIXME + TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT ) val debitRowId = dbRes.txRowId ?: throw internalServerError("Database did not return the debit tx row ID") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -141,7 +141,6 @@ fun doTokenAuth( fun forbidden( hint: String = "No rights on the resource", - // FIXME: create a 'generic forbidden' Taler EC. talerErrorCode: TalerErrorCode = TalerErrorCode.TALER_EC_END ): LibeufinBankException = LibeufinBankException( httpStatus = HttpStatusCode.Forbidden, talerError = TalerError( @@ -151,7 +150,7 @@ fun forbidden( fun unauthorized(hint: String = "Login failed"): LibeufinBankException = LibeufinBankException( httpStatus = HttpStatusCode.Unauthorized, talerError = TalerError( - code = TalerErrorCode.TALER_EC_BANK_LOGIN_FAILED.code, hint = hint + code = TalerErrorCode.TALER_EC_GENERIC_UNAUTHORIZED.code, hint = hint ) ) diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt @@ -72,7 +72,7 @@ class TalerApiTest { "credit_account": "BAR-IBAN-ABC" } """.trimIndent() - val resp = client.post("/accounts/foo/taler-wire-gateway/transfer") { + client.post("/accounts/foo/taler-wire-gateway/transfer") { basicAuth("foo", "pw") contentType(ContentType.Application.Json) expectSuccess = true @@ -80,7 +80,7 @@ class TalerApiTest { } // println(resp.bodyAsText()) // check idempotency - val idemResp = client.post("/accounts/foo/taler-wire-gateway/transfer") { + client.post("/accounts/foo/taler-wire-gateway/transfer") { basicAuth("foo", "pw") contentType(ContentType.Application.Json) expectSuccess = true diff --git a/util/src/main/kotlin/TalerErrorCode.kt b/util/src/main/kotlin/TalerErrorCode.kt @@ -170,6 +170,14 @@ enum class TalerErrorCode(val code: Int) { /** + * The body in the request could not be decompressed by the server. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + TALER_EC_GENERIC_COMPRESSION_INVALID(28), + + + /** * The currencies involved in the operation do not match. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). @@ -202,6 +210,38 @@ enum class TalerErrorCode(val code: Int) { /** + * The service refused the request as the given authorization token is unknown. + * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). + * (A value of 0 indicates that the error is generated client-side). + */ + TALER_EC_GENERIC_TOKEN_UNKNOWN(41), + + + /** + * The service refused the request as the given authorization token expired. + * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). + * (A value of 0 indicates that the error is generated client-side). + */ + TALER_EC_GENERIC_TOKEN_EXPIRED(42), + + + /** + * The service refused the request as the given authorization token is malformed. + * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). + * (A value of 0 indicates that the error is generated client-side). + */ + TALER_EC_GENERIC_TOKEN_MALFORMED(43), + + + /** + * The service refused the request due to lack of proper rights on the resource. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + TALER_EC_GENERIC_FORBIDDEN(44), + + + /** * The service failed initialize its connection to the database. * 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). @@ -482,6 +522,14 @@ enum class TalerErrorCode(val code: Int) { /** + * The coin is not known to the exchange (yet). + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN(1019), + + + /** * The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). @@ -890,19 +938,11 @@ enum class TalerErrorCode(val code: Int) { /** - * The reserve balance, status or history was requested for a reserve which is not known to the exchange. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - TALER_EC_EXCHANGE_RESERVES_STATUS_UNKNOWN(1250), - - - /** - * The reserve status was requested with a bad signature. + * The coin history was requested with a bad signature. * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). * (A value of 0 indicates that the error is generated client-side). */ - TALER_EC_EXCHANGE_RESERVES_STATUS_BAD_SIGNATURE(1251), + TALER_EC_EXCHANGE_COIN_HISTORY_BAD_SIGNATURE(1251), /** @@ -910,7 +950,7 @@ enum class TalerErrorCode(val code: Int) { * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). * (A value of 0 indicates that the error is generated client-side). */ - TALER_EC_EXCHANGE_RESERVES_HISTORY_BAD_SIGNATURE(1252), + TALER_EC_EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE(1252), /** @@ -3146,14 +3186,6 @@ enum class TalerErrorCode(val code: Int) { /** - * Could not login for the requested operation. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - TALER_EC_BANK_LOGIN_FAILED(5105), - - - /** * The bank account referenced in the requested operation was not found. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * (A value of 0 indicates that the error is generated client-side). @@ -3266,6 +3298,14 @@ enum class TalerErrorCode(val code: Int) { /** + * The client tried to register a new account under a reserved username (like 'admin' for example). + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + TALER_EC_BANK_RESERVED_USERNAME_CONFLICT(5120), + + + /** * 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). @@ -4295,4 +4335,6 @@ enum class TalerErrorCode(val code: Int) { * (A value of 0 indicates that the error is generated client-side). */ TALER_EC_END(9999), + + }