libeufin

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

commit 8398693a23f3774478221940f2ad683da4512b90
parent 5c66545f2ce01c7eb25dbb091871334b406f2687
Author: Antoine A <>
Date:   Sat,  2 May 2026 17:07:27 +0200

bank: more token API work

Diffstat:
Mcrates/libeufin-bank/src/api.rs | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mcrates/libeufin-bank/src/api/account.rs | 2+-
Mcrates/libeufin-bank/src/api/token.rs | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mcrates/libeufin-bank/src/auth.rs | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mcrates/libeufin-bank/src/config.rs | 4+++-
Mcrates/libeufin-bank/src/db/token.rs | 35++++++++++++++++++++---------------
Mcrates/libeufin-bank/src/lib.rs | 6++++--
Mcrates/libeufin-nexus/src/config.rs | 2+-
Mcrates/libeufin-nexus/src/db.rs | 2+-
Mdatabase-versioning/libeufin-bank-procedures.sql | 5+++--
10 files changed, 457 insertions(+), 147 deletions(-)

diff --git a/crates/libeufin-bank/src/api.rs b/crates/libeufin-bank/src/api.rs @@ -35,13 +35,14 @@ pub mod test { use axum::{ Router, - http::{Method, header::AUTHORIZATION}, + http::{Method, StatusCode, header::AUTHORIZATION}, }; use compact_str::CompactString; use sqlx::{PgPool, Postgres, pool::PoolConnection}; use taler_api::api::TalerRouter; use taler_common::{ config::Config, + error_code::ErrorCode, types::{ amount::{Amount, Decimal}, payto::{IbanPayto, Payto}, @@ -116,7 +117,7 @@ pub mod test { self.tokens.borrow()[username].clone() } - async fn token_auth_request( + async fn requesta( &self, method: Method, path: &str, @@ -125,48 +126,44 @@ pub mod test { let username = username.unwrap_or_else(|| Self::extract_username(path)); let token = self.cached_token(username).await; self.server - .method(method, path) + .request(method, path) .header(AUTHORIZATION, token) } pub async fn postpw(&self, path: &str) -> TestRequest { - Self::pw_auth(self.server.method(Method::POST, path), None) + Self::pw_auth(self.server.request(Method::POST, path), None) } pub async fn geta(&self, path: &str) -> TestRequest { - self.token_auth_request(Method::GET, path, None).await + self.requesta(Method::GET, path, None).await } pub async fn posta(&self, path: &str) -> TestRequest { - self.token_auth_request(Method::POST, path, None).await + self.requesta(Method::POST, path, None).await } pub async fn patcha(&self, path: &str) -> TestRequest { - self.token_auth_request(Method::PATCH, path, None).await + self.requesta(Method::PATCH, path, None).await } pub async fn deletea(&self, path: &str) -> TestRequest { - self.token_auth_request(Method::DELETE, path, None).await + self.requesta(Method::DELETE, path, None).await } pub async fn get_admin(&self, path: &str) -> TestRequest { - self.token_auth_request(Method::GET, path, Some("admin")) - .await + self.requesta(Method::GET, path, Some("admin")).await } pub async fn post_admin(&self, path: &str) -> TestRequest { - self.token_auth_request(Method::POST, path, Some("admin")) - .await + self.requesta(Method::POST, path, Some("admin")).await } pub async fn patch_admin(&self, path: &str) -> TestRequest { - self.token_auth_request(Method::PATCH, path, Some("admin")) - .await + self.requesta(Method::PATCH, path, Some("admin")).await } pub async fn delete_admin(&self, path: &str) -> TestRequest { - self.token_auth_request(Method::DELETE, path, Some("admin")) - .await + self.requesta(Method::DELETE, path, Some("admin")).await } } @@ -291,4 +288,55 @@ pub mod test { tokens: RefCell::new(BTreeMap::new()), } } + + pub enum Auth { + Admin, + Exchange, + User, + UserOnly, + } + + pub async fn auth_routine( + ctx: &BankTestCtx, + method: Method, + path: &str, + auth: Auth, + optional: bool, + ) { + // Bad header + ctx.request(method.clone(), path) + .header(AUTHORIZATION, "WTF") + .await + .assert_error(ErrorCode::GENERIC_UNAUTHORIZED); + + if !optional { + // No header + ctx.request(method.clone(), path).await.assert_error_status( + ErrorCode::GENERIC_PARAMETER_MISSING, + StatusCode::UNAUTHORIZED, + ); + + // Other account + ctx.requesta(method.clone(), path, Some("merchant")) + .await + .await + .assert_error(ErrorCode::GENERIC_FORBIDDEN); + } + + match auth { + Auth::Admin | Auth::Exchange => { + ctx.requesta(method.clone(), path, Some("merchant")) + .await + .await + .assert_error(ErrorCode::GENERIC_FORBIDDEN); + } + Auth::User => {} + Auth::UserOnly => { + ctx.requesta(method.clone(), path, Some("admin")) + .await + .await + .assert_error(ErrorCode::GENERIC_FORBIDDEN); + } + } + } } diff --git a/crates/libeufin-bank/src/api/account.rs b/crates/libeufin-bank/src/api/account.rs @@ -306,7 +306,7 @@ pub async fn create_admin_account( // TODO is this secure enough ? let pw = pw .map(|it| it.to_owned()) - .unwrap_or_else(|| Base32::<32>::rand().to_string()); + .unwrap_or_else(|| Base32::<32>::secure_rand().to_string()); let payto = match cfg.wire_method { WireMethod::iban => LibeufinId::IBAN(rand_iban_payto().into_inner()), diff --git a/crates/libeufin-bank/src/api/token.rs b/crates/libeufin-bank/src/api/token.rs @@ -19,11 +19,21 @@ use std::{sync::Arc, time::Duration}; -use axum::{Json, Router, extract::State, routing::post}; +use axum::{ + Json, Router, + extract::State, + response::{IntoResponse, NoContent}, + routing::{delete, get, post}, +}; +use compact_str::CompactString; use jiff::Timestamp; use serde::{Deserialize, Serialize}; -use taler_api::{error::failure, extract::Req}; +use taler_api::{ + error::{ApiResult, failure}, + extract::{Path, Query, Req}, +}; use taler_common::{ + api_params::PageParams, error_code::ErrorCode::{self}, types::{ base32::Base32, @@ -33,10 +43,14 @@ use taler_common::{ use crate::{ api::BankState, - auth::{RefreshAuth, TOKEN_PREFIX, TokenScope}, - db::token::{TokenCreationResult, access, create}, + auth::{Auth, TOKEN_PREFIX, Token, TokenScope, UserR, UserRW}, + db::{ + self, + token::{TokenCreationResult, access, create}, + }, }; +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TokenInfo { pub creation_time: TalerTimestamp, pub expiration: TalerTimestamp, @@ -49,6 +63,11 @@ pub struct TokenInfo { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TokenInfos { + pub tokens: Vec<TokenInfo>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TokenRequest { pub scope: TokenScope, pub duration: Option<RelativeTime>, @@ -61,66 +80,117 @@ pub struct TokenRequest { pub struct TokenSuccessResponse { pub access_token: String, pub expiration: TalerTimestamp, + pub token_id: u64, } pub fn token_api() -> Router<Arc<BankState>> { - Router::new().route( - "/accounts/{username}/token", - post( - async |RefreshAuth { username, token }: RefreshAuth, - State(state): State<Arc<BankState>>, - Req(req): Req<TokenRequest>| { - if let Some(token) = token { - // This block checks permissions ONLY IF the call was authenticated with a token - let token = access(&state.db, &token, &Timestamp::now()).await?; - let Some(token) = token else { - return Err(failure( - ErrorCode::BANK_UNMANAGED_EXCEPTION, - "Token used to auth not found in the database", - )); - }; - if !req.scope.logical().is_valid_scope(token.scope, true) { - return Err(failure( - ErrorCode::GENERIC_TOKEN_PERMISSION_INSUFFICIENT, - "Impossible to refresh a token with a larger scope", - )); + Router::new() + .route( + "/accounts/{username}/token", + post( + async |Auth { + username, token, .. + }: Auth<Token>, + State(state): State<Arc<BankState>>, + Req(req): Req<TokenRequest>| { + if let Some(token) = token { + // This block checks permissions ONLY IF the call was authenticated with a token + let token = access(&state.db, &token, &Timestamp::now()).await?; + let Some(token) = token else { + return Err(failure( + ErrorCode::BANK_UNMANAGED_EXCEPTION, + "Token used to auth not found in the database", + )); + }; + if !req.scope.logical().is_valid_scope(token.scope, true) { + return Err(failure( + ErrorCode::GENERIC_TOKEN_PERMISSION_INSUFFICIENT, + "Impossible to refresh a token with a larger scope", + )); + } } - } - // TODO secure random - let new = Base32::<32>::rand(); - let creation = Timestamp::now(); - let expiration = match req - .duration - .unwrap_or(RelativeTime::Duration(Duration::from_hours(24))) - { - RelativeTime::Forever => TalerTimestamp::Never, - RelativeTime::Duration(duration) => { - TalerTimestamp::Timestamp(creation + duration) + let new = Base32::<32>::secure_rand(); + let creation = Timestamp::now(); + let expiration = match req + .duration + .unwrap_or(RelativeTime::Duration(Duration::from_hours(24))) + { + RelativeTime::Forever => TalerTimestamp::Never, + RelativeTime::Duration(duration) => TalerTimestamp::Timestamp( + creation.checked_add(duration).map_err(|e| { + failure( + ErrorCode::GENERIC_JSON_INVALID, + format_args!("Bad token duration: {}", e), + ) + })?, + ), + }; + match create( + &state.db, + &username, + new.as_ref(), + &creation, + &expiration, + &req.scope, + req.refreshable, + req.description.as_deref(), + false, + ) + .await? + { + TokenCreationResult::Success(token_id) => Ok(Json(TokenSuccessResponse { + access_token: format!("{TOKEN_PREFIX}{new}"), + expiration, + token_id, + })), + TokenCreationResult::TanRequired => todo!(), } - }; - match create( - &state.db, - &username, - new.as_ref(), - &creation, - &expiration, - &req.scope, - req.refreshable, - req.description.as_deref(), - false, - ) - .await? - { - TokenCreationResult::Success => Ok(Json(TokenSuccessResponse { - access_token: format!("{TOKEN_PREFIX}{new}"), - expiration, - })), - TokenCreationResult::TanRequired => todo!(), - } - }, - ), - ) + }, + ) + .delete( + async |Auth { token, .. }: Auth<UserR>, State(state): State<Arc<BankState>>| { + if let Some(token) = token { + db::token::delete(&state.db, &token).await?; + } + ApiResult::Ok(NoContent) + }, + ), + ) + .route( + "/accounts/{username}/tokens/{id}", + delete( + async |Auth { username, .. }: Auth<UserRW>, + Path((_, id)): Path<(CompactString, u64)>, + State(state): State<Arc<BankState>>| { + if db::token::delete_by_id(&state.db, &username, id).await? { + ApiResult::Ok(NoContent) + } else { + ApiResult::Err(failure( + ErrorCode::BANK_TRANSACTION_NOT_FOUND, + format_args!("Token '{id}' not found"), + )) + } + }, + ), + ) + .route( + "/accounts/{username}/tokens", + get( + async |Auth { username, .. }: Auth<UserR>, + Query(params): Query<PageParams>, + State(state): State<Arc<BankState>>| { + let params = params.check()?; + let tokens = + db::token::page(&state.db, &params, &username, &Timestamp::now()).await?; + if tokens.is_empty() { + ApiResult::Ok(NoContent.into_response()) + } else { + ApiResult::Ok(Json(TokenInfos { tokens }).into_response()) + } + }, + ), + ) } #[cfg(test)] @@ -128,7 +198,7 @@ pub mod test { use std::str::FromStr; - use axum::http::header::AUTHORIZATION; + use axum::http::{Method, header::AUTHORIZATION}; use jiff::{SignedDuration, Timestamp}; use taler_common::{ error_code::ErrorCode, @@ -137,16 +207,34 @@ pub mod test { use taler_test_utils::{json, server::TestServer}; use crate::{ - api::{test::bank_setup_conf, token::TokenSuccessResponse}, + api::{ + test::{Auth, auth_routine, bank_setup_conf}, + token::{TokenInfos, TokenSuccessResponse}, + }, auth::TOKEN_PREFIX, db::token::access, }; #[tokio::test] - async fn create() { + async fn create_and_delete() { let ctx = bank_setup_conf("test.conf").await; - // TODO auth routine + auth_routine( + &ctx, + Method::POST, + "/accounts/customer/token", + Auth::User, + true, + ) + .await; + auth_routine( + &ctx, + Method::DELETE, + "/accounts/customer/tokens/1", + Auth::User, + false, + ) + .await; // Unknown account ctx.post("/accounts/merchant/token") @@ -184,8 +272,13 @@ pub mod test { .await .unwrap() .unwrap(); - let lifetime = token.creation.duration_until(token.expiration); - assert_eq!(lifetime, SignedDuration::from_hours(24)); + match token.expiration { + TalerTimestamp::Never => unreachable!(), + TalerTimestamp::Timestamp(expiration) => { + let lifetime = token.creation.duration_until(expiration); + assert_eq!(lifetime, SignedDuration::from_hours(24)); + } + } // Check valid refresh scope for (from_scope, to_scope) in [ @@ -287,5 +380,105 @@ pub mod test { })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); + + // Delete current token + let res: TokenSuccessResponse = ctx + .postpw("/accounts/merchant/token") + .await + .json(json!({ "scope": "readonly" })) + .await + .assert_ok_json(); + // Check OK + ctx.delete("/accounts/merchant/token") + .header(AUTHORIZATION, format!("Bearer {}", res.access_token)) + .json(json!({ "scope": "readonly" })) + .await + .assert_no_content(); + // Check token no longer work + ctx.delete("/accounts/merchant/token") + .header(AUTHORIZATION, format!("Bearer {}", res.access_token)) + .json(json!({ "scope": "readonly" })) + .await + .assert_error(ErrorCode::GENERIC_TOKEN_UNKNOWN); + + // Delete by id + let res: TokenSuccessResponse = ctx + .postpw("/accounts/merchant/token") + .await + .json(json!({ "scope": "readonly" })) + .await + .assert_ok_json(); + // Wrong account + ctx.deletea(&format!("/accounts/customer/tokens/{}", res.token_id)) + .await + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + // Check OK + ctx.deletea(&format!("/accounts/merchant/tokens/{}", res.token_id)) + .await + .await + .assert_no_content(); + ctx.deletea(&format!("/accounts/merchant/tokens/{}", res.token_id)) + .await + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + // Check token no longer work + ctx.post("/accounts/merchant/token") + .header(AUTHORIZATION, format!("Bearer {}", res.access_token)) + .json(json!({ "scope": "readonly" })) + .await + .assert_error(ErrorCode::GENERIC_TOKEN_UNKNOWN); + } + + #[tokio::test] + async fn get() { + let ctx = bank_setup_conf("test.conf").await; + auth_routine( + &ctx, + Method::GET, + "/accounts/customer/tokens", + Auth::User, + false, + ) + .await; + + // Check OK + for account in ["merchant", "customer"] { + ctx.geta(&format!("/accounts/{account}/tokens")) + .await + .await + .assert_no_content(); + } + for scope in ["readonly", "readwrite"] { + ctx.postpw("/accounts/merchant/token") + .await + .json(json!({ "scope": scope })) + .await + .assert_ok(); + } + ctx.postpw("/accounts/customer/token") + .await + .json(json!({ + "scope": "revenue", + "description": "description" + })) + .await + .assert_ok(); + let res: TokenInfos = ctx + .geta("/accounts/merchant/tokens") + .await + .await + .assert_ok_json(); + assert_eq!(res.tokens.len(), 2); + for token in res.tokens { + assert_eq!(token.description, None); + } + let res: TokenInfos = ctx + .geta("/accounts/customer/tokens") + .await + .await + .assert_ok_json(); + assert_eq!(res.tokens.len(), 1); + assert_eq!(res.tokens[0].description.as_deref(), Some("description")); } } diff --git a/crates/libeufin-bank/src/auth.rs b/crates/libeufin-bank/src/auth.rs @@ -17,13 +17,12 @@ * <http://www.gnu.org/licenses/> */ -use std::{fmt::Display, sync::Arc}; +use std::{fmt::Display, marker::PhantomData, sync::Arc}; use axum::{ - RequestPartsExt as _, - extract::{FromRequestParts, Path}, + extract::FromRequestParts, http::{ - HeaderValue, + HeaderMap, HeaderValue, StatusCode, Uri, header::{AUTHORIZATION, WWW_AUTHENTICATE}, request::Parts, }, @@ -32,10 +31,11 @@ use compact_str::CompactString; use jiff::Timestamp; use serde::{Deserialize, Serialize}; use sqlx::PgPool; -use taler_api::error::{ApiError, ApiResult, failure, failure_code, unauthorized}; +use taler_api::error::{ApiError, ApiResult, failure, failure_code, failure_status, unauthorized}; use taler_common::{ encoding::{base32, base64}, error_code::ErrorCode, + types::timestamp::TalerTimestamp, }; use tracing::warn; @@ -132,10 +132,10 @@ impl FromRequestParts<Arc<BankState>> for RegistrationAuth { TokenLogicalScope::readwrite, false, state.cfg.basic_auth_compat, - parts, + &parts.headers, ) .await?; - if info.is_admin() { + if !info.is_admin() { return Err(failure( ErrorCode::GENERIC_FORBIDDEN, "Only administrator allowed", @@ -145,60 +145,112 @@ impl FromRequestParts<Arc<BankState>> for RegistrationAuth { } } -pub struct RefreshAuth { +pub enum AuthKind { + AdminOnly, + UserOnly, + UserOrAdmin, +} + +pub trait AuthScope { + const SCOPE: TokenLogicalScope; + const KIND: AuthKind; + const ALLOW_BASIC_AUTH: bool = false; +} + +pub struct Auth<S: AuthScope> { pub username: CompactString, + pub info: BankInfo, pub token: Option<Vec<u8>>, + scope: PhantomData<S>, +} + +fn extract_username(url: &Uri) -> &str { + let mut iter = url.path().strip_prefix('/').unwrap().split('/'); + assert_eq!(iter.next(), Some("accounts")); + iter.next().unwrap() } -impl FromRequestParts<Arc<BankState>> for RefreshAuth { +impl<S: AuthScope> FromRequestParts<Arc<BankState>> for Auth<S> { type Rejection = ApiError; async fn from_request_parts( parts: &mut Parts, state: &Arc<BankState>, ) -> Result<Self, Self::Rejection> { - let Path(username): Path<CompactString> = parts.extract().await?; + let username = extract_username(&parts.uri); let (info, token) = auth_request( &state.db, &state.cfg.ctx, &state.cfg.pw_crypto, - TokenLogicalScope::refreshable, - true, + S::SCOPE, + S::ALLOW_BASIC_AUTH || state.cfg.basic_auth_compat, state.cfg.basic_auth_compat, - parts, + &parts.headers, ) .await?; - if info.is_admin() { - return Err(failure( - ErrorCode::GENERIC_FORBIDDEN, - "Only administrator allowed", - )); + + match S::KIND { + AuthKind::AdminOnly => { + if !info.is_admin() { + return Err(failure( + ErrorCode::GENERIC_FORBIDDEN, + "Only administrator allowed", + )); + } + } + AuthKind::UserOnly => { + if info.username != username { + return Err(failure( + ErrorCode::GENERIC_FORBIDDEN, + format_args!( + "Customer {} have no right on {username} account", + info.username + ), + )); + } + } + AuthKind::UserOrAdmin => { + if info.username != username && !info.is_admin() { + return Err(failure( + ErrorCode::GENERIC_FORBIDDEN, + format_args!( + "Customer {} have no right on {username} account", + info.username + ), + )); + } + } } - Ok(RefreshAuth { username, token }) + + Ok(Self { + username: username.into(), + info, + token, + scope: PhantomData, + }) } } -pub struct AdminRWAuth(BankInfo); +pub struct UserRW; -impl FromRequestParts<Arc<BankState>> for AdminRWAuth { - type Rejection = ApiError; +impl AuthScope for UserRW { + const SCOPE: TokenLogicalScope = TokenLogicalScope::readwrite; + const KIND: AuthKind = AuthKind::UserOrAdmin; +} - async fn from_request_parts( - parts: &mut Parts, - state: &Arc<BankState>, - ) -> Result<Self, Self::Rejection> { - let (info, _) = auth_request( - &state.db, - &state.cfg.ctx, - &state.cfg.pw_crypto, - TokenLogicalScope::readwrite, - false, - false, - parts, - ) - .await?; - Ok(Self(info)) - } +pub struct UserR; + +impl AuthScope for UserR { + const SCOPE: TokenLogicalScope = TokenLogicalScope::readonly; + const KIND: AuthKind = AuthKind::UserOrAdmin; +} + +pub struct Token; + +impl AuthScope for Token { + const SCOPE: TokenLogicalScope = TokenLogicalScope::refreshable; + const KIND: AuthKind = AuthKind::UserOrAdmin; + const ALLOW_BASIC_AUTH: bool = true; } /** @@ -215,17 +267,22 @@ async fn auth_request( scope: TokenLogicalScope, allow_pw: bool, compat_pw: bool, - parts: &Parts, + headers: &HeaderMap, ) -> ApiResult<(BankInfo, Option<Vec<u8>>)> { fn headers_malformed(hint: impl Display) -> ApiError { - ApiError::new(ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED).with_hint(hint) + failure_status( + ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED, + hint, + StatusCode::UNAUTHORIZED, + ) } - let header = parts.headers.get(AUTHORIZATION); + let header = headers.get(AUTHORIZATION); let Some(authorisation) = header else { - let err = failure( - ErrorCode::GENERIC_UNAUTHORIZED, + let err = failure_status( + ErrorCode::GENERIC_PARAMETER_MISSING, "Authorization header not found", + StatusCode::UNAUTHORIZED, ); if allow_pw || compat_pw { return Err(err.with_header( @@ -288,7 +345,9 @@ async fn auth_request( return Err(failure_code(ErrorCode::GENERIC_TOKEN_UNKNOWN)); }; - if token.expiration < now { + if let TalerTimestamp::Timestamp(expiration) = token.expiration + && expiration < now + { return Err(failure_code(ErrorCode::GENERIC_TOKEN_EXPIRED)); } else if !scope.is_valid_scope(token.scope, token.is_refreshable) { return Err(failure_code( diff --git a/crates/libeufin-bank/src/config.rs b/crates/libeufin-bank/src/config.rs @@ -27,6 +27,7 @@ use taler_common::{ map_config, types::amount::{Amount, Currency}, }; +use taler_macros::EnumMeta; use tracing::warn; use url::Url; @@ -44,8 +45,9 @@ pub struct CurrencySpecification { pub alt_unit_names: BTreeMap<CompactString, CompactString>, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, taler_macros::EnumMeta)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] #[enum_meta(Str)] +#[allow(non_camel_case_types)] pub enum WireMethod { iban, x_taler_bank, diff --git a/crates/libeufin-bank/src/db/token.rs b/crates/libeufin-bank/src/db/token.rs @@ -26,7 +26,7 @@ use taler_api::{ db::{BindHelper, TypeHelper}, serialized, }; -use taler_common::{api_common::ShortHashCode, api_params::Page, types::timestamp::TalerTimestamp}; +use taler_common::{api_params::Page, types::timestamp::TalerTimestamp}; use crate::{ api::token::TokenInfo, @@ -39,12 +39,12 @@ pub struct BearerToken { pub scope: TokenScope, pub is_refreshable: bool, pub creation: Timestamp, - pub expiration: Timestamp, + pub expiration: TalerTimestamp, } /** Result status of token creation */ pub enum TokenCreationResult { - Success, + Success(u64), TanRequired, } @@ -62,11 +62,7 @@ pub async fn create( ) -> sqlx::Result<TokenCreationResult> { serialized!( sqlx::query( - " - SELECT out_tan_required FROM create_token( - $1,$2,$3,$4,$5,$6,$7,$8 - ) - ", + "SELECT out_tan_required, out_token_id FROM create_token($1,$2,$3,$4,$5,$6,$7,$8)" ) .bind(username) .bind(content) @@ -80,7 +76,7 @@ pub async fn create( Ok(if r.try_get_flag("out_tan_required")? { TokenCreationResult::TanRequired } else { - TokenCreationResult::Success + TokenCreationResult::Success(r.try_get_u64("out_token_id")?) }) }) .fetch_one(db) @@ -114,7 +110,7 @@ pub async fn access( scope: r.try_get("scope")?, is_refreshable: r.try_get("is_refreshable")?, creation: r.try_get_timestamp("creation_time")?, - expiration: r.try_get_timestamp("expiration_time")?, + expiration: r.try_get_taler_timestamp("expiration_time")?, }) }) .fetch_optional(db) @@ -159,7 +155,7 @@ pub async fn access_info( scope: r.try_get("scope")?, is_refreshable: r.try_get("is_refreshable")?, creation: r.try_get_timestamp("creation_time")?, - expiration: r.try_get_timestamp("expiration_time")?, + expiration: r.try_get_taler_timestamp("expiration_time")?, }, BankInfo { username: r.try_get("username")?, @@ -185,11 +181,20 @@ pub async fn delete(db: &PgPool, token: &[u8]) -> sqlx::Result<bool> { Ok(res.rows_affected() > 0) } -pub async fn delete_by_id(db: &PgPool, id: u64) -> sqlx::Result<bool> { +pub async fn delete_by_id(db: &PgPool, username: &str, id: u64) -> sqlx::Result<bool> { let res = serialized!( - sqlx::query("DELETE FROM bearer_tokens WHERE bearer_token_id=$1") - .bind(id as i64) - .execute(db) + sqlx::query( + " + DELETE FROM bearer_tokens + USING customers + WHERE bearer_tokens.bank_customer = customers.customer_id + AND bearer_token_id = $1 + AND username = $2 + " + ) + .bind(id as i64) + .bind(username) + .execute(db) )?; Ok(res.rows_affected() > 0) } diff --git a/crates/libeufin-bank/src/lib.rs b/crates/libeufin-bank/src/lib.rs @@ -17,6 +17,7 @@ * <http://www.gnu.org/licenses/> */ +use serde::{Deserialize, Serialize}; use taler_common::config::parser::ConfigSource; use taler_macros::EnumMeta; @@ -43,11 +44,12 @@ pub const CONFIG_SOURCE: ConfigSource = PartialOrd, Ord, EnumMeta, - serde::Serialize, - serde::Deserialize, + Serialize, + Deserialize, )] #[sqlx(type_name = "tan_enum")] #[enum_meta(Str)] +#[allow(non_camel_case_types)] pub enum TanChannel { sms, email, diff --git a/crates/libeufin-nexus/src/config.rs b/crates/libeufin-nexus/src/config.rs @@ -31,7 +31,6 @@ use regex::Regex; use taler_api::config::DbCfg; use taler_common::{ config::{Config, ValueErr}, - map_config, types::{ amount::{Amount, Currency}, payto::{BankID, FullIbanPayto}, @@ -100,6 +99,7 @@ impl NexusHostCfg { #[derive(Debug, Clone, Copy, EnumMeta)] #[enum_meta(Str)] +#[allow(non_camel_case_types)] pub enum AccountType { exchange, normal, diff --git a/crates/libeufin-nexus/src/db.rs b/crates/libeufin-nexus/src/db.rs @@ -15,7 +15,7 @@ */ use jiff::Timestamp; -use sqlx::{PgPool, Row, types::Json}; +use sqlx::{PgPool, types::Json}; use taler_api::db::BindHelper; use taler_common::config::Config; use tokio::sync::watch::Sender; diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -236,7 +236,8 @@ CREATE FUNCTION create_token( IN in_refreshable BOOLEAN, IN in_description TEXT, IN in_is_tan BOOLEAN, - OUT out_tan_required BOOLEAN + OUT out_tan_required BOOLEAN, + OUT out_token_id INT8 ) LANGUAGE plpgsql AS $$ DECLARE @@ -268,7 +269,7 @@ INSERT INTO bearer_tokens ( in_refreshable, in_description, in_creation_time -); +) RETURNING bearer_token_id INTO out_token_id; END $$; CREATE FUNCTION bank_wire_transfer(