commit a90ee04123141b0544288e1dba3506829fc102a7
parent e5c3c0d1d2561d43829200321f4fd326174987ce
Author: Antoine A <>
Date: Fri, 4 Sep 2026 14:28:41 +0200
bank: many fixes
Diffstat:
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,
}