commit 61a3b2c89d4d6786469992f344ba379a1ab8db10 parent 2a31c2e83deaa9521cc471e0873229887ba694e1 Author: Antoine A <> Date: Mon, 6 Nov 2023 16:25:04 +0000 Send TAN code using scripts, fix reconfig and clean tests Diffstat:
20 files changed, 332 insertions(+), 188 deletions(-)
diff --git a/Makefile b/Makefile @@ -48,6 +48,7 @@ install-bank: ./gradlew bank:installShadowDist install -d $(abs_destdir)$(prefix) cp -r bank/build/install/bank-shadow/* -d $(abs_destdir)$(prefix) + cp -r contrib/libeufin-tan-*.sh -d $(abs_destdir)$(prefix) install-nexus: install -d $(nexus_config_dir) diff --git a/bank/conf/test.conf b/bank/conf/test.conf @@ -6,6 +6,8 @@ REGISTRATION_BONUS_ENABLED = NO SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com have_cashout = YES fiat_currency = FIAT +tan_sms = libeufin-tan-file.sh +tan_email = libeufin-tan-fail.sh [libeufin-bankdb-postgres] SQL_DIR = $DATADIR/sql/ diff --git a/bank/conf/test.conf b/bank/conf/test_no_tan.conf diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt @@ -144,11 +144,9 @@ private suspend fun doBasicAuth(db: Database, encodedCredentials: String): Strin */ limit = 2 ) - if (userAndPassSplit.size != 2) throw LibeufinBankException( - httpStatus = HttpStatusCode.BadRequest, talerError = TalerError( - code = TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED.code, - "Malformed Basic auth credentials found in the Authorization header." - ) + if (userAndPassSplit.size != 2) throw badRequest( + "Malformed Basic auth credentials found in the Authorization header.", + TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED ) val (login, plainPassword) = userAndPassSplit val passwordHash = db.customerPasswordHashFromLogin(login) ?: throw unauthorized() @@ -177,7 +175,7 @@ private suspend fun doTokenAuth( ): String? { val bareToken = splitBearerToken(token) ?: throw badRequest( "Bearer token malformed", - talerErrorCode = TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED + TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED ) val tokenBytes = try { Base32Crockford.decode(bareToken) @@ -204,10 +202,9 @@ private suspend fun doTokenAuth( return null } // Getting the related username. - return db.customerLoginFromId(maybeToken.bankCustomer) ?: throw LibeufinBankException( - httpStatus = HttpStatusCode.InternalServerError, talerError = TalerError( - code = TalerErrorCode.GENERIC_INTERNAL_INVARIANT_FAILURE.code, - hint = "Customer not found, despite token mentions it.", - ) + return db.customerLoginFromId(maybeToken.bankCustomer) ?: throw libeufinError( + HttpStatusCode.InternalServerError, + "Customer not found, despite token mentions it.", + TalerErrorCode.GENERIC_INTERNAL_INVARIANT_FAILURE ) } \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -79,7 +79,9 @@ data class BankConfig( val spaCaptchaURL: String?, val haveCashout: Boolean, val fiatCurrency: String?, - val conversionInfo: ConversionInfo? + val conversionInfo: ConversionInfo?, + val tanSms: String?, + val tanEmail: String?, ) @Serializable @@ -146,7 +148,9 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError { currencySpecification = currencySpecification, haveCashout = haveCashout, fiatCurrency = fiatCurrency, - conversionInfo = conversionInfo + conversionInfo = conversionInfo, + tanSms = lookupPath("libeufin-bank", "tan_sms"), + tanEmail = lookupPath("libeufin-bank", "tan_email"), ) } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -27,6 +27,7 @@ import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* +import java.util.concurrent.TimeUnit import java.io.File import kotlin.random.Random import net.taler.common.errorcodes.TalerErrorCode @@ -34,6 +35,8 @@ import net.taler.wallet.crypto.Base32Crockford import org.slf4j.Logger import org.slf4j.LoggerFactory import tech.libeufin.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.accountsMgmtHandlers") @@ -222,7 +225,7 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { val res = db.accountReconfig( login = username, name = req.name, - cashoutPayto = req.cashout_address, + cashoutPayto = req.cashout_payto_uri, emailAddress = req.challenge_contact_data?.email, isTalerExchange = req.is_taler_exchange, phoneNumber = req.challenge_contact_data?.phone, @@ -471,6 +474,15 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) { ctx.checkFiatCurrency(req.amount_credit) val tanChannel = req.tan_channel ?: TanChannel.sms + val tanScript = when (tanChannel) { + TanChannel.sms -> ctx.tanSms + TanChannel.email -> ctx.tanEmail + } ?: throw libeufinError( + HttpStatusCode.NotImplemented, + "Unsupported tan channel $tanChannel", + TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED + ) + val res = db.cashout.create( accountUsername = username, requestUid = req.request_uid, @@ -511,15 +523,25 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) { ) CashoutCreationResult.SUCCESS -> { res.tanCode?.run { - when (tanChannel) { - TanChannel.sms -> throw Exception("TODO") - TanChannel.email -> throw Exception("TODO") - TanChannel.file -> { - File("/tmp/cashout-tan.txt").writeText(this) + val exitValue = withContext(Dispatchers.IO) { + val process = ProcessBuilder(tanScript, res.tanInfo).start() + try { + process.outputWriter().use { it.write(res.tanCode) } + process.waitFor(10, TimeUnit.MINUTES) + } catch (e: Exception) { + process.destroy() } + process.exitValue() } + if (exitValue != 0) { + throw libeufinError( + HttpStatusCode.BadGateway, + "Tan channel script failure with exit value $exitValue", + TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED + ) + } + db.cashout.markSent(res.id!!, Instant.now(), TAN_RETRANSMISSION_PERIOD) } - db.cashout.markSent(res.id!!, Instant.now(), TAN_RETRANSMISSION_PERIOD) call.respond(CashoutPending(res.id.toString())) } } @@ -556,11 +578,16 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) { ) CashoutConfirmationResult.BAD_TAN_CODE -> throw forbidden( "Incorrect TAN code", - TalerErrorCode.END // TODO new ec + TalerErrorCode.BANK_TAN_CHALLENGE_FAILED + ) + CashoutConfirmationResult.NO_RETRY -> throw libeufinError( + HttpStatusCode.TooManyRequests, + "Too many failed confirmation attempt", + TalerErrorCode.BANK_TAN_RATE_LIMITED ) - CashoutConfirmationResult.NO_RETRY -> throw forbidden( - "Too manny failed confirmation attempt", - TalerErrorCode.END // TODO new ec + CashoutConfirmationResult.NO_CASHOUT_PAYTO -> throw conflict( + "Missing cashout payto uri", + TalerErrorCode.BANK_MISSING_TAN_INFO ) CashoutConfirmationResult.BALANCE_INSUFFICIENT -> throw conflict( "Insufficient funds", diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Error.kt b/bank/src/main/kotlin/tech/libeufin/bank/Error.kt @@ -49,62 +49,51 @@ data class TalerError( ) -fun forbidden( - hint: String = "No rights on the resource", - talerErrorCode: TalerErrorCode = TalerErrorCode.END +fun libeufinError( + status: HttpStatusCode, + hint: String?, + error: TalerErrorCode ): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.Forbidden, talerError = TalerError( - code = talerErrorCode.code, hint = hint + httpStatus = status, talerError = TalerError( + code = error.code, hint = hint ) ) -fun unauthorized(hint: String = "Login failed"): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.Unauthorized, talerError = TalerError( - code = TalerErrorCode.GENERIC_UNAUTHORIZED.code, hint = hint - ) -) +fun forbidden( + hint: String = "No rights on the resource", + error: TalerErrorCode = TalerErrorCode.END +): LibeufinBankException = libeufinError(HttpStatusCode.Forbidden, hint, error) -fun internalServerError(hint: String?): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.InternalServerError, talerError = TalerError( - code = TalerErrorCode.GENERIC_INTERNAL_INVARIANT_FAILURE.code, hint = hint - ) -) +fun unauthorized(hint: String? = "Login failed"): LibeufinBankException + = libeufinError(HttpStatusCode.Unauthorized, hint, TalerErrorCode.GENERIC_UNAUTHORIZED) + +fun internalServerError(hint: String?): LibeufinBankException + = libeufinError(HttpStatusCode.InternalServerError, hint, TalerErrorCode.GENERIC_INTERNAL_INVARIANT_FAILURE) fun notFound( - hint: String?, - talerEc: TalerErrorCode -): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.NotFound, talerError = TalerError( - code = talerEc.code, hint = hint - ) -) + hint: String, + error: TalerErrorCode +): LibeufinBankException = libeufinError(HttpStatusCode.NotFound, hint, error) fun conflict( - hint: String?, talerEc: TalerErrorCode -): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.Conflict, talerError = TalerError( - code = talerEc.code, hint = hint - ) -) + hint: String, error: TalerErrorCode +): LibeufinBankException = libeufinError(HttpStatusCode.Conflict, hint, error) fun badRequest( - hint: String? = null, talerErrorCode: TalerErrorCode = TalerErrorCode.GENERIC_JSON_INVALID -): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.BadRequest, talerError = TalerError( - code = talerErrorCode.code, hint = hint - ) -) + hint: String? = null, error: TalerErrorCode = TalerErrorCode.GENERIC_JSON_INVALID +): LibeufinBankException = libeufinError(HttpStatusCode.BadRequest, hint, error) + fun BankConfig.checkInternalCurrency(amount: TalerAmount) { if (amount.currency != currency) throw badRequest( "Wrong currency: expected internal currency $currency got ${amount.currency}", - talerErrorCode = TalerErrorCode.GENERIC_CURRENCY_MISMATCH + TalerErrorCode.GENERIC_CURRENCY_MISMATCH ) } fun BankConfig.checkFiatCurrency(amount: TalerAmount) { if (amount.currency != fiatCurrency) throw badRequest( "Wrong currency: expected fiat currency $fiatCurrency got ${amount.currency}", - talerErrorCode = TalerErrorCode.GENERIC_CURRENCY_MISMATCH + TalerErrorCode.GENERIC_CURRENCY_MISMATCH ) } \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -87,8 +87,8 @@ val corebankDecompressionPlugin = createApplicationPlugin("RequestingBodyDecompr } catch (e: Exception) { logger.error("Deflated request failed to inflate: ${e.message}") throw badRequest( - hint = "Could not inflate request", - talerErrorCode = TalerErrorCode.GENERIC_COMPRESSION_INVALID + "Could not inflate request", + TalerErrorCode.GENERIC_COMPRESSION_INVALID ) } brc diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -93,6 +93,17 @@ data class RegisterAccountRequest( val internal_payto_uri: IbanPayTo? = null ) +/** + * Request of PATCH /accounts/{USERNAME} + */ +@Serializable +data class AccountReconfiguration( + val challenge_contact_data: ChallengeContactData?, + val cashout_payto_uri: IbanPayTo?, + val name: String?, + val is_taler_exchange: Boolean?, + val debit_threshold: TalerAmount? +) /** * Type expected at POST /accounts/{USERNAME}/token @@ -148,8 +159,7 @@ data class BankAccount( // Allowed values for cashout TAN channels. enum class TanChannel { sms, - email, - file // Writes cashout TANs to /tmp, for testing. + email } // Scopes for authentication tokens. @@ -608,16 +618,4 @@ data class PublicAccount( @Serializable data class AccountPasswordChange( val new_password: String -) - -/** - * Request of PATCH /accounts/{USERNAME} - */ -@Serializable -data class AccountReconfiguration( - val challenge_contact_data: ChallengeContactData?, - val cashout_address: IbanPayTo?, - val name: String?, - val is_taler_exchange: Boolean?, - val debit_threshold: TalerAmount? ) \ 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 @@ -43,6 +43,7 @@ enum class CashoutConfirmationResult { BAD_TAN_CODE, BALANCE_INSUFFICIENT, NO_RETRY, + NO_CASHOUT_PAYTO, ABORTED } @@ -160,7 +161,8 @@ class CashoutDAO(private val db: Database) { out_bad_code, out_balance_insufficient, out_aborted, - out_no_retry + out_no_retry, + out_no_cashout_payto FROM cashout_confirm(?, ?, ?); """) stmt.setObject(1, opUuid) @@ -175,6 +177,7 @@ class CashoutDAO(private val db: Database) { it.getBoolean("out_balance_insufficient") -> CashoutConfirmationResult.BALANCE_INSUFFICIENT it.getBoolean("out_aborted") -> CashoutConfirmationResult.ABORTED it.getBoolean("out_no_retry") -> CashoutConfirmationResult.NO_RETRY + it.getBoolean("out_no_cashout_payto") -> CashoutConfirmationResult.NO_CASHOUT_PAYTO else -> CashoutConfirmationResult.SUCCESS } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -39,17 +39,17 @@ private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.helpers val reservedAccounts = setOf("admin", "bank") fun ApplicationCall.expectUriComponent(componentName: String) = - this.maybeUriComponent(componentName) ?: throw badRequest( - hint = "No username found in the URI", talerErrorCode = TalerErrorCode.GENERIC_PARAMETER_MISSING + maybeUriComponent(componentName) ?: throw badRequest( + "No username found in the URI", + TalerErrorCode.GENERIC_PARAMETER_MISSING ) /** Retrieve the bank account info for the selected username*/ -suspend fun ApplicationCall.bankAccount(db: Database): BankAccount { - return db.bankAccountGetFromCustomerLogin(username) ?: throw notFound( - hint = "Bank account for customer $username not found", - talerEc = TalerErrorCode.BANK_UNKNOWN_ACCOUNT +suspend fun ApplicationCall.bankAccount(db: Database): BankAccount + = db.bankAccountGetFromCustomerLogin(username) ?: throw notFound( + "Bank account for customer $username not found", + TalerErrorCode.BANK_UNKNOWN_ACCOUNT ) -} // Generates a new Payto-URI with IBAN scheme. fun genIbanPaytoUri(): String = "payto://iban/SANDBOXX/${getIban()}" @@ -109,7 +109,8 @@ fun ApplicationCall.uuidUriComponent(name: String): UUID { suspend fun ApplicationCall.getWithdrawal(db: Database, name: String): TalerWithdrawalOperation { val opId = uuidUriComponent(name) val op = db.withdrawal.get(opId) ?: throw notFound( - hint = "Withdrawal operation $opId not found", talerEc = TalerErrorCode.END + "Withdrawal operation $opId not found", + TalerErrorCode.BANK_TRANSACTION_NOT_FOUND ) return op } diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt @@ -40,7 +40,7 @@ class BankIntegrationApiTest { // Check unknown client.get("/taler-integration/withdrawal-operation/${UUID.randomUUID()}") - .assertNotFound() + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) // Check bad UUID client.get("/taler-integration/withdrawal-operation/chocolate") @@ -64,7 +64,7 @@ class BankIntegrationApiTest { // Check unknown client.post("/taler-integration/withdrawal-operation/${UUID.randomUUID()}") { jsonBody(req) - }.assertNotFound().assertErr(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) client.post("/accounts/merchant/withdrawals") { basicAuth("merchant", "merchant-password") @@ -86,7 +86,7 @@ class BankIntegrationApiTest { jsonBody(req) { "reserve_pub" to randEddsaPublicKey() } - }.assertConflict().assertErr(TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT) + }.assertConflict(TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT) } client.post("/accounts/merchant/withdrawals") { @@ -99,21 +99,21 @@ class BankIntegrationApiTest { // Check reserve_pub_reuse client.post("/taler-integration/withdrawal-operation/$uuid") { jsonBody(req) - }.assertConflict().assertErr(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT) + }.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT) // Check unknown account client.post("/taler-integration/withdrawal-operation/$uuid") { jsonBody { "reserve_pub" to randEddsaPublicKey() "selected_exchange" to IbanPayTo("payto://iban/UNKNOWN-IBAN-XYZ") } - }.assertConflict().assertErr(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + }.assertConflict(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) // Check account not exchange client.post("/taler-integration/withdrawal-operation/$uuid") { jsonBody { "reserve_pub" to randEddsaPublicKey() "selected_exchange" to IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ") } - }.assertConflict().assertErr(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) + }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) } } diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -204,7 +204,7 @@ class CoreBankAccountsMgmtApiTest { "password" to "password" "name" to "John Smith" } - }.assertConflict().assertErr(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) + }.assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) } // Testing login conflict @@ -212,16 +212,16 @@ class CoreBankAccountsMgmtApiTest { jsonBody(req) { "name" to "Foo" } - }.assertConflict().assertErr(TalerErrorCode.BANK_REGISTER_USERNAME_REUSE) + }.assertConflict(TalerErrorCode.BANK_REGISTER_USERNAME_REUSE) // Testing payto conflict client.post("/accounts") { jsonBody(req) { "username" to "bar" } - }.assertConflict().assertErr(TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE) + }.assertConflict(TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE) client.get("/accounts/bar") { basicAuth("admin", "admin-password") - }.assertNotFound().assertErr(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) } // Test account created with bonus @@ -251,10 +251,10 @@ class CoreBankAccountsMgmtApiTest { jsonBody(req) { "username" to "bar" } - }.assertConflict().assertErr(TalerErrorCode.BANK_UNALLOWED_DEBIT) + }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) client.get("/accounts/bar") { basicAuth("admin", "admin-password") - }.assertNotFound().assertErr(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) } // Test admin-only account creation @@ -282,13 +282,13 @@ class CoreBankAccountsMgmtApiTest { // Unknown account client.delete("/accounts/unknown") { basicAuth("admin", "admin-password") - }.assertNotFound().assertErr(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) // Reserved account reservedAccounts.forEach { client.delete("/accounts/$it") { basicAuth("admin", "admin-password") - }.assertConflict().assertErr(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) + }.assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) } // successful deletion @@ -305,7 +305,7 @@ class CoreBankAccountsMgmtApiTest { // Trying again must yield 404 client.delete("/accounts/john") { basicAuth("admin", "admin-password") - }.assertNotFound().assertErr(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) // fail to delete, due to a non-zero balance. @@ -317,7 +317,7 @@ class CoreBankAccountsMgmtApiTest { }.assertNoContent() client.delete("/accounts/merchant") { basicAuth("admin", "admin-password") - }.assertConflict().assertErr(TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO) + }.assertConflict(TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO) client.post("/accounts/merchant/transactions") { basicAuth("merchant", "merchant-password") jsonBody { @@ -335,7 +335,7 @@ class CoreBankAccountsMgmtApiTest { // Successful attempt now. val cashout = IbanPayTo(genIbanPaytoUri()) val req = json { - "cashout_address" to cashout.canonical + "cashout_payto_uri" to cashout.canonical "challenge_contact_data" to json { "phone" to "+99" "email" to "foo@example.com" @@ -671,7 +671,7 @@ class CoreBankTransactionsApiTest { // Check unknown transaction client.get("/accounts/merchant/transactions/3") { basicAuth("merchant", "merchant-password") - }.assertNotFound().assertErr(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) // Check wrong transaction client.get("/accounts/merchant/transactions/2") { basicAuth("merchant", "merchant-password") @@ -735,7 +735,7 @@ class CoreBankTransactionsApiTest { jsonBody(valid_req) { "amount" to "EUR:3.3" } - }.assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) // Surpassing the debt limit client.post("/accounts/merchant/transactions") { basicAuth("merchant", "merchant-password") @@ -743,7 +743,7 @@ class CoreBankTransactionsApiTest { jsonBody(valid_req) { "amount" to "KUDOS:555" } - }.assertConflict().assertErr(TalerErrorCode.BANK_UNALLOWED_DEBIT) + }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) // Missing message client.post("/accounts/merchant/transactions") { basicAuth("merchant", "merchant-password") @@ -759,7 +759,7 @@ class CoreBankTransactionsApiTest { jsonBody(valid_req) { "payto_uri" to "payto://iban/UNKNOWN-IBAN-XYZ?message=payout" } - }.assertConflict().assertErr(TalerErrorCode.BANK_UNKNOWN_CREDITOR) + }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR) // Transaction to self client.post("/accounts/merchant/transactions") { basicAuth("merchant", "merchant-password") @@ -767,7 +767,7 @@ class CoreBankTransactionsApiTest { jsonBody(valid_req) { "payto_uri" to "payto://iban/MERCHANT-IBAN-XYZ?message=payout" } - }.assertConflict().assertErr(TalerErrorCode.BANK_SAME_ACCOUNT) + }.assertConflict(TalerErrorCode.BANK_SAME_ACCOUNT) suspend fun checkBalance( merchantDebt: Boolean, @@ -811,7 +811,7 @@ class CoreBankTransactionsApiTest { jsonBody { "payto_uri" to "payto://iban/CUSTOMER-IBAN-XYZ?message=payout2&amount=KUDOS:3" } - }.assertConflict().assertErr(TalerErrorCode.BANK_UNALLOWED_DEBIT) + }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) checkBalance(true, "KUDOS:8.4", false, "KUDOS:6") // Send throught debt client.post("/accounts/customer/transactions") { @@ -838,13 +838,13 @@ class CoreBankWithdrawalApiTest { client.post("/accounts/exchange/withdrawals") { basicAuth("exchange", "exchange-password") jsonBody { "amount" to "KUDOS:9.0" } - }.assertConflict().assertErr(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) + }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) // Check insufficient fund client.post("/accounts/merchant/withdrawals") { basicAuth("merchant", "merchant-password") jsonBody { "amount" to "KUDOS:90" } - }.assertConflict().assertErr(TalerErrorCode.BANK_UNALLOWED_DEBIT) + }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) } // GET /withdrawals/withdrawal_id @@ -865,7 +865,8 @@ class CoreBankWithdrawalApiTest { client.get("/withdrawals/chocolate").assertBadRequest() // Check unknown - client.get("/withdrawals/${UUID.randomUUID()}").assertNotFound() + client.get("/withdrawals/${UUID.randomUUID()}") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } // POST /withdrawals/withdrawal_id/abort @@ -921,16 +922,16 @@ class CoreBankWithdrawalApiTest { client.post("/withdrawals/$uuid/confirm").assertNoContent() // Check error - client.post("/withdrawals/$uuid/abort").assertConflict() - .assertErr(TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT) + client.post("/withdrawals/$uuid/abort") + .assertConflict(TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT) } // Check bad UUID client.post("/withdrawals/chocolate/abort").assertBadRequest() // Check unknown - client.post("/withdrawals/${UUID.randomUUID()}/abort").assertNotFound() - .assertErr(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + client.post("/withdrawals/${UUID.randomUUID()}/abort") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } // POST /withdrawals/withdrawal_id/confirm @@ -945,8 +946,8 @@ class CoreBankWithdrawalApiTest { val uuid = resp.taler_withdraw_uri.split("/").last() // Check err - client.post("/withdrawals/$uuid/confirm").assertConflict() - .assertErr(TalerErrorCode.BANK_CONFIRM_INCOMPLETE) + client.post("/withdrawals/$uuid/confirm") + .assertConflict(TalerErrorCode.BANK_CONFIRM_INCOMPLETE) } // Check confirm selected @@ -985,8 +986,8 @@ class CoreBankWithdrawalApiTest { client.post("/withdrawals/$uuid/abort").assertNoContent() // Check error - client.post("/withdrawals/$uuid/confirm").assertConflict() - .assertErr(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) + client.post("/withdrawals/$uuid/confirm") + .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) } // Check balance insufficient @@ -1011,8 +1012,8 @@ class CoreBankWithdrawalApiTest { } }.assertNoContent() - client.post("/withdrawals/$uuid/confirm").assertConflict() - .assertErr(TalerErrorCode.BANK_UNALLOWED_DEBIT) + client.post("/withdrawals/$uuid/confirm") + .assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) // Check can abort because not confirmed client.post("/withdrawals/$uuid/abort").assertNoContent() @@ -1022,15 +1023,15 @@ class CoreBankWithdrawalApiTest { client.post("/withdrawals/chocolate/confirm").assertBadRequest() // Check unknown - client.post("/withdrawals/${UUID.randomUUID()}/confirm").assertNotFound() - .assertErr(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + client.post("/withdrawals/${UUID.randomUUID()}/confirm") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } } class CoreBankCashoutApiTest { - suspend fun tanCode(): String? { - val file = File("/tmp/cashout-tan.txt"); + suspend fun smsCode(info: String): String? { + val file = File("/tmp/tan-$info.txt"); if (file.exists()) { val code = file.readText() file.delete() @@ -1057,23 +1058,45 @@ class CoreBankCashoutApiTest { "request_uid" to randShortHashCode() "amount_debit" to "KUDOS:1" "amount_credit" to convert("KUDOS:1") - "tan_channel" to "file" } + // Check missing TAN info + client.post("/accounts/customer/cashouts") { + basicAuth("customer", "customer-password") + jsonBody(req) + }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO) + client.patch("/accounts/customer") { + basicAuth("customer", "customer-password") + jsonBody(json { + "challenge_contact_data" to json { + "phone" to "+99" + "email" to "foo@example.com" + } + }) + }.assertNoContent() + + // Check email TAN error + client.post("/accounts/customer/cashouts") { + basicAuth("customer", "customer-password") + jsonBody(req) { + "tan_channel" to "email" + } + }.assertStatus(HttpStatusCode.BadGateway, TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED) + // Check OK client.post("/accounts/customer/cashouts") { basicAuth("customer", "customer-password") jsonBody(req) }.assertOk().run { val id = json<CashoutPending>().cashout_id - tanCode() + smsCode("+99") // Check idempotency client.post("/accounts/customer/cashouts") { basicAuth("customer", "customer-password") jsonBody(req) }.assertOk().run { assertEquals(id, json<CashoutPending>().cashout_id) - assertNull(tanCode()) + assertNull(smsCode("+99")) } } @@ -1084,13 +1107,13 @@ class CoreBankCashoutApiTest { "amount_debit" to "KUDOS:2" "amount_credit" to convert("KUDOS:2") } - }.assertConflict().assertErr(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) + }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) // Check exchange account client.post("/accounts/exchange/cashouts") { basicAuth("exchange", "exchange-password") jsonBody(req) - }.assertConflict().assertErr(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) + }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) // Check insufficient fund client.post("/accounts/customer/cashouts") { @@ -1099,7 +1122,7 @@ class CoreBankCashoutApiTest { "amount_debit" to "KUDOS:75" "amount_credit" to convert("KUDOS:75") } - }.assertConflict().assertErr(TalerErrorCode.BANK_UNALLOWED_DEBIT) + }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) // Check wrong conversion client.post("/accounts/customer/cashouts") { @@ -1107,7 +1130,7 @@ class CoreBankCashoutApiTest { jsonBody(req) { "amount_credit" to convert("KUDOS:2") } - }.assertConflict().assertErr(TalerErrorCode.BANK_BAD_CONVERSION) + }.assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) // Check wrong currency client.post("/accounts/customer/cashouts") { @@ -1115,32 +1138,49 @@ class CoreBankCashoutApiTest { jsonBody(req) { "amount_debit" to "EUR:1" } - }.assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) client.post("/accounts/customer/cashouts") { basicAuth("customer", "customer-password") jsonBody(req) { "amount_credit" to "EUR:1" } - }.assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + } - // Check missing TAN info + // POST /accounts/{USERNAME}/cashouts + @Test + fun create_no_tan() = bankSetup("test_no_tan.conf") { _ -> + val req = json { + "request_uid" to randShortHashCode() + "amount_debit" to "KUDOS:1" + "amount_credit" to convert("KUDOS:1") + } + + // Check unsupported TAN channel client.post("/accounts/customer/cashouts") { basicAuth("customer", "customer-password") - jsonBody(req) { - "tan_channel" to "sms" - } - }.assertConflict().assertErr(TalerErrorCode.BANK_MISSING_TAN_INFO) + jsonBody(req) + }.assertStatus(HttpStatusCode.NotImplemented, TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED) } // POST /accounts/{USERNAME}/cashouts/{CASHOUT_ID}/abort @Test fun abort() = bankSetup { _ -> // TODO auth routine + client.patch("/accounts/customer") { + basicAuth("customer", "customer-password") + jsonBody(json { + "cashout_payto_uri" to IbanPayTo(genIbanPaytoUri()) + "challenge_contact_data" to json { + "phone" to "+99" + } + }) + }.assertNoContent() + val req = json { "request_uid" to randShortHashCode() "amount_debit" to "KUDOS:1" "amount_credit" to convert("KUDOS:1") - "tan_channel" to "file" } // Check abort created @@ -1169,40 +1209,47 @@ class CoreBankCashoutApiTest { client.post("/accounts/customer/cashouts/$uuid/confirm") { basicAuth("customer", "customer-password") - jsonBody { "tan" to tanCode() } + jsonBody { "tan" to smsCode("+99") } }.assertNoContent() // Check error client.post("/accounts/customer/cashouts/$uuid/abort") { basicAuth("customer", "customer-password") - }.assertConflict().assertErr(TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT) + }.assertConflict(TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT) } // Check bad UUID client.post("/accounts/customer/cashouts/chocolate/abort") { basicAuth("customer", "customer-password") - jsonBody { "tan" to tanCode() } + jsonBody { "tan" to "code" } }.assertBadRequest() // Check unknown client.post("/accounts/customer/cashouts/${UUID.randomUUID()}/abort") { basicAuth("customer", "customer-password") - jsonBody { "tan" to tanCode() } - }.assertNotFound().assertErr(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + jsonBody { "tan" to "code" } + }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } // POST /accounts/{USERNAME}/cashouts/{CASHOUT_ID}/confirm @Test fun confirm() = bankSetup { _ -> // TODO auth routine + client.patch("/accounts/customer") { + basicAuth("customer", "customer-password") + jsonBody(json { + "challenge_contact_data" to json { + "phone" to "+99" + } + }) + }.assertNoContent() + val req = json { "request_uid" to randShortHashCode() "amount_debit" to "KUDOS:1" "amount_credit" to convert("KUDOS:1") - "tan_channel" to "file" } - // TODO check sms and mail TAN channel // Check confirm client.post("/accounts/customer/cashouts") { basicAuth("customer", "customer-password") @@ -1210,14 +1257,29 @@ class CoreBankCashoutApiTest { }.assertOk().run { val uuid = json<CashoutPending>().cashout_id + // Check missing cashout address + client.post("/accounts/customer/cashouts/$uuid/confirm") { + basicAuth("customer", "customer-password") + jsonBody { "tan" to "code" } + }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO) + client.patch("/accounts/customer") { + basicAuth("customer", "customer-password") + jsonBody(json { + "cashout_payto_uri" to IbanPayTo(genIbanPaytoUri()) + "challenge_contact_data" to json { + "phone" to "+99" + } + }) + }.assertNoContent() + // Check bad TAN code client.post("/accounts/customer/cashouts/$uuid/confirm") { basicAuth("customer", "customer-password") jsonBody { "tan" to "nice-try" } - }.assertForbidden() - - val code = tanCode() + }.assertForbidden(TalerErrorCode.BANK_TAN_CHALLENGE_FAILED) + val code = smsCode("+99") + // Check OK client.post("/accounts/customer/cashouts/$uuid/confirm") { basicAuth("customer", "customer-password") @@ -1246,8 +1308,8 @@ class CoreBankCashoutApiTest { client.post("/accounts/customer/cashouts/$uuid/confirm"){ basicAuth("customer", "customer-password") - jsonBody { "tan" to tanCode() } - }.assertConflict().assertErr(TalerErrorCode.BANK_UNALLOWED_DEBIT) + jsonBody { "tan" to smsCode("+99") } + }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) // Check can abort because not confirmed client.post("/accounts/customer/cashouts/$uuid/abort") { @@ -1265,7 +1327,7 @@ class CoreBankCashoutApiTest { client.post("/accounts/customer/cashouts/${UUID.randomUUID()}/confirm") { basicAuth("customer", "customer-password") jsonBody { "tan" to "code" } - }.assertNotFound().assertErr(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } // GET /cashout-rate @@ -1281,12 +1343,12 @@ class CoreBankCashoutApiTest { client.get("/cashout-rate").assertBadRequest() // Too small client.get("/cashout-rate?amount_debit=KUDOS:0.08") - .assertConflict().assertErr(TalerErrorCode.BANK_BAD_CONVERSION) + .assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) // Wrong currency client.get("/cashout-rate?amount_debit=FIAT:1") - .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) client.get("/cashout-rate?amount_credit=KUDOS:1") - .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) } // GET /cashin-rate @@ -1306,8 +1368,8 @@ class CoreBankCashoutApiTest { client.get("/cashin-rate").assertBadRequest() // Wrong currency client.get("/cashin-rate?amount_debit=KUDOS:1") - .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) client.get("/cashin-rate?amount_credit=FIAT:1") - .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) } } \ No newline at end of file diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -97,7 +97,7 @@ class WireGatewayApiTest { if (requireAdmin) basicAuth("admin", "admin-password") else basicAuth("merchant", "merchant-password") - }.assertConflict().assertErr(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) + }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) } // Testing the POST /transfer call from the TWG API. @@ -117,7 +117,7 @@ class WireGatewayApiTest { client.post("/accounts/exchange/taler-wire-gateway/transfer") { basicAuth("exchange", "exchange-password") jsonBody(valid_req) - }.assertConflict().assertErr(TalerErrorCode.BANK_UNALLOWED_DEBIT) + }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) // Giving debt allowance and checking the OK case. setMaxDebt("exchange", TalerAmount("KUDOS:1000")) @@ -139,7 +139,7 @@ class WireGatewayApiTest { "wtid" to randShortHashCode() "exchange_base_url" to "http://different-exchange.example.com/" } - }.assertConflict().assertErr(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) + }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) // Currency mismatch client.post("/accounts/exchange/taler-wire-gateway/transfer") { @@ -147,7 +147,7 @@ class WireGatewayApiTest { jsonBody(valid_req) { "amount" to "EUR:33" } - }.assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) // Unknown account client.post("/accounts/exchange/taler-wire-gateway/transfer") { @@ -157,7 +157,7 @@ class WireGatewayApiTest { "wtid" to randShortHashCode() "credit_account" to "payto://iban/UNKNOWN-IBAN-XYZ" } - }.assertConflict().assertErr(TalerErrorCode.BANK_UNKNOWN_CREDITOR) + }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR) // Same account client.post("/accounts/exchange/taler-wire-gateway/transfer") { @@ -167,7 +167,7 @@ class WireGatewayApiTest { "wtid" to randShortHashCode() "credit_account" to "payto://iban/EXCHANGE-IBAN-XYZ" } - }.assertConflict().assertErr(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) + }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) // Bad BASE32 wtid client.post("/accounts/exchange/taler-wire-gateway/transfer") { @@ -499,7 +499,7 @@ class WireGatewayApiTest { client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { basicAuth("admin", "admin-password") jsonBody(valid_req) - }.assertConflict().assertErr(TalerErrorCode.BANK_UNALLOWED_DEBIT) + }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) // Giving debt allowance and checking the OK case. setMaxDebt("merchant", TalerAmount("KUDOS:1000")) @@ -512,13 +512,13 @@ class WireGatewayApiTest { client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { basicAuth("admin", "admin-password") jsonBody(valid_req) - }.assertConflict().assertErr(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT) + }.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT) // Currency mismatch client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { basicAuth("admin", "admin-password") jsonBody(valid_req) { "amount" to "EUR:33" } - }.assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) // Unknown account client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { @@ -527,7 +527,7 @@ class WireGatewayApiTest { "reserve_pub" to randEddsaPublicKey() "debit_account" to "payto://iban/UNKNOWN-IBAN-XYZ" } - }.assertConflict().assertErr(TalerErrorCode.BANK_UNKNOWN_DEBTOR) + }.assertConflict(TalerErrorCode.BANK_UNKNOWN_DEBTOR) // Same account client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { @@ -536,7 +536,7 @@ class WireGatewayApiTest { "reserve_pub" to randEddsaPublicKey() "debit_account" to "payto://iban/EXCHANGE-IBAN-XYZ" } - }.assertConflict().assertErr(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) + }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) // Bad BASE32 reserve_pub client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -105,18 +105,27 @@ suspend fun ApplicationTestBuilder.assertBalance(account: String, info: CreditDe /* ----- Assert ----- */ -fun HttpResponse.assertStatus(status: HttpStatusCode): HttpResponse { +suspend fun HttpResponse.assertStatus(status: HttpStatusCode, err: TalerErrorCode?): HttpResponse { assertEquals(status, this.status); + if (err != null) assertErr(err) return this } -fun HttpResponse.assertOk(): HttpResponse = assertStatus(HttpStatusCode.OK) -fun HttpResponse.assertCreated(): HttpResponse = assertStatus(HttpStatusCode.Created) -fun HttpResponse.assertNoContent(): HttpResponse = assertStatus(HttpStatusCode.NoContent) -fun HttpResponse.assertNotFound(): HttpResponse = assertStatus(HttpStatusCode.NotFound) -fun HttpResponse.assertUnauthorized(): HttpResponse = assertStatus(HttpStatusCode.Unauthorized) -fun HttpResponse.assertConflict(): HttpResponse = assertStatus(HttpStatusCode.Conflict) -fun HttpResponse.assertBadRequest(): HttpResponse = assertStatus(HttpStatusCode.BadRequest) -fun HttpResponse.assertForbidden(): HttpResponse = assertStatus(HttpStatusCode.Forbidden) +suspend fun HttpResponse.assertOk(): HttpResponse + = assertStatus(HttpStatusCode.OK, null) +suspend fun HttpResponse.assertCreated(): HttpResponse + = assertStatus(HttpStatusCode.Created, null) +suspend fun HttpResponse.assertNoContent(): HttpResponse + = assertStatus(HttpStatusCode.NoContent, null) +suspend fun HttpResponse.assertNotFound(err: TalerErrorCode?): HttpResponse + = assertStatus(HttpStatusCode.NotFound, err) +suspend fun HttpResponse.assertUnauthorized(): HttpResponse + = assertStatus(HttpStatusCode.Unauthorized, null) +suspend fun HttpResponse.assertConflict(err: TalerErrorCode?): HttpResponse + = assertStatus(HttpStatusCode.Conflict, err) +suspend fun HttpResponse.assertBadRequest(err: TalerErrorCode? = null): HttpResponse + = assertStatus(HttpStatusCode.BadRequest, err) +suspend fun HttpResponse.assertForbidden(err: TalerErrorCode? = null): HttpResponse + = assertStatus(HttpStatusCode.Forbidden, err) suspend fun HttpResponse.assertErr(code: TalerErrorCode): HttpResponse { diff --git a/contrib/libeufin-tan-fail.sh b/contrib/libeufin-tan-fail.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# This file is in the public domain. +exit 1 diff --git a/contrib/libeufin-tan-file.sh b/contrib/libeufin-tan-file.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# This file is in the public domain. +cat > /tmp/tan-$1.txt diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql @@ -36,7 +36,7 @@ CREATE TYPE token_scope_enum AS ENUM ('readonly', 'readwrite'); CREATE TYPE tan_enum - AS ENUM ('sms', 'email', 'file'); -- file is for testing purposes. + AS ENUM ('sms', 'email'); CREATE TYPE cashout_status_enum AS ENUM ('pending', 'confirmed'); @@ -205,8 +205,6 @@ CREATE TABLE IF NOT EXISTS cashout_operations ON UPDATE RESTRICT ); --- FIXME: table comment missing - -- end of: cashout management -- start of: Taler integration diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -171,6 +171,9 @@ LANGUAGE plpgsql AS $$ DECLARE my_customer_id INT8; BEGIN +IF (in_max_debt.val IS NULL) THEN + in_max_debt = NULL; +END IF; -- Get user ID and check reconfig rights SELECT customer_id, @@ -1028,7 +1031,6 @@ END IF; SELECT bank_account_id, is_taler_exchange, CASE - WHEN in_tan_channel = 'file' THEN login -- unused WHEN in_tan_channel = 'sms' THEN phone WHEN in_tan_channel = 'email' THEN email END @@ -1102,7 +1104,8 @@ CREATE OR REPLACE FUNCTION cashout_confirm( OUT out_bad_code BOOLEAN, OUT out_balance_insufficient BOOLEAN, OUT out_aborted BOOLEAN, - OUT out_no_retry BOOLEAN + OUT out_no_retry BOOLEAN, + OUT out_no_cashout_payto BOOLEAN ) LANGUAGE plpgsql as $$ DECLARE @@ -1119,18 +1122,22 @@ SELECT local_transaction IS NOT NULL, aborted, subject, bank_account, challenge, - (amount_debit).val, (amount_debit).frac + (amount_debit).val, (amount_debit).frac, + cashout_payto IS NULL INTO already_confirmed, out_aborted, subject_local, wallet_account_id, challenge_id, - amount_local.val, amount_local.frac - FROM cashout_operations + amount_local.val, amount_local.frac, + out_no_cashout_payto + FROM cashout_operations + JOIN bank_accounts ON bank_account_id=bank_account + JOIN customers ON customer_id=owning_customer_id WHERE cashout_uuid=in_cashout_uuid; IF NOT FOUND THEN out_no_op=TRUE; RETURN; -ELSIF already_confirmed OR out_aborted THEN +ELSIF already_confirmed OR out_aborted OR out_no_cashout_payto THEN RETURN; END IF; diff --git a/util/src/main/kotlin/TalerErrorCode.kt b/util/src/main/kotlin/TalerErrorCode.kt @@ -2250,7 +2250,7 @@ enum class TalerErrorCode(val code: Int) { /** - * The currency specified in the operation does not work with the current state of the given resource. + * The currency specified in the operation does not work with the current state of the given resource. * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). * (A value of 0 indicates that the error is generated client-side). */ @@ -3394,6 +3394,38 @@ enum class TalerErrorCode(val code: Int) { /** + * The request rate is too high. The server is refusing requests to guard against brute-force attacks. + * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TAN_RATE_LIMITED(5131), + + + /** + * This TAN channel is not supported. + * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TAN_CHANNEL_NOT_SUPPORTED(5132), + + + /** + * Failed to send TAN using the helper script. Either script is not found, or script timeout, or script terminated with a non-successful result. + * 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). + */ + BANK_TAN_CHANNEL_SCRIPT_FAILED(5133), + + + /** + * The client's response to the challenge was invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TAN_CHALLENGE_FAILED(5134), + + + /** * 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). @@ -4418,6 +4450,14 @@ enum class TalerErrorCode(val code: Int) { /** + * The token cannot be valid as no address was ever provided by the client. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_MISSING_ADDRESS(9759), + + + /** * End of error code range. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side).