libeufin

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

commit bc35aba3fbac892029ea17ee7ee4d1b4949650c0
parent b4e89515f9dab4aa6599ea71716135f6912caa8a
Author: Antoine A <>
Date:   Tue, 29 Oct 2024 14:12:57 +0100

bank: deprecate password auth

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Config.kt | 6++++--
Mbank/src/main/kotlin/tech/libeufin/bank/Constants.kt | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/api/ConversionApi.kt | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt | 99++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/api/RevenueApi.kt | 10+++++-----
Mbank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt | 26+++++++++++++-------------
Mbank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mbank/src/test/kotlin/AmountTest.kt | 1+
Mbank/src/test/kotlin/BankIntegrationApiTest.kt | 1+
Mbank/src/test/kotlin/ConversionApiTest.kt | 10++++------
Mbank/src/test/kotlin/CoreBankApiTest.kt | 201+++++++++++++++++++++++++++++++++----------------------------------------------
Mbank/src/test/kotlin/DatabaseTest.kt | 5++---
Mbank/src/test/kotlin/GcTest.kt | 5+++--
Mbank/src/test/kotlin/PaytoTest.kt | 1+
Mbank/src/test/kotlin/RevenueApiTest.kt | 1+
Mbank/src/test/kotlin/SecurityTest.kt | 1+
Mbank/src/test/kotlin/StatsTest.kt | 3++-
Mbank/src/test/kotlin/WireGatewayApiTest.kt | 1+
Mbank/src/test/kotlin/bench.kt | 34+++++++++++-----------------------
Mbank/src/test/kotlin/helpers.kt | 72++++++++++--------------------------------------------------------------
Mbank/src/test/kotlin/routines.kt | 31++++++-------------------------
Mcommon/src/main/kotlin/test/helpers.kt | 111++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcontrib/bank.conf | 4++++
Mtestbench/src/test/kotlin/IntegrationTest.kt | 55+++++++++++++++++++++++++------------------------------
24 files changed, 406 insertions(+), 372 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -54,7 +54,8 @@ data class BankConfig( val gcAbortAfter: Duration, val gcCleanAfter: Duration, val gcDeleteAfter: Duration, - val pwdCheckQuality: Boolean + val pwdCheckQuality: Boolean, + val basicAuthCompat: Boolean ) { val dbCfg: DatabaseConfig by lazy { val sect = cfg.section("libeufin-bankdb-postgres") @@ -155,7 +156,8 @@ private fun TalerConfig.loadBankConfig(): BankConfig = section("libeufin-bank"). gcAbortAfter = duration("gc_abort_after").require(), gcCleanAfter = duration("gc_clean_after").require(), gcDeleteAfter = duration("gc_delete_after").require(), - pwdCheckQuality = boolean("pwd_check").default(true) + pwdCheckQuality = boolean("pwd_check").require(), + basicAuthCompat = boolean("pwd_auth_compat").require() ) } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt @@ -37,6 +37,6 @@ val RESERVED_ACCOUNTS = setOf("admin", "bank") const val IBAN_ALLOCATION_RETRY_COUNTER: Int = 5 // API version -const val COREBANK_API_VERSION: String = "5:2:2" +const val COREBANK_API_VERSION: String = "5:3:2" const val CONVERSION_API_VERSION: String = "0:1:0" const val INTEGRATION_API_VERSION: String = "4:0:4" diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/ConversionApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/ConversionApi.kt @@ -98,7 +98,7 @@ fun Routing.conversionApi(db: Database, ctx: BankConfig) = conditional(ctx.allow } } } - authAdmin(db, ctx.pwCrypto, TokenScope.readwrite) { + authAdmin(db, ctx.pwCrypto, TokenScope.readwrite, ctx.basicAuthCompat) { post("/conversion-info/conversion-rate") { val req = call.receive<ConversionRate>() for (regionalAmount in sequenceOf(req.cashin_fee, req.cashin_tiny_amount, req.cashout_min_amount)) { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt @@ -70,7 +70,7 @@ fun Routing.coreBankApi(db: Database, cfg: BankConfig) { ) ) } - authAdmin(db, cfg.pwCrypto, TokenScope.readonly) { + authAdmin(db, cfg.pwCrypto, TokenScope.readonly, cfg.basicAuthCompat) { get("/monitor") { val params = MonitorParams.extract(call.request.queryParameters) call.respond(db.monitor(params)) @@ -86,7 +86,7 @@ fun Routing.coreBankApi(db: Database, cfg: BankConfig) { private fun Routing.coreBankTokenApi(db: Database, cfg: BankConfig) { val TOKEN_DEFAULT_DURATION: Duration = Duration.ofDays(1L) - auth(db, cfg.pwCrypto, TokenScope.refreshable) { + auth(db, cfg.pwCrypto, TokenScope.refreshable, cfg.basicAuthCompat, allowPw = true) { post("/accounts/{USERNAME}/token") { val existingToken = call.authToken val req = call.receive<TokenRequest>() @@ -137,7 +137,7 @@ private fun Routing.coreBankTokenApi(db: Database, cfg: BankConfig) { ) } } - auth(db, cfg.pwCrypto, TokenScope.readonly) { + auth(db, cfg.pwCrypto, TokenScope.readonly, cfg.basicAuthCompat) { delete("/accounts/{USERNAME}/token") { val token = call.authToken ?: throw badRequest("Basic auth not supported here.") db.token.delete(token) @@ -306,11 +306,11 @@ suspend fun patchAccount( ) } -private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { - authAdmin(db, ctx.pwCrypto, TokenScope.readwrite, !ctx.allowRegistration) { +private fun Routing.coreBankAccountsApi(db: Database, cfg: BankConfig) { + authAdmin(db, cfg.pwCrypto, TokenScope.readwrite, cfg.basicAuthCompat, !cfg.allowRegistration) { post("/accounts") { val req = call.receive<RegisterAccountRequest>() - when (val result = createAccount(db, ctx, req, call.isAdmin)) { + when (val result = createAccount(db, cfg, req, call.isAdmin)) { AccountCreationResult.BonusBalanceInsufficient -> throw conflict( "Insufficient admin funds to grant bonus", TalerErrorCode.BANK_UNALLOWED_DEBIT @@ -329,10 +329,11 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { } auth( db, - ctx.pwCrypto, + cfg.pwCrypto, TokenScope.readwrite, + cfg.basicAuthCompat, allowAdmin = true, - requireAdmin = !ctx.allowAccountDeletion + requireAdmin = !cfg.allowAccountDeletion ) { delete("/accounts/{USERNAME}") { val challenge = call.checkChallenge(db, Operation.account_delete) @@ -343,7 +344,7 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { "Cannot delete reserved accounts", TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT ) - if (call.username == "exchange" && ctx.allowConversion) + if (call.username == "exchange" && cfg.allowConversion) throw conflict( "Cannot delete 'exchange' accounts when conversion is enabled", TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT @@ -360,10 +361,10 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { } } } - auth(db, ctx.pwCrypto, TokenScope.readwrite, allowAdmin = true) { + auth(db, cfg.pwCrypto, TokenScope.readwrite, cfg.basicAuthCompat, allowAdmin = true) { patch("/accounts/{USERNAME}") { val (req, challenge) = call.receiveChallenge<AccountReconfiguration>(db, Operation.account_reconfig) - val res = patchAccount(db, ctx, req, call.username, call.isAdmin, challenge != null, challenge?.channel, challenge?.info) + val res = patchAccount(db, cfg, req, call.username, call.isAdmin, challenge != null, challenge?.channel, challenge?.info) when (res) { AccountPatchResult.Success -> call.respond(HttpStatusCode.NoContent) is AccountPatchResult.TanRequired -> { @@ -401,9 +402,9 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD ) } - val newPassword = req.new_password.checkPw(ctx.pwdCheckQuality) + val newPassword = req.new_password.checkPw(cfg.pwdCheckQuality) - when (db.account.reconfigPassword(call.username, newPassword, req.old_password, call.isAdmin || challenge != null, ctx.pwCrypto)) { + when (db.account.reconfigPassword(call.username, newPassword, req.old_password, call.isAdmin || challenge != null, cfg.pwCrypto)) { AccountPatchAuthResult.Success -> call.respond(HttpStatusCode.NoContent) AccountPatchAuthResult.TanRequired -> call.respondChallenge(db, Operation.account_auth_reconfig, req) AccountPatchAuthResult.UnknownAccount -> throw unknownAccount(call.username) @@ -416,17 +417,17 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { } get("/public-accounts") { val params = AccountParams.extract(call.request.queryParameters) - val publicAccounts = db.account.pagePublic(params, ctx.payto) + val publicAccounts = db.account.pagePublic(params, cfg.payto) if (publicAccounts.isEmpty()) { call.respond(HttpStatusCode.NoContent) } else { call.respond(PublicAccountsResponse(publicAccounts)) } } - authAdmin(db, ctx.pwCrypto, TokenScope.readonly) { + authAdmin(db, cfg.pwCrypto, TokenScope.readonly, cfg.basicAuthCompat) { get("/accounts") { val params = AccountParams.extract(call.request.queryParameters) - val accounts = db.account.pageAdmin(params, ctx.payto) + val accounts = db.account.pageAdmin(params, cfg.payto) if (accounts.isEmpty()) { call.respond(HttpStatusCode.NoContent) } else { @@ -434,22 +435,22 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { } } } - auth(db, ctx.pwCrypto, TokenScope.readonly, allowAdmin = true) { + auth(db, cfg.pwCrypto, TokenScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { get("/accounts/{USERNAME}") { - val account = db.account.get(call.username, ctx.payto) ?: throw unknownAccount(call.username) + val account = db.account.get(call.username, cfg.payto) ?: throw unknownAccount(call.username) call.respond(account) } } } -private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { - auth(db, ctx.pwCrypto, TokenScope.readonly, allowAdmin = true) { +private fun Routing.coreBankTransactionsApi(db: Database, cfg: BankConfig) { + auth(db, cfg.pwCrypto, TokenScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { get("/accounts/{USERNAME}/transactions") { val params = HistoryParams.extract(call.request.queryParameters) - val bankAccount = call.bankInfo(db, ctx.payto) + val bankAccount = call.bankInfo(db, cfg.payto) val history: List<BankAccountTransactionInfo> = - db.transaction.pollHistory(params, bankAccount.bankAccountId, ctx.payto) + db.transaction.pollHistory(params, bankAccount.bankAccountId, cfg.payto) if (history.isEmpty()) { call.respond(HttpStatusCode.NoContent) } else { @@ -458,21 +459,21 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { } get("/accounts/{USERNAME}/transactions/{T_ID}") { val tId = call.longPath("T_ID") - val tx = db.transaction.get(tId, call.username, ctx.payto) ?: throw notFound( + val tx = db.transaction.get(tId, call.username, cfg.payto) ?: throw notFound( "Bank transaction '$tId' not found", TalerErrorCode.BANK_TRANSACTION_NOT_FOUND ) call.respond(tx) } } - auth(db, ctx.pwCrypto, TokenScope.readwrite) { + auth(db, cfg.pwCrypto, TokenScope.readwrite, cfg.basicAuthCompat) { post("/accounts/{USERNAME}/transactions") { val (req, challenge) = call.receiveChallenge<TransactionCreateRequest>(db, Operation.bank_transaction) val subject = req.payto_uri.message ?: throw badRequest("Wire transfer lacks subject") val amount = req.payto_uri.amount ?: req.amount ?: throw badRequest("Wire transfer lacks amount") - ctx.checkRegionalCurrency(amount) + cfg.checkRegionalCurrency(amount) val res = db.transaction.create( creditAccountPayto = req.payto_uri, @@ -482,9 +483,9 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { timestamp = Instant.now(), requestUid = req.request_uid, is2fa = challenge != null, - wireTransferFees = ctx.wireTransferFees, - minAmount = ctx.minAmount, - maxAmount = ctx.maxAmount + wireTransferFees = cfg.wireTransferFees, + minAmount = cfg.minAmount, + maxAmount = cfg.maxAmount ) when (res) { BankTransactionResult.UnknownDebtor -> throw unknownAccount(call.username) @@ -518,12 +519,12 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { } } -private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) { - auth(db, ctx.pwCrypto, TokenScope.readwrite) { +private fun Routing.coreBankWithdrawalApi(db: Database, cfg: BankConfig) { + auth(db, cfg.pwCrypto, TokenScope.readwrite, cfg.basicAuthCompat) { post("/accounts/{USERNAME}/withdrawals") { val req = call.receive<BankAccountCreateWithdrawalRequest>() - req.amount?.run(ctx::checkRegionalCurrency) - req.suggested_amount?.run(ctx::checkRegionalCurrency) + req.amount?.run(cfg::checkRegionalCurrency) + req.suggested_amount?.run(cfg::checkRegionalCurrency) val opId = UUID.randomUUID() when (db.withdrawal.create( call.username, @@ -531,9 +532,9 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) { req.amount, req.suggested_amount, Instant.now(), - ctx.wireTransferFees, - ctx.minAmount, - ctx.maxAmount + cfg.wireTransferFees, + cfg.minAmount, + cfg.maxAmount )) { WithdrawalCreationResult.UnknownAccount -> throw unknownAccount(call.username) WithdrawalCreationResult.AccountIsExchange -> throw conflict( @@ -561,16 +562,16 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) { post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") { val id = call.uuidPath("withdrawal_id") val (req, challenge) = call.receiveChallenge<BankAccountConfirmWithdrawalRequest>(db, Operation.withdrawal, BankAccountConfirmWithdrawalRequest()) - req.amount?.run(ctx::checkRegionalCurrency) + req.amount?.run(cfg::checkRegionalCurrency) when (db.withdrawal.confirm( call.username, id, Instant.now(), req.amount, challenge != null, - ctx.wireTransferFees, - ctx.minAmount, - ctx.maxAmount + cfg.wireTransferFees, + cfg.minAmount, + cfg.maxAmount )) { WithdrawalConfirmationResult.UnknownOperation -> throw notFound( "Withdrawal operation $id not found", @@ -636,13 +637,13 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) { } } -private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) = conditional(ctx.allowConversion) { - auth(db, ctx.pwCrypto, TokenScope.readwrite) { +private fun Routing.coreBankCashoutApi(db: Database, cfg: BankConfig) = conditional(cfg.allowConversion) { + auth(db, cfg.pwCrypto, TokenScope.readwrite, cfg.basicAuthCompat) { post("/accounts/{USERNAME}/cashouts") { val (req, challenge) = call.receiveChallenge<CashoutRequest>(db, Operation.cashout) - ctx.checkRegionalCurrency(req.amount_debit) - ctx.checkFiatCurrency(req.amount_credit) + cfg.checkRegionalCurrency(req.amount_debit) + cfg.checkFiatCurrency(req.amount_credit) val res = db.cashout.create( username = call.username, @@ -686,7 +687,7 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) = conditio } } } - auth(db, ctx.pwCrypto, TokenScope.readonly, allowAdmin = true) { + auth(db, cfg.pwCrypto, TokenScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { get("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}") { val id = call.longPath("CASHOUT_ID") val cashout = db.cashout.get(id, call.username) ?: throw notFound( @@ -705,7 +706,7 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) = conditio } } } - authAdmin(db, ctx.pwCrypto, TokenScope.readonly) { + authAdmin(db, cfg.pwCrypto, TokenScope.readonly, cfg.basicAuthCompat) { get("/cashouts") { val params = PageParams.extract(call.request.queryParameters) val cashouts = db.cashout.pageAll(params) @@ -718,8 +719,8 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) = conditio } } -private fun Routing.coreBankTanApi(db: Database, ctx: BankConfig) { - auth(db, ctx.pwCrypto, TokenScope.readwrite) { +private fun Routing.coreBankTanApi(db: Database, cfg: BankConfig) { + auth(db, cfg.pwCrypto, TokenScope.readwrite, cfg.basicAuthCompat) { post("/accounts/{USERNAME}/challenge/{CHALLENGE_ID}") { val id = call.longPath("CHALLENGE_ID") val res = db.tan.send( @@ -737,9 +738,9 @@ private fun Routing.coreBankTanApi(db: Database, ctx: BankConfig) { ) is TanSendResult.Success -> { res.tanCode?.run { - val (tanScript, tanEnv) = ctx.tanChannels[res.tanChannel] + val (tanScript, tanEnv) = cfg.tanChannels[res.tanChannel] ?: throw unsupportedTanChannel(res.tanChannel) - val msg = "T-${res.tanCode} is your ${ctx.name} verification code" + val msg = "T-${res.tanCode} is your ${cfg.name} verification code" val exitValue = withContext(Dispatchers.IO) { val builder = ProcessBuilder(tanScript.toString(), res.tanInfo) builder.redirectErrorStream(true) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/RevenueApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/RevenueApi.kt @@ -31,17 +31,17 @@ import tech.libeufin.common.HistoryParams import tech.libeufin.common.RevenueConfig import tech.libeufin.common.RevenueIncomingHistory -fun Routing.revenueApi(db: Database, ctx: BankConfig) { - auth(db, ctx.pwCrypto, TokenScope.revenue) { +fun Routing.revenueApi(db: Database, cfg: BankConfig) { + auth(db, cfg.pwCrypto, TokenScope.revenue, cfg.basicAuthCompat) { get("/accounts/{USERNAME}/taler-revenue/config") { call.respond(RevenueConfig( - currency = ctx.regionalCurrency + currency = cfg.regionalCurrency )) } get("/accounts/{USERNAME}/taler-revenue/history") { val params = HistoryParams.extract(call.request.queryParameters) - val bankAccount = call.bankInfo(db, ctx.payto) - val items = db.transaction.revenueHistory(params, bankAccount.bankAccountId, ctx.payto) + val bankAccount = call.bankInfo(db, cfg.payto) + val items = db.transaction.revenueHistory(params, bankAccount.bankAccountId, cfg.payto) if (items.isEmpty()) { call.respond(HttpStatusCode.NoContent) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt @@ -39,16 +39,16 @@ import tech.libeufin.common.* import java.time.Instant -fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { - auth(db, ctx.pwCrypto, TokenScope.readwrite) { +fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) { + auth(db, cfg.pwCrypto, TokenScope.readwrite, cfg.basicAuthCompat) { get("/accounts/{USERNAME}/taler-wire-gateway/config") { call.respond(WireGatewayConfig( - currency = ctx.regionalCurrency + currency = cfg.regionalCurrency )) } post("/accounts/{USERNAME}/taler-wire-gateway/transfer") { val req = call.receive<TransferRequest>() - ctx.checkRegionalCurrency(req.amount) + cfg.checkRegionalCurrency(req.amount) val res = db.exchange.transfer( req = req, username = call.username, @@ -81,13 +81,13 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { } } } - auth(db, ctx.pwCrypto, TokenScope.readonly) { + auth(db, cfg.pwCrypto, TokenScope.readonly, cfg.basicAuthCompat) { suspend fun <T> ApplicationCall.historyEndpoint( reduce: (List<T>, String) -> Any, dbLambda: suspend ExchangeDAO.(HistoryParams, Long, BankPaytoCtx) -> List<T> ) { val params = HistoryParams.extract(this.request.queryParameters) - val bankAccount = this.bankInfo(db, ctx.payto) + val bankAccount = this.bankInfo(db, cfg.payto) if (!bankAccount.isTalerExchange) throw conflict( @@ -95,7 +95,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE ) - val items = db.exchange.dbLambda(params, bankAccount.bankAccountId, ctx.payto) + val items = db.exchange.dbLambda(params, bankAccount.bankAccountId, cfg.payto) if (items.isEmpty()) { this.respond(HttpStatusCode.NoContent) @@ -111,7 +111,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { } get("/accounts/{USERNAME}/taler-wire-gateway/transfers") { val params = TransferParams.extract(call.request.queryParameters) - val bankAccount = call.bankInfo(db, ctx.payto) + val bankAccount = call.bankInfo(db, cfg.payto) if (!bankAccount.isTalerExchange) throw conflict( "${call.username} is not an exchange account.", @@ -121,7 +121,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { if (params.status != null && params.status != TransferStatusState.success && params.status != TransferStatusState.permanent_failure) { call.respond(HttpStatusCode.NoContent) } else { - val items = db.exchange.pageTransfer(params.page, bankAccount.bankAccountId, params.status, ctx.payto) + val items = db.exchange.pageTransfer(params.page, bankAccount.bankAccountId, params.status, cfg.payto) if (items.isEmpty()) { call.respond(HttpStatusCode.NoContent) @@ -131,7 +131,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { } } get("/accounts/{USERNAME}/taler-wire-gateway/transfers/{ROW_ID}") { - val bankAccount = call.bankInfo(db, ctx.payto) + val bankAccount = call.bankInfo(db, cfg.payto) if (!bankAccount.isTalerExchange) throw conflict( "${call.username} is not an exchange account.", @@ -139,21 +139,21 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { ) val txId = call.longPath("ROW_ID") - val transfer = db.exchange.getTransfer(bankAccount.bankAccountId, txId, ctx.payto) ?: throw notFound( + val transfer = db.exchange.getTransfer(bankAccount.bankAccountId, txId, cfg.payto) ?: throw notFound( "Transfer '$txId' not found", TalerErrorCode.BANK_TRANSACTION_NOT_FOUND ) call.respond(transfer) } } - authAdmin(db, ctx.pwCrypto, TokenScope.readwrite) { + authAdmin(db, cfg.pwCrypto, TokenScope.readwrite, cfg.basicAuthCompat) { suspend fun ApplicationCall.addIncoming( amount: TalerAmount, debitAccount: Payto, subject: String, metadata: TalerIncomingMetadata ) { - ctx.checkRegionalCurrency(amount) + cfg.checkRegionalCurrency(amount) val timestamp = Instant.now() val res = db.exchange.addIncoming( amount = amount, diff --git a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt @@ -19,6 +19,8 @@ package tech.libeufin.bank.auth +import org.slf4j.Logger +import org.slf4j.LoggerFactory import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.response.* @@ -34,6 +36,8 @@ import tech.libeufin.common.api.intercept import tech.libeufin.common.crypto.PwCrypto import java.time.Instant +private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-auth") + /** Used to store if the currently authenticated user is admin */ private val AUTH_IS_ADMIN = AttributeKey<Boolean>("is_admin") @@ -58,23 +62,29 @@ val ApplicationCall.authToken: ByteArray? get() = attributes.getOrNull(AUTH_TOKE * * You can check is the currently authenticated user is admin using [isAdmin]. **/ -fun Route.authAdmin(db: Database, pwCrypto: PwCrypto, scope: TokenScope, enforce: Boolean = true, callback: Route.() -> Unit): Route = - intercept("AuthAdmin", callback) { - if (enforce) { - val username = this.authenticateBankRequest(db, pwCrypto, scope) - if (username != "admin") { - throw unauthorized("Only administrator allowed") - } - this.attributes.put(AUTH_IS_ADMIN, true) - } else { - val username = try { - this.authenticateBankRequest(db, pwCrypto, scope) - } catch (e: Exception) { - null - } - this.attributes.put(AUTH_IS_ADMIN, username == "admin") +fun Route.authAdmin( + db: Database, + pwCrypto: PwCrypto, + scope: TokenScope, + compatPw: Boolean, + enforce: Boolean = true, + callback: Route.() -> Unit +): Route = intercept("AuthAdmin", callback) { + if (enforce) { + val username = this.authenticateBankRequest(db, pwCrypto, scope, false, compatPw) + if (username != "admin") { + throw unauthorized("Only administrator allowed") + } + this.attributes.put(AUTH_IS_ADMIN, true) + } else { + val username = try { + this.authenticateBankRequest(db, pwCrypto, scope, false, compatPw) + } catch (e: Exception) { + null } + this.attributes.put(AUTH_IS_ADMIN, username == "admin") } +} /** @@ -85,19 +95,27 @@ fun Route.authAdmin(db: Database, pwCrypto: PwCrypto, scope: TokenScope, enforce * * You can check is the currently authenticated user is admin using [isAdmin]. **/ -fun Route.auth(db: Database, pwCrypto: PwCrypto ,scope: TokenScope, allowAdmin: Boolean = false, requireAdmin: Boolean = false, callback: Route.() -> Unit): Route = - intercept("Auth", callback) { - val authUsername = this.authenticateBankRequest(db, pwCrypto, scope) - if (requireAdmin && authUsername != "admin") { - throw unauthorized("Only administrator allowed") - } else { - val hasRight = authUsername == username || (allowAdmin && authUsername == "admin") - if (!hasRight) { - throw unauthorized("Customer $authUsername have no right on $username account") - } +fun Route.auth( + db: Database, + pwCrypto: PwCrypto, + scope: TokenScope, + compatPw: Boolean, + allowAdmin: Boolean = false, + requireAdmin: Boolean = false, + allowPw: Boolean = false, + callback: Route.() -> Unit +): Route = intercept("Auth", callback) { + val authUsername = this.authenticateBankRequest(db, pwCrypto, scope, allowPw, compatPw) + if (requireAdmin && authUsername != "admin") { + throw unauthorized("Only administrator allowed") + } else { + val hasRight = authUsername == username || (allowAdmin && authUsername == "admin") + if (!hasRight) { + throw unauthorized("Customer $authUsername have no right on $username account") } - this.attributes.put(AUTH_IS_ADMIN, authUsername == "admin") } + this.attributes.put(AUTH_IS_ADMIN, authUsername == "admin") +} /** * Authenticate an HTTP request for [requiredScope] according to the scheme that is mentioned @@ -106,7 +124,13 @@ fun Route.auth(db: Database, pwCrypto: PwCrypto ,scope: TokenScope, allowAdmin: * * Returns the authenticated customer username. */ -private suspend fun ApplicationCall.authenticateBankRequest(db: Database, pwCrypto: PwCrypto, requiredScope: TokenScope): String { +private suspend fun ApplicationCall.authenticateBankRequest( + db: Database, + pwCrypto: PwCrypto, + requiredScope: TokenScope, + allowPw: Boolean, + compatPw: Boolean +): String { val header = request.headers[HttpHeaders.Authorization] // Basic auth challenge @@ -124,9 +148,9 @@ private suspend fun ApplicationCall.authenticateBankRequest(db: Database, pwCryp TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED ) return when (scheme) { - "Basic" -> doBasicAuth(db, content, pwCrypto) + "Basic" -> doBasicAuth(db, content, pwCrypto, allowPw, compatPw) "Bearer" -> doTokenAuth(db, content, requiredScope) - else -> throw unauthorized("Authorization method wrong or not supported") + else -> throw unauthorized("Authorization method '$scheme' wrong or not supported") } } @@ -135,12 +159,24 @@ private suspend fun ApplicationCall.authenticateBankRequest(db: Database, pwCryp * * Returns the authenticated customer username */ -private suspend fun doBasicAuth(db: Database, encoded: String, pwCrypto: PwCrypto): String { +private suspend fun doBasicAuth( + db: Database, + encoded: String, + pwCrypto: PwCrypto, + allowPw: Boolean, + compatPw: Boolean +): String { val decoded = String(encoded.decodeBase64(), Charsets.UTF_8) val (username, plainPassword) = decoded.splitOnce(":") ?: throw badRequest( "Malformed Basic auth credentials found in the Authorization header", TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED ) + if (!allowPw) { + logger.warn("User '$username' used deprecated password auth") + if (!compatPw) { + throw unauthorized("Authorization method 'Basic' not supported") + } + } return when (db.account.checkPassword(username, plainPassword, pwCrypto)) { CheckPasswordResult.UnknownAccount -> throw unauthorized("Unknown account") CheckPasswordResult.PasswordMismatch -> throw unauthorized("Bad password") diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt @@ -21,6 +21,7 @@ import io.ktor.http.* import org.junit.Test import tech.libeufin.common.* import tech.libeufin.common.db.* +import tech.libeufin.common.test.* import kotlin.test.assertEquals class AmountTest { diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt @@ -24,6 +24,7 @@ import tech.libeufin.bank.BankWithdrawalOperationPostResponse import tech.libeufin.bank.BankWithdrawalOperationStatus import tech.libeufin.bank.WithdrawalStatus import tech.libeufin.common.* +import tech.libeufin.common.test.* import java.util.* import kotlin.test.assertEquals diff --git a/bank/src/test/kotlin/ConversionApiTest.kt b/bank/src/test/kotlin/ConversionApiTest.kt @@ -21,6 +21,7 @@ import io.ktor.client.request.* import org.junit.Test import tech.libeufin.bank.ConversionResponse import tech.libeufin.common.* +import tech.libeufin.common.test.* import kotlin.test.assertEquals class ConversionApiTest { @@ -46,20 +47,17 @@ class ConversionApiTest { "cashout_min_amount" to "KUDOS:0.1" } // Good rates - client.post("/conversion-info/conversion-rate") { - pwAuth("admin") + client.postAdmin("/conversion-info/conversion-rate") { json(ok) }.assertNoContent() // Bad currency - client.post("/conversion-info/conversion-rate") { - pwAuth("admin") + client.postAdmin("/conversion-info/conversion-rate") { json(ok) { "cashout_fee" to "CHF:0.003" } }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) // Subcent cashout tiny amount - client.post("/conversion-info/conversion-rate") { - pwAuth("admin") + client.postAdmin("/conversion-info/conversion-rate") { json(ok) { "cashout_tiny_amount" to "EUR:0.0001" } diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -28,6 +28,7 @@ import tech.libeufin.bank.auth.TOKEN_PREFIX import tech.libeufin.common.* import tech.libeufin.common.crypto.CryptoUtil import tech.libeufin.common.db.one +import tech.libeufin.common.test.* import java.time.Duration import java.time.Instant import java.util.* @@ -83,12 +84,8 @@ class CoreBankConfigTest { fun monitor() = bankSetup { authRoutine(HttpMethod.Get, "/monitor", requireAdmin = true) // Check OK - client.get("/monitor?timeframe=day&which=25") { - pwAuth("admin") - }.assertOk() - client.get("/monitor?timeframe=day=which=25") { - pwAuth("admin") - }.assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) + client.getAdmin("/monitor?timeframe=day&which=25").assertOk() + client.getAdmin("/monitor?timeframe=day=which=25").assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) } } @@ -98,8 +95,23 @@ class CoreBankTokenApiTest { fun post() = bankSetup { db -> authRoutine(HttpMethod.Post, "/accounts/merchant/token") + // Unknown account + client.post("/accounts/merchant/token") { + basicAuth("unknown", "password") + }.assertUnauthorized() + + // Wrong password + client.post("/accounts/merchant/token") { + basicAuth("merchant", "wrong-password") + }.assertUnauthorized() + + // Wrong account + client.post("/accounts/merchant/token") { + basicAuth("exchange", "merchant-password") + }.assertUnauthorized() + // New default token - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to "readonly" } }.assertOkJson<TokenSuccessResponse> { // Checking that the token lifetime defaulted to 24 hours. @@ -109,7 +121,7 @@ class CoreBankTokenApiTest { } // Check default duration - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to "readonly" } }.assertOkJson<TokenSuccessResponse> { // Checking that the token lifetime defaulted to 24 hours. @@ -127,7 +139,7 @@ class CoreBankTokenApiTest { "readwrite" to "revenue", "readonly" to "revenue", )) { - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to fromScope "refreshable" to true @@ -147,7 +159,7 @@ class CoreBankTokenApiTest { "revenue" to "readonly", "revenue" to "readwrite" )) { - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to fromScope "refreshable" to true @@ -162,7 +174,7 @@ class CoreBankTokenApiTest { } // Check no refreshable - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to "readonly" } @@ -175,7 +187,7 @@ class CoreBankTokenApiTest { } // Check 'forever' case. - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to "readonly" "duration" to obj { @@ -188,7 +200,7 @@ class CoreBankTokenApiTest { } // Check too big or invalid durations - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to "readonly" "duration" to obj { @@ -196,7 +208,7 @@ class CoreBankTokenApiTest { } } }.assertBadRequest() - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to "readonly" "duration" to obj { @@ -204,7 +216,7 @@ class CoreBankTokenApiTest { } } }.assertBadRequest() - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to "readonly" "duration" to obj { @@ -217,8 +229,7 @@ class CoreBankTokenApiTest { // DELETE /accounts/USERNAME/token @Test fun delete() = bankSetup { - // TODO test restricted - val token = client.postA("/accounts/merchant/token") { + val token = client.postPw("/accounts/merchant/token") { json { "scope" to "readonly" } }.assertOkJson<TokenSuccessResponse>().access_token // Check OK @@ -229,9 +240,6 @@ class CoreBankTokenApiTest { client.delete("/accounts/merchant/token") { headers[HttpHeaders.Authorization] = "Bearer $token" }.assertUnauthorized() - - // Checking merchant can still be served by basic auth, after token deletion. - client.getA("/accounts/merchant").assertOk() } // GET /accounts/USERNAME/token @@ -239,28 +247,30 @@ class CoreBankTokenApiTest { fun get() = bankSetup { // Check OK for (account in listOf("merchant", "customer")) { - client.getA("/accounts/$account/tokens").assertNoContent() + client.getA("/accounts/$account/tokens").assertOkJson<TokenInfos> { + assertEquals(1, it.tokens.size) + } } - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to "readonly" } }.assertOk() - client.postA("/accounts/merchant/token") { + client.postPw("/accounts/merchant/token") { json { "scope" to "readwrite" } }.assertOk() - client.postA("/accounts/customer/token") { + client.postPw("/accounts/customer/token") { json { "scope" to "revenue" "description" to "description" } }.assertOk() client.getA("/accounts/merchant/tokens").assertOkJson<TokenInfos> { - assertEquals(2, it.tokens.size) + assertEquals(3, it.tokens.size) for (token in it.tokens) { assertNull(token.description) } } client.getA("/accounts/customer/tokens").assertOkJson<TokenInfos> { - assertEquals(1, it.tokens.size) + assertEquals(2, it.tokens.size) assertEquals("description", it.tokens[0].description) } } @@ -332,9 +342,8 @@ class CoreBankAccountsApiTest { client.post("/accounts") { json(req) }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT) - client.post("/accounts") { + client.postAdmin("/accounts") { json(req) - pwAuth("admin") }.assertOk() } @@ -348,9 +357,8 @@ class CoreBankAccountsApiTest { client.post("/accounts") { json(req) }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT) - client.post("/accounts") { + client.postAdmin("/accounts") { json(req) - pwAuth("admin") }.assertOk() } @@ -367,16 +375,14 @@ class CoreBankAccountsApiTest { client.post("/accounts") { json(req) }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL) - client.post("/accounts") { + client.postAdmin("/accounts") { json(req) - pwAuth("admin") }.assertOk() } // Check tan info for (channel in listOf("sms", "email")) { - client.post("/accounts") { - pwAuth("admin") + client.postAdmin("/accounts") { json { "username" to "bat2" "password" to "password" @@ -418,9 +424,7 @@ class CoreBankAccountsApiTest { "username" to "bar" } }.assertConflict(TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE) - client.get("/accounts/bar") { - pwAuth("admin") - }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + client.getAdmin("/accounts/bar").assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) // Testing bad payto kind client.post("/accounts") { json(req) { @@ -510,8 +514,7 @@ class CoreBankAccountsApiTest { // Check ok repeat(100) { - client.post("/accounts") { - pwAuth("admin") + client.postAdmin("/accounts") { json(req) { "username" to "foo$it" } @@ -521,23 +524,19 @@ class CoreBankAccountsApiTest { assertBalance("admin", "-KUDOS:10000") // Check insufficient fund - client.post("/accounts") { - pwAuth("admin") + client.postAdmin("/accounts") { json(req) { "username" to "bar" } }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - client.get("/accounts/bar") { - pwAuth("admin") - }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + client.getAdmin("/accounts/bar").assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) } // Test admin-only account creation @Test fun createRestricted() = bankSetup(conf = "test_restrict.conf") { authRoutine(HttpMethod.Post, "/accounts", requireAdmin = true) - client.post("/accounts") { - pwAuth("admin") + client.postAdmin("/accounts") { json { "username" to "baz" "password" to "password-xyz" @@ -549,8 +548,7 @@ class CoreBankAccountsApiTest { // Test admin-only account creation @Test fun createTanErr() = bankSetup(conf = "test_tan_err.conf") { - client.post("/accounts") { - pwAuth("admin") + client.postAdmin("/accounts") { json { "username" to "baz" "password" to "xyz" @@ -588,9 +586,8 @@ class CoreBankAccountsApiTest { // Reserved account RESERVED_ACCOUNTS.forEach { - client.delete("/accounts/$it") { - pwAuth("admin") - }.assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) + client.deleteAdmin("/accounts/$it") + .assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) } client.deleteA("/accounts/exchange") .assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) @@ -615,15 +612,14 @@ class CoreBankAccountsApiTest { .assertNoContent() // Account no longer exists client.deleteA("/accounts/john").assertUnauthorized() - client.delete("/accounts/john") { - pwAuth("admin") - }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + client.deleteAdmin("/accounts/john") + .assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) } @Test fun softDelete() = bankSetup { db -> // Create all kind of operations - val token = client.postA("/accounts/customer/token") { + val token = client.postPw("/accounts/customer/token") { json { "scope" to "readonly" } }.assertOkJson<TokenSuccessResponse>().access_token val tx_id = client.postA("/accounts/customer/transactions") { @@ -667,23 +663,19 @@ class CoreBankAccountsApiTest { 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.getAdmin("/accounts/customer/transactions/$tx_id") + .assertOkJson<BankAccountTransactionInfo>() + client.getAdmin("/accounts/customer/cashouts/$cashout_id") + .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.getAdmin("/accounts/customer/transactions/$tx_id") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + client.getAdmin("/accounts/customer/cashouts/$cashout_id") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) client.get("/withdrawals/$withdrawal_id") .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } @@ -693,8 +685,7 @@ class CoreBankAccountsApiTest { fun deleteRestricted() = bankSetup(conf = "test_restrict.conf") { authRoutine(HttpMethod.Post, "/accounts", requireAdmin = true) // Exchange is still restricted - client.delete("/accounts/exchange") { - pwAuth("admin") + client.deleteAdmin("/accounts/exchange") { }.assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) } @@ -714,8 +705,7 @@ class CoreBankAccountsApiTest { json(req) }.assertConflict(error) // Check admin always can - client.patch("/accounts/merchant") { - pwAuth("admin") + client.patchAdmin("/accounts/merchant") { json(req) }.assertNoContent() // Check idempotent @@ -765,8 +755,7 @@ class CoreBankAccountsApiTest { ) // Check currency - client.patch("/accounts/merchant") { - pwAuth("admin") + client.patchAdmin("/accounts/merchant") { json(req) { "debit_threshold" to "EUR:100" } }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) @@ -824,8 +813,7 @@ class CoreBankAccountsApiTest { Triple(cashout.full("Full name"), null, cashout.full("Full name")), Triple(cashout.full("Full second name"), "Another name", cashout.full("Full second name")) )) { - client.patch("/accounts/cashout") { - pwAuth("admin") + client.patchAdmin("/accounts/cashout") { json { "cashout_payto_uri" to cashout if (name != null) "name" to name @@ -891,20 +879,22 @@ class CoreBankAccountsApiTest { authRoutine(HttpMethod.Patch, "/accounts/merchant/auth", allowAdmin = true) // Changing the password. - client.patch("/accounts/customer/auth") { - basicAuth("customer", "customer-password") + client.patchA("/accounts/customer/auth") { json { "old_password" to "customer-password" "new_password" to "new-password" } }.assertNoContent() // Previous password should fail. - client.patch("/accounts/customer/auth") { + client.post("/accounts/customer/token") { basicAuth("customer", "customer-password") }.assertUnauthorized() // New password should succeed. - client.patch("/accounts/customer/auth") { + client.post("/accounts/customer/token") { basicAuth("customer", "new-password") + json { "scope" to "readonly" } + }.assertOk() + client.patchA("/accounts/customer/auth") { json { "old_password" to "new-password" "new_password" to "customer-password" @@ -913,8 +903,7 @@ class CoreBankAccountsApiTest { // Check require test old password - client.patch("/accounts/customer/auth") { - basicAuth("customer", "customer-password") + client.patchA("/accounts/customer/auth") { json { "old_password" to "bad-password" "new_password" to "new-password" @@ -922,8 +911,7 @@ class CoreBankAccountsApiTest { }.assertConflict(TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD) // Check require old password for user - client.patch("/accounts/customer/auth") { - basicAuth("customer", "customer-password") + client.patchA("/accounts/customer/auth") { json { "new_password" to "new-password" } @@ -944,8 +932,7 @@ class CoreBankAccountsApiTest { }.assertConflict(TalerErrorCode.BANK_PASSWORD_TOO_LONG) // Check admin - client.patch("/accounts/customer/auth") { - pwAuth("admin") + client.patchAdmin("/accounts/customer/auth") { json { "new_password" to "customer-password" } @@ -959,8 +946,7 @@ class CoreBankAccountsApiTest { "new_password" to "it-password" } }.assertChallenge().assertNoContent() - client.patch("/accounts/customer/auth") { - pwAuth("admin") + client.patchAdmin("/accounts/customer/auth") { json { "new_password" to "new-password" } @@ -978,8 +964,7 @@ class CoreBankAccountsApiTest { } }.assertNoContent() // Testing long password - client.patch("/accounts/merchant/auth") { - basicAuth("merchant", "short") + client.patchA("/accounts/merchant/auth") { json { "old_password" to "short" "new_password" to "looooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password" @@ -994,13 +979,9 @@ class CoreBankAccountsApiTest { // Remove default accounts val defaultAccounts = listOf("merchant", "exchange", "customer") defaultAccounts.forEach { - client.delete("/accounts/$it") { - pwAuth("admin") - }.assertNoContent() + client.deleteAdmin("/accounts/$it").assertNoContent() } - client.get("/accounts") { - pwAuth("admin") - }.assertOkJson<ListBankAccountsResponse> { + client.getAdmin("/accounts").assertOkJson<ListBankAccountsResponse> { for (account in it.accounts) { if (defaultAccounts.contains(account.username)) { assertEquals(AccountStatus.deleted, account.status) @@ -1012,9 +993,7 @@ class CoreBankAccountsApiTest { 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") - }.assertOkJson<ListBankAccountsResponse>() + client.getAdmin("/accounts").assertOkJson<ListBankAccountsResponse>() // Gen some public and private accounts repeat(5) { @@ -1035,9 +1014,7 @@ class CoreBankAccountsApiTest { } } // All accounts - client.get("/accounts?limit=10"){ - pwAuth("admin") - }.assertOkJson<ListBankAccountsResponse> { + client.getAdmin("/accounts?limit=10").assertOkJson<ListBankAccountsResponse> { assertEquals(6, it.accounts.size) it.accounts.forEachIndexed { idx, it -> if (idx == 0) { @@ -1048,9 +1025,7 @@ class CoreBankAccountsApiTest { } } // Filtering - client.get("/accounts?filter_name=3"){ - pwAuth("admin") - }.assertOkJson<ListBankAccountsResponse> { + client.getAdmin("/accounts?filter_name=3").assertOkJson<ListBankAccountsResponse> { assertEquals(1, it.accounts.size) assertEquals("3", it.accounts[0].username) } @@ -1774,8 +1749,7 @@ class CoreBankCashoutApiTest { }.assertConflict(TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL) // Check custom min account - client.patch("/accounts/customer") { - pwAuth("admin") + client.patchAdmin("/accounts/customer") { json { "min_cashout" to "KUDOS:10" } @@ -1787,8 +1761,7 @@ class CoreBankCashoutApiTest { "amount_credit" to convert("KUDOS:5") } }.assertConflict(TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL) - client.patch("/accounts/customer") { - pwAuth("admin") + client.patchAdmin("/accounts/customer") { json { "min_cashout" to (null as String?) } @@ -1963,19 +1936,16 @@ class CoreBankTanApiTest { .assertNoContent() // Admin has no 2FA - client.patch("/accounts/merchant") { - pwAuth("admin") + client.patchAdmin("/accounts/merchant") { json { "contact_data" to obj { "phone" to "+99" } "tan_channel" to "sms" } }.assertNoContent() - client.patch("/accounts/merchant") { - pwAuth("admin") + client.patchAdmin("/accounts/merchant") { json { "tan_channel" to "email" } }.assertNoContent() - client.patch("/accounts/merchant") { - pwAuth("admin") + client.patchAdmin("/accounts/merchant") { json { "tan_channel" to null as String? } }.assertNoContent() @@ -2030,8 +2000,7 @@ class CoreBankTanApiTest { @Test fun sendTanErr() = bankSetup("test_tan_err.conf") { // Check fail - client.patch("/accounts/merchant") { - pwAuth("admin") + client.patchAdmin("/accounts/merchant") { json { "contact_data" to obj { "phone" to "+1234" } "tan_channel" to "sms" diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt @@ -29,6 +29,7 @@ import tech.libeufin.common.assertOk import tech.libeufin.common.db.one import tech.libeufin.common.db.oneOrNull import tech.libeufin.common.json +import tech.libeufin.common.test.* import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit @@ -91,9 +92,7 @@ class DatabaseTest { } // Complex read transaction: stored procedure launch { - client.getA("/monitor") { - pwAuth("admin") - }.assertOk() + client.getAdmin("/monitor").assertOk() } // GC logic launch { diff --git a/bank/src/test/kotlin/GcTest.kt b/bank/src/test/kotlin/GcTest.kt @@ -27,6 +27,7 @@ import tech.libeufin.bank.db.ExchangeDAO.TransferResult import tech.libeufin.bank.db.TransactionDAO.BankTransactionResult import tech.libeufin.bank.db.WithdrawalDAO.* import tech.libeufin.common.* +import tech.libeufin.common.test.* import tech.libeufin.common.db.one import java.time.Duration import java.time.Instant @@ -90,7 +91,7 @@ class GcTest { db.tan.new(account, Operation.cashout, "", "", time, 0, Duration.ZERO, null, null) } } - assertNbTokens(4) + assertNbTokens(5) assertNbTan(4) // Create test operations @@ -157,7 +158,7 @@ class GcTest { ) // Check hard delete assertNbAccount(5) - assertNbTokens(1) + assertNbTokens(3) assertNbTan(1) assertNbTx(24) assertNbCashout(3) diff --git a/bank/src/test/kotlin/PaytoTest.kt b/bank/src/test/kotlin/PaytoTest.kt @@ -23,6 +23,7 @@ import tech.libeufin.bank.BankAccountTransactionInfo import tech.libeufin.bank.RegisterAccountResponse import tech.libeufin.bank.TransactionCreateResponse import tech.libeufin.common.* +import tech.libeufin.common.test.* import kotlin.test.assertEquals class PaytoTest { diff --git a/bank/src/test/kotlin/RevenueApiTest.kt b/bank/src/test/kotlin/RevenueApiTest.kt @@ -21,6 +21,7 @@ import io.ktor.http.* import org.junit.Test import tech.libeufin.common.RevenueIncomingHistory import tech.libeufin.common.assertOk +import tech.libeufin.common.test.* class RevenueApiTest { // GET /accounts/{USERNAME}/taler-revenue/config diff --git a/bank/src/test/kotlin/SecurityTest.kt b/bank/src/test/kotlin/SecurityTest.kt @@ -23,6 +23,7 @@ import io.ktor.http.content.* import kotlinx.serialization.json.Json import org.junit.Test import tech.libeufin.common.* +import tech.libeufin.common.test.* inline fun <reified B> HttpRequestBuilder.jsonDeflate(b: B) { val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b) diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt @@ -28,6 +28,7 @@ import tech.libeufin.common.TalerAmount import tech.libeufin.common.assertOkJson import tech.libeufin.common.db.executeQueryCheck import tech.libeufin.common.micros +import tech.libeufin.common.test.* import java.time.Instant import java.time.LocalDateTime import java.time.ZoneOffset @@ -62,7 +63,7 @@ class StatsTest { fiatAmount: String? = null ) { Timeframe.entries.forEach { timeframe -> - client.get("/monitor?timestamp=${timeframe.name}") { pwAuth("admin") }.assertOkJson<MonitorResponse> { + client.getAdmin("/monitor?timestamp=${timeframe.name}").assertOkJson<MonitorResponse> { val resp = it as MonitorWithConversion assertEquals(count, dbCount(resp)) assertEquals(TalerAmount(regionalAmount), regionalVolume(resp)) diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -21,6 +21,7 @@ import io.ktor.http.* import io.ktor.server.testing.* import org.junit.Test import tech.libeufin.common.* +import tech.libeufin.common.test.* import kotlin.test.* class WireGatewayApiTest { diff --git a/bank/src/test/kotlin/bench.kt b/bank/src/test/kotlin/bench.kt @@ -141,50 +141,42 @@ class Bench { client.post("/accounts") { json { "username" to "account_bench_$it" - "password" to "password" + "password" to "account_bench_$it-password" "name" to "Bench Account $it" } }.assertOkJson<RegisterAccountResponse>().internal_payto_uri } measureAction("account_reconfig") { - client.patch("/accounts/account_bench_$it") { - basicAuth("account_bench_$it", "password") + client.patchA("/accounts/account_bench_$it") { json { "name" to "New Bench Account $it" } }.assertNoContent() } measureAction("account_reconfig_auth") { - client.patch("/accounts/account_bench_$it/auth") { - basicAuth("account_bench_$it", "password") + client.patchA("/accounts/account_bench_$it/auth") { json { - "old_password" to "password" - "new_password" to "password" + "old_password" to "account_bench_$it-password" + "new_password" to "account_bench_$it-password" } }.assertNoContent() } measureAction("account_list") { - client.get("/accounts") { - basicAuth("admin", "admin-password") - }.assertOk() + client.getAdmin("/accounts").assertOk() } measureAction("account_list_public") { client.get("/public-accounts").assertOk() } measureAction("account_get") { - client.get("/accounts/account_bench_$it") { - basicAuth("account_bench_$it", "password") - }.assertOk() + client.getA("/accounts/account_bench_$it").assertOk() } measureAction("account_delete") { - client.delete("/accounts/account_bench_$it") { - basicAuth("account_bench_$it", "password") - }.assertNoContent() + client.deleteA("/accounts/account_bench_$it").assertNoContent() } // Tokens val tokens = measureAction("token_create") { - client.postA("/accounts/customer/token") { + client.postPw("/accounts/customer/token") { json { "scope" to "readonly" "refreshable" to true @@ -279,9 +271,7 @@ class Bench { client.getA("/accounts/customer/cashouts").assertOk() } measureAction("cashout_history_admin") { - client.get("/cashouts") { - pwAuth("admin") - }.assertOk() + client.getAdmin("/cashouts").assertOk() } // Wire gateway @@ -347,9 +337,7 @@ class Bench { // Other measureAction("monitor") { - client.get("/monitor") { - pwAuth("admin") - }.assertOk() + client.getAdmin("/monitor").assertOk() } db.gc.collect(Instant.now(), java.time.Duration.ZERO, java.time.Duration.ZERO, java.time.Duration.ZERO) measureAction("gc") { diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -27,6 +27,7 @@ import tech.libeufin.bank.* import tech.libeufin.bank.db.AccountDAO.AccountCreationResult import tech.libeufin.bank.db.Database import tech.libeufin.common.* +import tech.libeufin.common.test.* import tech.libeufin.common.db.dbInit import tech.libeufin.common.db.pgDataSource import java.nio.file.NoSuchFileException @@ -60,6 +61,7 @@ fun setup( conf: String = "test.conf", lambda: suspend (Database, BankConfig) -> Unit ) = runBlocking { + globalTestTokens.clear() val cfg = bankConfig(Path("conf/$conf")) pgDataSource(cfg.dbCfg.dbConnStr).run { dbInit(cfg.dbCfg, "libeufin-nexus", true) @@ -142,8 +144,7 @@ fun bankSetup( } if (cfg.allowConversion) { // Set conversion rates - client.post("/conversion-info/conversion-rate") { - pwAuth("admin") + client.postAdmin("/conversion-info/conversion-rate") { json { "cashin_ratio" to "0.8" "cashin_fee" to "KUDOS:0.02" @@ -169,17 +170,14 @@ fun dbSetup(lambda: suspend (Database) -> Unit) = /** Set [account] debit threshold to [maxDebt] amount */ suspend fun ApplicationTestBuilder.setMaxDebt(account: String, maxDebt: String) { - client.patch("/accounts/$account") { - pwAuth("admin") + client.patchAdmin("/accounts/$account") { json { "debit_threshold" to maxDebt } }.assertNoContent() } /** Check [account] balance is [amount], [amount] is prefixed with + for credit and - for debit */ suspend fun ApplicationTestBuilder.assertBalance(account: String, amount: String) { - client.get("/accounts/$account") { - pwAuth("admin") - }.assertOkJson<AccountData> { + client.getAdmin("/accounts/$account").assertOkJson<AccountData> { val balance = it.balance val fmt = "${if (balance.credit_debit_indicator == CreditDebitInfo.debit) '-' else '+'}${balance.amount}" assertEquals(amount, fmt, "For $account") @@ -221,8 +219,7 @@ suspend fun ApplicationTestBuilder.transfer(amount: String, payto: IbanPayto = m /** Perform a taler incoming transaction of [amount] from merchant to exchange */ suspend fun ApplicationTestBuilder.addIncoming(amount: String) { - client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { - pwAuth("admin") + client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { json { "amount" to TalerAmount(amount) "reserve_pub" to EddsaPublicKey.rand() @@ -233,8 +230,7 @@ suspend fun ApplicationTestBuilder.addIncoming(amount: String) { /** Perform a taler kyc transaction of [amount] from merchant to exchange */ suspend fun ApplicationTestBuilder.addKyc(amount: String) { - client.post("/accounts/exchange/taler-wire-gateway/admin/add-kycauth") { - pwAuth("admin") + client.postA("/accounts/exchange/taler-wire-gateway/admin/add-kycauth") { json { "amount" to TalerAmount(amount) "account_pub" to EddsaPublicKey.rand() @@ -280,8 +276,7 @@ suspend fun ApplicationTestBuilder.withdrawal(amount: String) { } suspend fun ApplicationTestBuilder.fillCashoutInfo(account: String) { - client.patch("/accounts/$account") { - pwAuth("admin") + client.patchAdmin("/accounts/$account") { json { "cashout_payto_uri" to unknownPayto "contact_data" to obj { @@ -292,8 +287,7 @@ suspend fun ApplicationTestBuilder.fillCashoutInfo(account: String) { } suspend fun ApplicationTestBuilder.fillTanInfo(account: String) { - client.patch("/accounts/$account") { - pwAuth("admin") + client.patchAdmin("/accounts/$account") { json { "contact_data" to obj { "phone" to "+${Random.nextInt(0, 10000)}" @@ -353,7 +347,7 @@ suspend fun HttpResponse.assertChallenge( json { "tan" to code } }.assertNoContent() return call.client.request(this.call.request.url) { - pwAuth(username) + tokenAuth(call.client, username) method = call.request.method headers[X_CHALLENGE_ID] = "$id" } @@ -368,52 +362,6 @@ fun assertException(msg: String, lambda: () -> Unit) { } } -/* ----- Auth ----- */ - -/** Auto auth get request */ -suspend inline fun HttpClient.getA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse { - return get(url) { - pwAuth() - builder(this) - } -} - -/** Auto auth post request */ -suspend inline fun HttpClient.postA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse { - return post(url) { - pwAuth() - builder(this) - } -} - -/** Auto auth patch request */ -suspend inline fun HttpClient.patchA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse { - return patch(url) { - pwAuth() - builder(this) - } -} - -/** Auto auth delete request */ -suspend inline fun HttpClient.deleteA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse { - return delete(url) { - pwAuth() - builder(this) - } -} - -fun HttpRequestBuilder.pwAuth(username: String? = null) { - if (username != null) { - basicAuth("$username", "$username-password") - } else if (url.pathSegments.contains("admin")) { - basicAuth("admin", "admin-password") - } else if (url.pathSegments[1] == "accounts") { - // Extract username from path - val username = url.pathSegments[2] - basicAuth(username, "$username-password") - } -} - /* ----- Random data generation ----- */ fun randBase32Crockford(length: Int) = Base32Crockford.encode(ByteArray(length).rand()) diff --git a/bank/src/test/kotlin/routines.kt b/bank/src/test/kotlin/routines.kt @@ -27,8 +27,7 @@ import kotlinx.serialization.json.JsonObject import tech.libeufin.bank.BankAccountCreateWithdrawalResponse import tech.libeufin.bank.WithdrawalStatus import tech.libeufin.common.* -import tech.libeufin.common.test.abstractHistoryRoutine -import tech.libeufin.common.test.assertTime +import tech.libeufin.common.test.* import kotlin.test.assertEquals // Test endpoint is correctly authenticated @@ -52,36 +51,18 @@ suspend fun ApplicationTestBuilder.authRoutine( this.method = method headers[HttpHeaders.Authorization] = "WTF" }.assertBadRequest(TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED) - - // Unknown account - client.request(path) { - this.method = method - basicAuth("unknown", "password") - }.assertUnauthorized() - - // Wrong password - client.request(path) { - this.method = method - basicAuth("merchant", "wrong-password") - }.assertUnauthorized() - - // Wrong account - client.request(path) { - this.method = method - basicAuth("exchange", "merchant-password") - }.assertUnauthorized() if (requireAdmin) { // Not exchange account client.request(path) { this.method = method - pwAuth("merchant") + tokenAuth(client, "merchant") }.assertUnauthorized() } else if (!allowAdmin) { // Check no admin client.request(path) { this.method = method - pwAuth("admin") + tokenAuth(client, "admin") }.assertUnauthorized() } @@ -90,7 +71,7 @@ suspend fun ApplicationTestBuilder.authRoutine( client.request(path) { this.method = method if (body != null) json(body) - pwAuth(if (requireAdmin) "admin" else "merchant") + tokenAuth(client, if (requireAdmin) "admin" else "merchant") }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) } } @@ -104,8 +85,8 @@ suspend inline fun <reified B> ApplicationTestBuilder.historyRoutine( auth: String? = null ) { abstractHistoryRoutine(ids, registered, ignored, polling) { params: String -> - client.get("$url?$params") { - pwAuth(auth) + client.getA("$url?$params") { + tokenAuth(client, auth) } } } diff --git a/common/src/main/kotlin/test/helpers.kt b/common/src/main/kotlin/test/helpers.kt @@ -19,11 +19,14 @@ package tech.libeufin.common.test +import io.ktor.client.* +import io.ktor.client.request.* import io.ktor.client.statement.* -import tech.libeufin.common.PageParams -import tech.libeufin.common.assertOk -import tech.libeufin.common.json +import io.ktor.http.* +import io.ktor.server.testing.* +import tech.libeufin.common.* import kotlin.test.assertEquals +import kotlinx.serialization.json.* /* ----- Assert ----- */ @@ -59,4 +62,106 @@ suspend inline fun <reified B> HttpResponse.assertHistoryIds(size: Int, ids: (B) } return body +} + +/* ----- Auth ----- */ + +typealias RequestLambda = suspend HttpRequestBuilder.() -> Unit; + +/** Auto token auth GET request */ +suspend fun HttpClient.getA(url: String, builder: RequestLambda = {}): HttpResponse + = tokenAuthRequest(url, HttpMethod.Get, null, builder) +/** Auto token auth POST request */ +suspend fun HttpClient.postA(url: String, builder: RequestLambda = {}): HttpResponse + = tokenAuthRequest(url, HttpMethod.Post, null, builder) +/** Auto token auth PATCH request */ +suspend fun HttpClient.patchA(url: String, builder: RequestLambda = {}): HttpResponse + = tokenAuthRequest(url, HttpMethod.Patch, null, builder) +/** Auto token auth DELETE request */ +suspend fun HttpClient.deleteA(url: String, builder: RequestLambda = {}): HttpResponse + = tokenAuthRequest(url, HttpMethod.Delete, null, builder) + +/** Admin token auth GET request */ +suspend fun HttpClient.getAdmin(url: String, builder: RequestLambda = {}): HttpResponse + = tokenAuthRequest(url, HttpMethod.Get, "admin", builder) +/** Admin token auth PATCH request */ +suspend fun HttpClient.patchAdmin(url: String, builder: RequestLambda = {}): HttpResponse + = tokenAuthRequest(url, HttpMethod.Patch, "admin", builder) +/** Admin token auth POST request */ +suspend fun HttpClient.postAdmin(url: String, builder: RequestLambda = {}): HttpResponse + = tokenAuthRequest(url, HttpMethod.Post, "admin", builder) +/** Admin token auth DELETE request */ +suspend fun HttpClient.deleteAdmin(url: String, builder: RequestLambda = {}): HttpResponse + = tokenAuthRequest(url, HttpMethod.Delete, "admin", builder) + +/** Auto pw auth GET request */ +suspend fun HttpClient.getPw(url: String, builder: RequestLambda = {}): HttpResponse + = pwAuthRequest(url, HttpMethod.Get, null, builder) +/** Auto pw auth POST request */ +suspend fun HttpClient.postPw(url: String, builder: RequestLambda = {}): HttpResponse + = pwAuthRequest(url, HttpMethod.Post, null, builder) +/** Auto pw auth PATCH request */ +suspend fun HttpClient.patchPw(url: String, builder: RequestLambda = {}): HttpResponse + = pwAuthRequest(url, HttpMethod.Patch, null, builder) +/** Auto pw auth DELETE request */ +suspend fun HttpClient.deletePw(url: String, builder: RequestLambda = {}): HttpResponse + = pwAuthRequest(url, HttpMethod.Delete, null, builder) + +private suspend fun HttpClient.tokenAuthRequest( + url: String, + method: HttpMethod, + username: String?, + builder: RequestLambda = {} +): HttpResponse = request(url) { + this.method = method + tokenAuth(this@tokenAuthRequest, username) + builder(this) +} + +private suspend fun HttpClient.pwAuthRequest( + url: String, + method: HttpMethod, + username: String?, + builder: RequestLambda = {} +): HttpResponse = request(url) { + this.method = method + pwAuth(username) + builder(this) +} + +private fun HttpRequestBuilder.extractUsername(username: String? = null): String? + = when { + username != null -> username + url.pathSegments.contains("admin") -> "admin" + url.pathSegments[1] == "accounts" -> url.pathSegments[2] + else -> null + } + +fun HttpRequestBuilder.pwAuth(username: String? = null) { + val username = extractUsername(username) ?: return + basicAuth("$username", "$username-password") +} + +val globalTestTokens = mutableMapOf<String, String>() + +suspend fun HttpRequestBuilder.tokenAuth(client: HttpClient, username: String? = null) { + // Get username from arg or path + val username = extractUsername(username) ?: return + // Get cached token or create it + var token = globalTestTokens.get(username) + if (token == null) { + val response = client.post("/accounts/$username/token") { + pwAuth() + json { + "scope" to "readwrite" + "duration" to obj { + "d_us" to "forever" + } + } + }.assertOkJson<JsonObject>() + token = Json.decodeFromJsonElement<String>(response.get("access_token")!!) + globalTestTokens.set(username, token) + } + // Set authorization header + headers[HttpHeaders.Authorization] = "Bearer $token" } \ No newline at end of file diff --git a/contrib/bank.conf b/contrib/bank.conf @@ -95,6 +95,10 @@ PWD_HASH_CONFIG = { "cost": 8 } # Unstable flag, will become a non configurable default in a future version PWD_CHECK = yes +# Whether to allow password auth everywhere +# Unstable flag, will become a non configurable default in a future version +PWD_AUTH_COMPAT = no + # Time after which pending operations are aborted during garbage collection GC_ABORT_AFTER = 15m diff --git a/testbench/src/test/kotlin/IntegrationTest.kt b/testbench/src/test/kotlin/IntegrationTest.kt @@ -33,6 +33,7 @@ import tech.libeufin.bank.ConversionResponse import tech.libeufin.bank.RegisterAccountResponse import tech.libeufin.bank.cli.LibeufinBank import tech.libeufin.common.* +import tech.libeufin.common.test.* import tech.libeufin.common.api.engine import tech.libeufin.common.db.one import tech.libeufin.nexus.* @@ -59,6 +60,7 @@ fun HttpResponse.assertNoContent() { } fun server(lambda: () -> Unit) { + globalTestTokens.clear() // Start the HTTP server in another thread kotlin.concurrent.thread(isDaemon = true) { lambda() @@ -99,13 +101,17 @@ inline fun assertException(msg: String, lambda: () -> Unit) { class IntegrationTest { val nexusCmd = LibeufinNexus() val bankCmd = LibeufinBank() - val client = HttpClient(CIO) + val client = HttpClient(CIO) { + defaultRequest { + url("http://0.0.0.0:8080/") + } + } @Test fun mini() { val flags = "-c conf/mini.conf -L DEBUG" bankCmd.run("dbinit $flags -r") - bankCmd.run("passwd admin password $flags") + bankCmd.run("passwd admin admin-password $flags") bankCmd.run("dbinit $flags") // Idempotent server { @@ -114,7 +120,7 @@ class IntegrationTest { setup("conf/mini.conf") { // Check bank is running - client.get("http://0.0.0.0:8080/public-accounts").assertNoContent() + client.get("/public-accounts").assertNoContent() } bankCmd.run("gc $flags") @@ -130,7 +136,7 @@ class IntegrationTest { val flags = "-c conf/integration.conf -L DEBUG" nexusCmd.run("dbinit $flags -r") bankCmd.run("dbinit $flags -r") - bankCmd.run("passwd admin password $flags") + bankCmd.run("passwd admin admin-password $flags") suspend fun NexusDb.checkCount(nbIncoming: Int, nbBounce: Int, nbTalerable: Int) { serializable( @@ -183,7 +189,7 @@ class IntegrationTest { ) // Create exchange account - bankCmd.run("create-account $flags -u exchange -p password --name 'Mr Money' --exchange") + bankCmd.run("create-account $flags -u exchange -p exchange-password --name 'Mr Money' --exchange") assertException("ERROR: cashin currency conversion failed: missing conversion rates") { registerIncomingPayment(db, cfg, reservePayment) @@ -195,8 +201,7 @@ class IntegrationTest { } // Set conversion rates - client.post("http://0.0.0.0:8080/conversion-info/conversion-rate") { - basicAuth("admin", "password") + client.postAdmin("/conversion-info/conversion-rate") { json { "cashin_ratio" to "0.8" "cashin_fee" to "KUDOS:0.02" @@ -224,9 +229,7 @@ class IntegrationTest { amount = TalerAmount("EUR:0.01"), )) db.checkCount(2, 1, 1) - client.get("http://0.0.0.0:8080/accounts/exchange/transactions") { - basicAuth("exchange", "password") - }.assertNoContent() + client.getA("/accounts/exchange/transactions").assertNoContent() // Check success val validPayment = reservePayment.copy( @@ -235,9 +238,8 @@ class IntegrationTest { ) registerIncomingPayment(db, cfg, validPayment) db.checkCount(3, 1, 2) - client.get("http://0.0.0.0:8080/accounts/exchange/transactions") { - basicAuth("exchange", "password") - }.assertOkJson<BankAccountTransactionsResponse>() + client.getA("/accounts/exchange/transactions") + .assertOkJson<BankAccountTransactionsResponse>() // Check idempotency registerIncomingPayment(db, cfg, validPayment) @@ -253,9 +255,9 @@ class IntegrationTest { val flags = "-c conf/integration.conf -L DEBUG" nexusCmd.run("dbinit $flags -r") bankCmd.run("dbinit $flags -r") - bankCmd.run("passwd admin password $flags") + bankCmd.run("passwd admin admin-password $flags") bankCmd.run("edit-account admin --debit_threshold KUDOS:1000 $flags") - bankCmd.run("create-account $flags -u exchange -p password --name 'Mr Money' --exchange") + bankCmd.run("create-account $flags -u exchange -p exchange-password --name 'Mr Money' --exchange") nexusCmd.run("dbinit $flags") // Idempotent bankCmd.run("dbinit $flags") // Idempotent @@ -268,11 +270,10 @@ class IntegrationTest { val fiatPayTo = IbanPayto.rand() // Create user - client.post("http://0.0.0.0:8080/accounts") { - basicAuth("admin", "password") + client.postAdmin("/accounts") { json { "username" to "customer" - "password" to "password" + "password" to "customer-password" "name" to "JohnSmith" "internal_payto_uri" to userPayTo "cashout_payto_uri" to fiatPayTo @@ -284,8 +285,7 @@ class IntegrationTest { }.assertOkJson<RegisterAccountResponse>() // Set conversion rates - client.post("http://0.0.0.0:8080/conversion-info/conversion-rate") { - basicAuth("admin", "password") + client.postAdmin("/conversion-info/conversion-rate") { json { "cashin_ratio" to "0.8" "cashin_fee" to "KUDOS:0.02" @@ -306,18 +306,14 @@ class IntegrationTest { val amount = TalerAmount("EUR:${20+i}") val subject = "cashin test $i: $reservePub" nexusCmd.run("testing fake-incoming $flags --subject \"$subject\" --amount $amount $userPayTo") - val converted = client.get("http://0.0.0.0:8080/conversion-info/cashin-rate?amount_debit=EUR:${20 + i}") + val converted = client.get("/conversion-info/cashin-rate?amount_debit=EUR:${20 + i}") .assertOkJson<ConversionResponse>().amount_credit - client.get("http://0.0.0.0:8080/accounts/exchange/transactions") { - basicAuth("exchange", "password") - }.assertOkJson<BankAccountTransactionsResponse> { + client.getA("/accounts/exchange/transactions").assertOkJson<BankAccountTransactionsResponse> { val tx = it.transactions.first() assertEquals(subject, tx.subject) assertEquals(converted, tx.amount) } - client.get("http://0.0.0.0:8080/accounts/exchange/taler-wire-gateway/history/incoming") { - basicAuth("exchange", "password") - }.assertOkJson<IncomingHistory> { + client.getA("/accounts/exchange/taler-wire-gateway/history/incoming").assertOkJson<IncomingHistory> { val tx = it.incoming_transactions.first() assertEquals(converted, tx.amount) assertIs<IncomingReserveTransaction>(tx) @@ -329,10 +325,9 @@ class IntegrationTest { repeat(3) { i -> val requestUid = ShortHashCode.rand() val amount = TalerAmount("KUDOS:${10+i}") - val convert = client.get("http://0.0.0.0:8080/conversion-info/cashout-rate?amount_debit=$amount") + val convert = client.get("/conversion-info/cashout-rate?amount_debit=$amount") .assertOkJson<ConversionResponse>().amount_credit - client.post("http://0.0.0.0:8080/accounts/customer/cashouts") { - basicAuth("customer", "password") + client.postA("/accounts/customer/cashouts") { json { "request_uid" to requestUid "amount_debit" to amount