commit 004fc57de2e604ad29816d79bd4f593b2c26090a
parent d66b43e756f2b9f39e720b466fa34b25d0a7d522
Author: Antoine A <>
Date: Wed, 10 Jan 2024 11:23:34 +0000
Allow account creation with 2FA by admin
Diffstat:
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).