libeufin

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

commit 004fc57de2e604ad29816d79bd4f593b2c26090a
parent d66b43e756f2b9f39e720b466fa34b25d0a7d522
Author: Antoine A <>
Date:   Wed, 10 Jan 2024 11:23:34 +0000

Allow account creation with 2FA by admin

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 24++++++++++++++++++------
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 1+
Mbank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt | 44+++++++++++++++++++++++++-------------------
Mbank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 6+++++-
Mbank/src/test/kotlin/CoreBankApiTest.kt | 32+++++++++++++++++++++++++++++++-
Mbank/src/test/kotlin/helpers.kt | 18+++++++++++++++---
Mutil/src/main/kotlin/TalerErrorCode.kt | 8++++++++
7 files changed, 103 insertions(+), 30 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -150,12 +150,23 @@ suspend fun createAccount(db: Database, ctx: BankConfig, req: RegisterAccountReq TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT ) - if (req.debit_threshold != null && !isAdmin) - throw conflict( - "only admin account can choose the debit limit", - TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT - ) - + if (!isAdmin) { + if (req.debit_threshold != null) + throw conflict( + "only admin account can choose the debit limit", + TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT + ) + if (req.tan_channel != null) + throw conflict( + "only admin account can enable 2fa on creation", + TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL + ) + } else { + if (req.tan_channel != null && ctx.tanChannels.get(req.tan_channel) == null) { + throw unsupportedTanChannel(req.tan_channel) + } + } + if (req.username == "exchange" && !req.is_taler_exchange) throw conflict( "'exchange' account must be a taler exchange account", @@ -181,6 +192,7 @@ suspend fun createAccount(db: Database, ctx: BankConfig, req: RegisterAccountReq maxDebt = req.debit_threshold ?: ctx.defaultDebtLimit, bonus = if (!req.is_taler_exchange) ctx.registrationBonus else TalerAmount(0, 0, ctx.regionalCurrency), + tanChannel = req.tan_channel, checkPaytoIdempotent = req.internal_payto_uri != null ) // Retry with new IBAN diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -176,6 +176,7 @@ data class RegisterAccountRequest( val cashout_payto_uri: IbanPayTo? = null, val payto_uri: IbanPayTo? = null, val debit_threshold: TalerAmount? = null, + val tan_channel: TanChannel? = null, // TODO remove val internal_payto_uri: IbanPayTo? = null, val challenge_contact_data: ChallengeContactData? = null, diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -38,14 +38,15 @@ class AccountDAO(private val db: Database) { login: String, password: String, name: String, - email: String? = null, - phone: String? = null, - cashoutPayto: IbanPayTo? = null, + email: String?, + phone: String?, + cashoutPayto: IbanPayTo?, internalPaytoUri: IbanPayTo, isPublic: Boolean, isTalerExchange: Boolean, maxDebt: TalerAmount, bonus: TalerAmount, + tanChannel: TanChannel?, // Whether to check [internalPaytoUri] for idempotency checkPaytoIdempotent: Boolean ): AccountCreationResult = db.serializable { it -> @@ -59,11 +60,13 @@ class AccountDAO(private val db: Database) { AND (NOT ? OR internal_payto_uri=?) AND is_public=? AND is_taler_exchange=? + AND tan_channel IS NOT DISTINCT FROM ?::tan_enum FROM customers JOIN bank_accounts ON customer_id=owning_customer_id WHERE login=? """).run { + // TODO check max debt setString(1, name) setString(2, email) setString(3, phone) @@ -72,11 +75,13 @@ class AccountDAO(private val db: Database) { setString(6, internalPaytoUri.canonical) setBoolean(7, isPublic) setBoolean(8, isTalerExchange) - setString(9, login) + setString(9, tanChannel?.name) + setString(10, login) oneOrNull { CryptoUtil.checkpw(password, it.getString(1)) && it.getBoolean(2) } } + println(idempotent) if (idempotent != null) { if (idempotent) { AccountCreationResult.Success @@ -84,6 +89,20 @@ class AccountDAO(private val db: Database) { AccountCreationResult.LoginReuse } } else { + conn.prepareStatement(""" + INSERT INTO iban_history( + iban + ,creation_time + ) VALUES (?, ?) + """).run { + setString(1, internalPaytoUri.iban) + setLong(2, now) + if (!executeUpdateViolation()) { + conn.rollback() + return@transaction AccountCreationResult.PayToReuse + } + } + val customerId = conn.prepareStatement(""" INSERT INTO customers ( login @@ -93,7 +112,7 @@ class AccountDAO(private val db: Database) { ,phone ,cashout_payto ,tan_channel - ) VALUES (?, ?, ?, ?, ?, ?, NULL) + ) VALUES (?, ?, ?, ?, ?, ?, ?::tan_enum) RETURNING customer_id """ ).run { @@ -103,22 +122,9 @@ class AccountDAO(private val db: Database) { setString(4, email) setString(5, phone) setString(6, cashoutPayto?.canonical) + setString(7, tanChannel?.name) oneOrNull { it.getLong("customer_id") }!! } - - conn.prepareStatement(""" - INSERT INTO iban_history( - iban - ,creation_time - ) VALUES (?, ?) - """).run { - setString(1, internalPaytoUri.iban) - setLong(2, now) - if (!executeUpdateViolation()) { - conn.rollback() - return@transaction AccountCreationResult.PayToReuse - } - } conn.prepareStatement(""" INSERT INTO bank_accounts( diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -133,7 +133,11 @@ suspend fun maybeCreateAdminAccount(db: Database, ctx: BankConfig, pw: String? = isTalerExchange = false, maxDebt = ctx.defaultDebtLimit, bonus = TalerAmount(0, 0, ctx.regionalCurrency), - checkPaytoIdempotent = false + checkPaytoIdempotent = false, + email = null, + phone = null, + cashoutPayto = null, + tanChannel = null ) } diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -214,7 +214,7 @@ class CoreBankAccountsApiTest { json(req) }.assertOk() - // Check debit_threshold + // Check admin only debit_threshold obj { "username" to "bat" "password" to "password" @@ -230,6 +230,22 @@ class CoreBankAccountsApiTest { }.assertOk() } + // Check admin only tan_channel + obj { + "username" to "bat2" + "password" to "password" + "name" to "Bat" + "tan_channel" to "sms" + }.let { req -> + client.post("/accounts") { + json(req) + }.assertErr(TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL) + client.post("/accounts") { + json(req) + pwAuth("admin") + }.assertOk() + } + // Reserved account RESERVED_ACCOUNTS.forEach { client.post("/accounts") { @@ -316,6 +332,20 @@ class CoreBankAccountsApiTest { }.assertOk() } + // Test admin-only account creation + @Test + fun createTanErr() = bankSetup(conf = "test_tan_err.conf") { _ -> + client.post("/accounts") { + pwAuth("admin") + json { + "username" to "baz" + "password" to "xyz" + "name" to "Mallory" + "tan_channel" to "email" + } + }.assertConflict(TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED) + } + // DELETE /accounts/USERNAME @Test fun delete() = bankSetup { _ -> diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -91,7 +91,11 @@ fun bankSetup( isTalerExchange = false, isPublic = false, bonus = bonus, - checkPaytoIdempotent = false + checkPaytoIdempotent = false, + email = null, + phone = null, + cashoutPayto = null, + tanChannel = null )) assertEquals(AccountCreationResult.Success, db.account.create( login = "exchange", @@ -102,7 +106,11 @@ fun bankSetup( isTalerExchange = true, isPublic = false, bonus = bonus, - checkPaytoIdempotent = false + checkPaytoIdempotent = false, + email = null, + phone = null, + cashoutPayto = null, + tanChannel = null )) assertEquals(AccountCreationResult.Success, db.account.create( login = "customer", @@ -113,7 +121,11 @@ fun bankSetup( isTalerExchange = false, isPublic = false, bonus = bonus, - checkPaytoIdempotent = false + checkPaytoIdempotent = false, + email = null, + phone = null, + cashoutPayto = null, + tanChannel = null )) // Create admin account assertEquals(AccountCreationResult.Success, maybeCreateAdminAccount(db, ctx, "admin-password")) diff --git a/util/src/main/kotlin/TalerErrorCode.kt b/util/src/main/kotlin/TalerErrorCode.kt @@ -3514,6 +3514,14 @@ enum class TalerErrorCode(val code: Int) { /** + * A non-admin user has tried to create an account with 2fa. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_SET_TAN_CHANNEL(5145), + + + /** * 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).