libeufin

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

commit a90ee04123141b0544288e1dba3506829fc102a7
parent e5c3c0d1d2561d43829200321f4fd326174987ce
Author: Antoine A <>
Date:   Fri,  4 Sep 2026 14:28:41 +0200

bank: many fixes

Diffstat:
Mlibeufin-bank/conf/test_restrict.conf | 2++
Mlibeufin-bank/src/api.rs | 2+-
Mlibeufin-bank/src/api/account.rs | 25++++++++++++++++++++++---
Mlibeufin-bank/src/api/tan.rs | 8+++++---
Mlibeufin-bank/src/api/token.rs | 2+-
Mlibeufin-bank/src/config.rs | 19++++++++++++++++++-
6 files changed, 49 insertions(+), 9 deletions(-)

diff --git a/libeufin-bank/conf/test_restrict.conf b/libeufin-bank/conf/test_restrict.conf @@ -7,6 +7,8 @@ DEFAULT_DEBT_LIMIT = KUDOS:100 allow_conversion = YES FIAT_CURRENCY = EUR PWD_HASH_CONFIG = { "cost": 4 } +tan_sms = libeufin-tan-file.sh +tan_email = libeufin-tan-file.sh [libeufin-bankdb-postgres] CONFIG = postgresql:///taler_rust_check \ No newline at end of file diff --git a/libeufin-bank/src/api.rs b/libeufin-bank/src/api.rs @@ -363,7 +363,7 @@ pub mod test { } })) .await - .maybe_challenge(&self) + .maybe_challenge(self) .await .assert_ok_json::<serde_json::Value>(); let token = res["access_token"].as_str().unwrap(); diff --git a/libeufin-bank/src/api/account.rs b/libeufin-bank/src/api/account.rs @@ -317,8 +317,11 @@ impl AccountReconfiguration { pub fn channels(&self) -> Option<&[TanChannel]> { if let Some(many) = &self.tan_channels { Some(many) - } else if let Some(one) = self.tan_channel.opt() { - Some(std::slice::from_ref(one)) + } else if let Some(value) = self.tan_channel.inner() { + match value { + Some(one) => Some(std::slice::from_ref(one)), + None => Some(&[]), + } } else { None } @@ -600,7 +603,13 @@ pub fn account_api() -> Router<Arc<BankState>> { "Cannot delete 'exchange' accounts when conversion is enabled", )); } - match db::account::delete(&state.db, &auth.username, mfa.is_2fa()).await? { + match db::account::delete( + &state.db, + &auth.username, + auth.is_admin() || mfa.is_2fa(), + ) + .await? + { DeletionResult::Success => Ok(NoContent.into_response()), DeletionResult::UnknownAccount => { Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) @@ -1450,11 +1459,21 @@ pub mod test { let ctx = ctx.swap_cfg("test_restrict.conf").await; ctx.auth_routine(Method::DELETE, "/accounts/merchant", Auth::Admin) .await; + ctx.delete_admin("/accounts/merchant") + .await + .assert_no_content(); + // Exchange is still restricted ctx.delete_admin("/accounts/exchange") .await .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT); + // Test 2FA account deletion + ctx.fill_tan_info("customer").await; + ctx.delete_admin("/accounts/customer") + .await + .assert_no_content(); + // Test delete exchange account let ctx = ctx.swap_cfg("test_no_conversion.conf").await; // Exchange is no longer restricted diff --git a/libeufin-bank/src/api/tan.rs b/libeufin-bank/src/api/tan.rs @@ -77,7 +77,7 @@ pub fn tan_api() -> Router<Arc<BankState>> { async |State(state): State<Arc<BankState>>, Path((_, id)): Path<((), Uuid)>| { match send(&state.db, &id, &Timestamp::now(), MAX_ACTIVE_CHALLENGES).await? { - SendResult::NotFound => Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)), + SendResult::NotFound => Err(failure_code(ErrorCode::BANK_CHALLENGE_NOT_FOUND)), SendResult::Expired => Err(failure_code(ErrorCode::BANK_TAN_CHALLENGE_EXPIRED)), SendResult::TooMany => Err(failure_code(ErrorCode::BANK_TAN_RATE_LIMITED)), SendResult::Solved => Ok(StatusCode::GONE.into_response()), @@ -87,7 +87,9 @@ pub fn tan_api() -> Router<Arc<BankState>> { code, expiration, } => { - let (script, env) = &state.cfg.tan_channels[&channel]; + let Some((script, env) )= &state.cfg.tan_channels.get(&channel) else { + return Err(failure_code(ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED)) + }; let msg = format!("T-{code} is your {} verification code", state.cfg.name); trace!(target: "tan", "send {code} with {script}"); let res = async { @@ -405,7 +407,7 @@ pub mod test { // Unknown challenge ctx.posta(format!("/accounts/merchant/challenge/{}", Uuid::new_v4())) .await - .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + .assert_error(ErrorCode::BANK_CHALLENGE_NOT_FOUND); // Unknown challenge ctx.posta("/accounts/merchant/challenge/BAD") diff --git a/libeufin-bank/src/api/token.rs b/libeufin-bank/src/api/token.rs @@ -148,7 +148,7 @@ pub fn token_api() -> Router<Arc<BankState>> { &req.scope, req.refreshable, req.description.as_deref(), - auth.is_admin() || mfa.is_2fa(), + mfa.is_2fa(), ) .await? }; diff --git a/libeufin-bank/src/config.rs b/libeufin-bank/src/config.rs @@ -187,7 +187,9 @@ impl BankCfg { }, wire_method, pw_crypto: map_config!(s, "password hash algorithm", "pwd_hash_algorithm", - "bcrypt" => { s.json::<BcryptCfg>("pwd_hash_config").require()?.into() } + "bcrypt" => { + s.json::<BcryptCfg>("pwd_hash_config").require()?.into() + } ) .require()?, gc_abort_after: s.span("gc_abort_after").require()?, @@ -267,8 +269,23 @@ impl BankCfg { } } +fn deserialize_bcrypt_cost<'de, D>(deserializer: D) -> Result<u32, D::Error> +where + D: serde::Deserializer<'de>, +{ + let cost = u32::deserialize(deserializer)?; + + if (4..=31).contains(&cost) { + Ok(cost) + } else { + Err(serde::de::Error::custom(format!( + "expected bcrypt cost in [4, 31], got {cost}" + ))) + } +} #[derive(serde::Deserialize)] struct BcryptCfg { + #[serde(deserialize_with = "deserialize_bcrypt_cost")] cost: u32, }