libeufin

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

commit 92d10fc076e807cfbbf40a7f846e0d3c1764c545
parent 01eafe1f9d53f7d9150dfa8ea69afc92eb1afa3d
Author: Antoine A <>
Date:   Fri,  5 Jan 2024 18:12:16 +0000

Bring back the 4 call design

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 48+++++++++++++-----------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/Tan.kt | 27+++++++++++++++++++++++++++
Mbank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt | 30++++++++++++++++++++++++++++++
Mbank/src/test/kotlin/helpers.kt | 13+++++++++----
4 files changed, 79 insertions(+), 39 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -338,17 +338,18 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { requireAdmin = !ctx.allowAccountDeletion ) { delete("/accounts/{USERNAME}") { - call.deleteAccountHttp(db, ctx, false) + val challenge = call.challenge(db, Operation.account_delete) + call.deleteAccountHttp(db, ctx, challenge != null) } } auth(db, TokenScope.readwrite, allowAdmin = true) { patch("/accounts/{USERNAME}") { - val req = call.receive<AccountReconfiguration>() - call.patchAccountHttp(db, ctx, req, false) + val (req, challenge) = call.receiveChallenge<AccountReconfiguration>(db, Operation.account_reconfig) + call.patchAccountHttp(db, ctx, req, challenge != null, challenge?.channel, challenge?.info) } patch("/accounts/{USERNAME}/auth") { - val req = call.receive<AccountPasswordChange>() - call.reconfigAuthHttp(db, ctx, req, false) + val (req, challenge) = call.receiveChallenge<AccountPasswordChange>(db, Operation.account_auth_reconfig) + call.reconfigAuthHttp(db, ctx, req, challenge != null) } } get("/public-accounts") { @@ -441,8 +442,8 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { } auth(db, TokenScope.readwrite) { post("/accounts/{USERNAME}/transactions") { - val req = call.receive<TransactionCreateRequest>() - call.bankTransactionHttp(db, ctx, req, false) + val (req, challenge) = call.receiveChallenge<TransactionCreateRequest>(db, Operation.bank_transaction) + call.bankTransactionHttp(db, ctx, req, challenge != null) } } } @@ -521,7 +522,8 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) { } post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") { val opId = call.uuidUriComponent("withdrawal_id") - call.confirmWithdrawalHttp(db, ctx, opId, false) + val challenge = call.challenge(db, Operation.withdrawal) + call.confirmWithdrawalHttp(db, ctx, opId, challenge != null) } } get("/withdrawals/{withdrawal_id}") { @@ -598,8 +600,8 @@ suspend fun ApplicationCall.cashoutHttp(db: Database, ctx: BankConfig, req: Cash private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) = conditional(ctx.allowConversion) { auth(db, TokenScope.readwrite) { post("/accounts/{USERNAME}/cashouts") { - val req = call.receive<CashoutRequest>() - call.cashoutHttp(db, ctx, req, false) + val (req, challenge) = call.receiveChallenge<CashoutRequest>(db, Operation.cashout) + call.cashoutHttp(db, ctx, req, challenge != null) } } auth(db, TokenScope.readonly) { @@ -708,31 +710,7 @@ private fun Routing.coreBankTanApi(db: Database, ctx: BankConfig) { "Challenge expired", TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED ) - is TanSolveResult.Success -> when (res.op) { - Operation.account_reconfig -> { - val tmp = Json.decodeFromString<AccountReconfiguration>(res.body); - call.patchAccountHttp(db, ctx, tmp, true, res.channel, res.info) - } - Operation.account_auth_reconfig -> { - val tmp = Json.decodeFromString<AccountPasswordChange>(res.body) - call.reconfigAuthHttp(db, ctx, tmp, true) - } - Operation.account_delete -> { - call.deleteAccountHttp(db, ctx, true) - } - Operation.bank_transaction -> { - val tmp = Json.decodeFromString<TransactionCreateRequest>(res.body) - call.bankTransactionHttp(db, ctx, tmp, true) - } - Operation.cashout -> { - val tmp = Json.decodeFromString<CashoutRequest>(res.body) - call.cashoutHttp(db, ctx, tmp, true) - } - Operation.withdrawal -> { - val tmp = Json.decodeFromString<StoredUUID>(res.body) - call.confirmWithdrawalHttp(db, ctx, tmp.value, true) - } - } + is TanSolveResult.Success -> call.respond(HttpStatusCode.NoContent) } } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Tan.kt b/bank/src/main/kotlin/tech/libeufin/bank/Tan.kt @@ -27,6 +27,8 @@ import io.ktor.http.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.application.* +import tech.libeufin.bank.TanDAO.Challenge +import io.ktor.util.pipeline.PipelineContext inline suspend fun <reified B> ApplicationCall.respondChallenge( @@ -55,6 +57,31 @@ inline suspend fun <reified B> ApplicationCall.respondChallenge( ) } +inline suspend fun <reified B> ApplicationCall.receiveChallenge( + db: Database, + op: Operation +): Pair<B, Challenge?> { + val id = request.headers["X-Challenge-Id"]?.toLongOrNull() + return if (id != null) { + val challenge = db.tan.challenge(id, username, op)!! + Pair(Json.decodeFromString(challenge.body), challenge) + } else { + Pair(this.receive(), null) + } +} + +suspend fun ApplicationCall.challenge( + db: Database, + op: Operation +): Challenge? { + val id = request.headers["X-Challenge-Id"]?.toLongOrNull() + return if (id != null) { + db.tan.challenge(id, username, op)!! + } else { + null + } +} + object Tan { private val CODE_FORMAT = DecimalFormat("00000000"); private val SECURE_RNG = SecureRandom() diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt @@ -143,4 +143,34 @@ class TanDAO(private val db: Database) { } } } + + data class Challenge ( + val body: String, + val channel: TanChannel?, + val info: String? + ) + + /** Get a solved TAN challenge [id] for account [login] and [op] */ + suspend fun challenge( + id: Long, + login: String, + op: Operation + ) = db.serializable { conn -> + val stmt = conn.prepareStatement(""" + SELECT body, tan_challenges.tan_channel, tan_info + FROM tan_challenges + JOIN customers ON customer=customer_id + WHERE challenge_id=? AND op=?::op_enum AND login=? + """) + stmt.setLong(1, id) + stmt.setString(2, op.name) + stmt.setString(3, login) + stmt.oneOrNull { + Challenge( + body = it.getString("body"), + channel = it.getString("tan_channel")?.run { TanChannel.valueOf(this) }, + info = it.getString("tan_info") + ) + } + } } \ No newline at end of file diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -345,14 +345,19 @@ suspend fun HttpResponse.maybeChallenge(): HttpResponse { suspend fun HttpResponse.assertChallenge( check: suspend (TanChannel, String) -> Unit = { _, _ -> } ): HttpResponse { - val id = this.assertAcceptedJson<TanChallenge>().challenge_id - val username = this.call.request.url.pathSegments[2] - val res = this.call.client.postA("/accounts/$username/challenge/$id").assertOkJson<TanTransmission>() + val id = assertAcceptedJson<TanChallenge>().challenge_id + val username = call.request.url.pathSegments[2] + val res = call.client.postA("/accounts/$username/challenge/$id").assertOkJson<TanTransmission>() check(res.tan_channel, res.tan_info) val code = tanCode(res.tan_info) assertNotNull(code) - return this.call.client.postA("/accounts/$username/challenge/$id/confirm") { + call.client.postA("/accounts/$username/challenge/$id/confirm") { json { "tan" to code } + }.assertNoContent() + return call.client.request(this.call.request.url) { + pwAuth(username) + method = call.request.method + headers["X-Challenge-Id"] = "$id" } }