commit 92d10fc076e807cfbbf40a7f846e0d3c1764c545
parent 01eafe1f9d53f7d9150dfa8ea69afc92eb1afa3d
Author: Antoine A <>
Date: Fri, 5 Jan 2024 18:12:16 +0000
Bring back the 4 call design
Diffstat:
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"
}
}