diff options
author | Florian Dold <florian@dold.me> | 2024-09-23 21:37:05 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-09-23 21:37:12 +0200 |
commit | a0b89530827e4f7d477c02a5fabdb0d0d54c610d (patch) | |
tree | 3e097b0dbbaebebdc397ee7dd2e94bab68d1e3c0 | |
parent | 8f0e2ca8eb70560a8616b8f1b9407738099825e3 (diff) |
wallet-core: check credit/debit account restrictions, test
-rw-r--r-- | packages/taler-harness/src/harness/harness.ts | 15 | ||||
-rw-r--r-- | packages/taler-harness/src/harness/helpers.ts | 4 | ||||
-rw-r--r-- | packages/taler-harness/src/integrationtests/test-account-restrictions.ts | 168 | ||||
-rw-r--r-- | packages/taler-harness/src/integrationtests/testrunner.ts | 2 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/coinSelection.ts | 14 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 11 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/deposits.ts | 3 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/withdraw.ts | 35 |
8 files changed, 238 insertions, 14 deletions
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 9ed4e77ce..86ed98f1c 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -25,7 +25,6 @@ * Imports */ import { - AccountRestriction, AmountJson, Amounts, Configuration, @@ -603,6 +602,12 @@ class BankServiceBase { ) {} } +export type RestrictionFlag = "credit-restriction" | "debit-restriction"; + +export type HarnessAccountRestriction = + | [RestrictionFlag, "deny"] + | [RestrictionFlag, "regex", string, string, string]; + export interface HarnessExchangeBankAccount { accountName: string; accountPassword: string; @@ -611,13 +616,12 @@ export interface HarnessExchangeBankAccount { conversionUrl?: string; - debitRestrictions?: AccountRestriction[]; - creditRestrictions?: AccountRestriction[]; - /** * If set, the harness will not automatically configure the wire fee for this account. */ skipWireFeeCreation?: boolean; + + accountRestrictions?: HarnessAccountRestriction[]; } /** @@ -1380,6 +1384,9 @@ export class ExchangeService implements ExchangeServiceInterface { if (acct.conversionUrl != null) { optArgs.push("conversion-url", acct.conversionUrl); } + if (acct.accountRestrictions != null) { + optArgs.push(...acct.accountRestrictions.flat(1)); + } await runCommand( this.globalState, diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts index c6e9b2d35..73dcd1550 100644 --- a/packages/taler-harness/src/harness/helpers.ts +++ b/packages/taler-harness/src/harness/helpers.ts @@ -59,6 +59,7 @@ import { FakebankService, getTestHarnessPaytoForLabel, GlobalTestState, + HarnessAccountRestriction, HarnessExchangeBankAccount, harnessHttpLib, LibeufinBankService, @@ -130,6 +131,8 @@ export interface EnvOptions { walletTestObservability?: boolean; + accountRestrictions?: HarnessAccountRestriction[]; + additionalExchangeConfig?(e: ExchangeService): void; additionalMerchantConfig?(m: MerchantService): void; additionalBankConfig?(b: BankService): void; @@ -486,6 +489,7 @@ export async function createSimpleTestkudosEnvironmentV3( accountPassword: exchangeBankPassword, accountPaytoUri: exchangePaytoUri, skipWireFeeCreation: opts.skipWireFeeCreation === true, + accountRestrictions: opts.accountRestrictions, }; await exchange.addBankAccount("1", exchangeBankAccount); diff --git a/packages/taler-harness/src/integrationtests/test-account-restrictions.ts b/packages/taler-harness/src/integrationtests/test-account-restrictions.ts new file mode 100644 index 000000000..0b8e16fa2 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-account-restrictions.ts @@ -0,0 +1,168 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + AmountString, + j2s, + Logger, + NotificationType, + TalerCorebankApiClient, + TransactionMajorState, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + ExchangeServiceInterface, + GlobalTestState, + WalletClient, +} from "../harness/harness.js"; +import { + createSimpleTestkudosEnvironmentV3, + WithdrawViaBankResult, +} from "../harness/helpers.js"; + +const logger = new Logger("test-account-restrictions.ts"); + +/** + * Test for credit/debit account restrictions. + */ +export async function runAccountRestrictionsTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t, undefined, { + accountRestrictions: [ + [ + "debit-restriction", + "regex", + "payto://x-taler-bank/.*/foo-.*", + "bla", + "{}", + ], + [ + "credit-restriction", + "regex", + "payto://x-taler-bank/.*/foo-.*", + "bla", + "{}", + ], + ], + }); + + // Withdraw digital cash into the wallet. + + // The test is incomplete: The wallet can't check the account restrictions + // against the sender wire account, because fakebank doesn't report + // sender_wire yet. + + const withdrawalResult = await myWithdrawViaBank(t, { + walletClient, + bankClient, + exchange, + amount: "TESTKUDOS:20", + acctname: "foo-123", + }); + + await withdrawalResult.withdrawalFinishedCond; + + // Invalid account, does not start with "foo-" + const err = await t.assertThrowsTalerErrorAsync(async () => { + await walletClient.call(WalletApiOperation.CheckDeposit, { + amount: "TESTKUDOS:5", + depositPaytoUri: "payto://x-taler-bank/localhost/bar-42", + }); + }); + + logger.info(`checkResp ${j2s(err)}`); + + // Valid account + await walletClient.call(WalletApiOperation.CheckDeposit, { + amount: "TESTKUDOS:5", + depositPaytoUri: "payto://x-taler-bank/localhost/foo-42", + }); +} + +export async function myWithdrawViaBank( + t: GlobalTestState, + p: { + walletClient: WalletClient; + bankClient: TalerCorebankApiClient; + exchange: ExchangeServiceInterface; + amount: AmountString | string; + restrictAge?: number; + acctname: string; + }, +): Promise<WithdrawViaBankResult> { + const { walletClient: wallet, bankClient, exchange, amount } = p; + + const user = await bankClient.createRandomBankUser(); + await bankClient.registerAccountExtended({ + name: p.acctname, + password: "test", + username: p.acctname, + }); + const bankClient2 = new TalerCorebankApiClient(bankClient.baseUrl); + bankClient2.setAuth({ + username: user.username, + password: user.password, + }); + + const wop = await bankClient2.createWithdrawalOperation( + user.username, + amount, + ); + + // Hand it to the wallet + + await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { + talerWithdrawUri: wop.taler_withdraw_uri, + restrictAge: p.restrictAge, + }); + + // Withdraw (AKA select) + + const acceptRes = await wallet.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + restrictAge: p.restrictAge, + }, + ); + + const withdrawalFinishedCond = wallet.waitForNotificationCond( + (x) => + x.type === NotificationType.TransactionStateTransition && + x.newTxState.major === TransactionMajorState.Done && + x.transactionId === acceptRes.transactionId, + ); + + // Confirm it + + await bankClient2.confirmWithdrawalOperation(user.username, { + withdrawalOperationId: wop.withdrawal_id, + }); + + return { + accountPaytoUri: user.accountPaytoUri, + withdrawalFinishedCond, + transactionId: acceptRes.transactionId, + }; +} + +runAccountRestrictionsTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 8ae179701..e0f6ee65b 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -28,6 +28,7 @@ import { shouldLingerInTest, } from "../harness/harness.js"; import { getSharedTestDir } from "../harness/helpers.js"; +import { runAccountRestrictionsTest } from "./test-account-restrictions.js"; import { runAgeRestrictionsDepositTest } from "./test-age-restrictions-deposit.js"; import { runAgeRestrictionsMerchantTest } from "./test-age-restrictions-merchant.js"; import { runAgeRestrictionsMixedMerchantTest } from "./test-age-restrictions-mixed-merchant.js"; @@ -270,6 +271,7 @@ const allTests: TestMainFunction[] = [ runKycMerchantAggregateTest, runKycDepositDepositKyctransferTest, runWithdrawalPrepareTest, + runAccountRestrictionsTest, ]; export interface TestRunSpec { diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts index 01f8c49a9..bc9d51ec7 100644 --- a/packages/taler-wallet-core/src/coinSelection.ts +++ b/packages/taler-wallet-core/src/coinSelection.ts @@ -192,6 +192,20 @@ async function internalSelectPayCoins( | { sel: SelResult; coinRes: SelectedCoin[]; tally: CoinSelectionTally } | undefined > { + let restrictWireMethod; + if (req.depositPaytoUri) { + const parsedPayto = parsePaytoUri(req.depositPaytoUri); + if (!parsedPayto) { + throw Error("invalid payto URI"); + } + restrictWireMethod = parsedPayto.targetType; + if (restrictWireMethod !== req.restrictWireMethod) { + logger.warn(`conflicting payto URI and wire method restriction`); + } + } else { + restrictWireMethod = req.restrictWireMethod; + } + const { contractTermsAmount, depositFeeLimit } = req; const candidateRes = await selectPayCandidates(wex, tx, { currency: Amounts.currencyOf(req.contractTermsAmount), diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 346e56c11..2b64d8d9d 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -416,6 +416,8 @@ export interface ReserveBankInfo { currency: string | undefined; externalConfirmation?: boolean; + + senderWire?: string; } /** @@ -1420,6 +1422,7 @@ export const enum WithdrawalRecordType { export interface WgInfoBankIntegrated { withdrawalType: WithdrawalRecordType.BankIntegrated; + /** * Extra state for when this is a withdrawal involving * a Taler-integrated bank. @@ -1532,14 +1535,6 @@ export interface WithdrawalGroupRecord { status: WithdrawalGroupStatus; /** - * Wire information (as payto URI) for the bank account that - * transferred funds for this reserve. - * - * FIXME: Doesn't this belong to the bankAccounts object store? - */ - senderWire?: string; - - /** * Restrict withdrawals from this reserve to this age. */ restrictAge?: number; diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts index a4dd09005..2e0eca8d3 100644 --- a/packages/taler-wallet-core/src/deposits.ts +++ b/packages/taler-wallet-core/src/deposits.ts @@ -1523,6 +1523,7 @@ async function processDepositGroupPendingDeposit( exchanges: contractData.allowedExchanges, }, restrictWireMethod: contractData.wireMethod, + depositPaytoUri: dg.wire.payto_uri, contractTermsAmount: Amounts.parseOrThrow(contractData.amount), depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee), prevPayCoins: [], @@ -1906,6 +1907,7 @@ export async function internalCheckDepositGroup( exchanges: contractData.allowedExchanges, }, restrictWireMethod: contractData.wireMethod, + depositPaytoUri: req.depositPaytoUri, contractTermsAmount: Amounts.parseOrThrow(contractData.amount), depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee), prevPayCoins: [], @@ -2007,6 +2009,7 @@ export async function createDepositGroup( })), }, restrictWireMethod: depositPayto.targetType, + depositPaytoUri: req.depositPaytoUri, contractTermsAmount: amount, depositFeeLimit: amount, prevPayCoins: [], diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts index 2739294df..add04f3dc 100644 --- a/packages/taler-wallet-core/src/withdraw.ts +++ b/packages/taler-wallet-core/src/withdraw.ts @@ -85,6 +85,7 @@ import { WithdrawalType, addPaytoQueryParams, assertUnreachable, + checkAccountRestriction, checkDbInvariant, checkLogicInvariant, codecForAccountKycStatus, @@ -101,6 +102,7 @@ import { getRandomBytes, j2s, makeErrorDetail, + parsePaytoUri, parseTalerUri, parseWithdrawUri, } from "@gnu-taler/taler-util"; @@ -2439,7 +2441,7 @@ export async function processWithdrawalGroup( case WithdrawalGroupStatus.PendingWaitConfirmBank: return await processReserveBankStatus(wex, withdrawalGroupId); case WithdrawalGroupStatus.PendingKyc: - return processWithdrawalGroupPendingKyc(wex, withdrawalGroup); + return await processWithdrawalGroupPendingKyc(wex, withdrawalGroup); case WithdrawalGroupStatus.PendingReady: // Continue with the actual withdrawal! return await processWithdrawalGroupPendingReady(wex, withdrawalGroup); @@ -3123,7 +3125,6 @@ export async function internalPrepareCreateWithdrawalGroup( status: args.reserveStatus, withdrawalGroupId, restrictAge: args.restrictAge, - senderWire: undefined, timestampFinish: undefined, wgInfo: args.wgInfo, }; @@ -3354,6 +3355,7 @@ export async function prepareBankIntegratedWithdrawal( timestampReserveInfoPosted: undefined, wireTypes: withdrawInfo.wireTypes, currency: withdrawInfo.currency, + senderWire: withdrawInfo.senderWire, externalConfirmation, }, }, @@ -3426,6 +3428,10 @@ export async function confirmWithdrawal( bankCurrency = withdrawalGroup.wgInfo.bankInfo.currency; } + if (exchange.currency !== bankCurrency) { + throw Error("currency mismatch between exchange and bank"); + } + const exchangePaytoUri = await getExchangePaytoUri( wex, selectedExchange, @@ -3441,6 +3447,31 @@ export async function confirmWithdrawal( wex.cancellationToken, ); + const senderWire = withdrawalGroup.wgInfo.bankInfo.senderWire; + + if (senderWire) { + logger.info(`sender wire is ${senderWire}`); + const parsedSenderWire = parsePaytoUri(senderWire); + if (!parsedSenderWire) { + throw Error("invalid payto URI"); + } + let acceptable = false; + for (const acc of withdrawalAccountList) { + const parsedExchangeWire = parsePaytoUri(acc.paytoUri); + if (!parsedExchangeWire) { + continue; + } + if (!checkAccountRestriction(senderWire, acc.creditRestrictions ?? [])) { + continue; + } + acceptable = true; + break; + } + if (!acceptable) { + throw Error("bank account not acceptable by the exchange"); + } + } + const ctx = new WithdrawTransactionContext( wex, withdrawalGroup.withdrawalGroupId, |