commit 034b675906062a83a22d830164e24a6acd31276d
parent 03b8a0058c7030bd23336112320d879e080bc753
Author: Florian Dold <florian@dold.me>
Date: Wed, 30 Oct 2024 13:51:33 +0100
harness: aml program support, reproducer for #9154
Diffstat:
5 files changed, 275 insertions(+), 27 deletions(-)
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
@@ -33,6 +33,7 @@ import {
TalerCoreBankHttpClient,
TalerMerchantInstanceHttpClient,
TalerMerchantManagementHttpClient,
+ TalerProtocolTimestamp,
TransactionsResponse,
createRFC8959AccessTokenEncoded,
createRFC8959AccessTokenPlain,
@@ -63,21 +64,25 @@ import { deepStrictEqual } from "assert";
import fs from "fs";
import os from "os";
import path from "path";
+import {
+ AmlOutcome,
+ codecForAmlProgramInput,
+} from "../../taler-util/src/types-taler-aml.js";
import { runBench1 } from "./bench1.js";
import { runBench2 } from "./bench2.js";
import { runBench3 } from "./bench3.js";
import { runEnvFull } from "./env-full.js";
import { runEnv1 } from "./env1.js";
import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+} from "./harness/environments.js";
+import {
GlobalTestState,
WalletClient,
delayMs,
runTestWithState,
} from "./harness/harness.js";
-import {
- createSimpleTestkudosEnvironmentV2,
- createWalletDaemonWithClient,
-} from "./harness/environments.js";
import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
import { lintExchangeDeployment } from "./lint.js";
@@ -1421,6 +1426,59 @@ testingCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => {
console.log("check passed!");
});
+export const amlProgramCli = testingCli.subcommand(
+ "amlProgram",
+ "aml-program",
+ {
+ help: "Command line interface for the GNU Taler test/deployment harness.",
+ },
+);
+
+amlProgramCli
+ .subcommand("noRules", "no-rules")
+ .flag("requires", ["-r"])
+ .flag("attributes", ["-a"])
+ .maybeOption("config", ["-c", "--config"], clk.STRING)
+ .action(async (args) => {
+ logger.info("Hello, this is the no-rules AML program.");
+ if (args.noRules.requires) {
+ logger.info("Reporting requirements");
+ // No requirements.
+ return;
+ }
+
+ if (args.noRules.attributes) {
+ logger.info("reporting attributes");
+ // No attributes
+ return;
+ }
+
+ const buffers = [];
+
+ // node.js readable streams implement the async iterator protocol
+ for await (const data of process.stdin) {
+ buffers.push(data);
+ }
+
+ const finalBuffer = Buffer.concat(buffers);
+ const inputStr = finalBuffer.toString("utf-8");
+ const inputJson = JSON.parse(inputStr);
+ const progInput = codecForAmlProgramInput().decode(inputJson);
+
+ logger.info(`got input: ${j2s(progInput)}`);
+
+ const outcome: AmlOutcome = {
+ new_rules: {
+ expiration_time: TalerProtocolTimestamp.never(),
+ rules: [],
+ custom_measures: {},
+ },
+ events: [],
+ };
+
+ console.log(j2s(outcome));
+ });
+
export function main() {
testingCli.run();
}
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts b/packages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (C) 2024 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
@@ -66,14 +66,10 @@ function adjustExchangeConfig(config: Configuration) {
config.setString(
"AML-PROGRAM-P1",
"command",
- "taler-exchange-helper-measure-test-form",
+ "taler-harness aml-program no-rules",
);
config.setString("AML-PROGRAM-P1", "enabled", "true");
- config.setString(
- "AML-PROGRAM-P1",
- "description",
- "test for full_name and birthdate",
- );
+ config.setString("AML-PROGRAM-P1", "description", "remove all rules");
config.setString("AML-PROGRAM-P1", "description_i18n", "{}");
config.setString("AML-PROGRAM-P1", "fallback", "M1");
diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts
@@ -83,6 +83,7 @@ interface Alternative {
class ObjectCodecBuilder<OutputType, PartialOutputType> {
private propList: Prop[] = [];
+ private _allowExtra: boolean = false;
/**
* Define a property for the object.
@@ -98,6 +99,11 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> {
return this as any;
}
+ allowExtra(): ObjectCodecBuilder<OutputType, PartialOutputType> {
+ this._allowExtra = true;
+ return this;
+ }
+
/**
* Return the built codec.
*
@@ -106,6 +112,7 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> {
*/
build(objectDisplayName: string): Codec<PartialOutputType> {
const propList = this.propList;
+ const allowExtra = this._allowExtra;
return {
decode(x: any, c?: Context): PartialOutputType {
if (!c) {
@@ -129,6 +136,20 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> {
);
obj[prop.name] = propVal;
}
+ for (const prop in x) {
+ if (prop in obj) {
+ continue;
+ }
+ if (allowExtra) {
+ obj[prop] = x[prop];
+ } else {
+ logger.warn(
+ `Extra property ${prop} for ${objectDisplayName} at ${renderContext(
+ c,
+ )}`,
+ );
+ }
+ }
return obj as PartialOutputType;
},
};
@@ -146,7 +167,7 @@ class UnionCodecBuilder<
constructor(
private discriminator: TagPropertyLabel,
private baseCodec?: Codec<CommonBaseType>,
- ) { }
+ ) {}
/**
* Define a property for the object.
@@ -491,7 +512,10 @@ export function codecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> {
};
}
-export function codecOptionalDefault<V>(innerCodec: Codec<V>, def: V): Codec<V> {
+export function codecOptionalDefault<V>(
+ innerCodec: Codec<V>,
+ def: V,
+): Codec<V> {
return {
decode(x: any, c?: Context): V {
if (x === undefined || x === null) {
@@ -503,18 +527,17 @@ export function codecOptionalDefault<V>(innerCodec: Codec<V>, def: V): Codec<V>
}
export function codecForLazy<V>(innerCodec: () => Codec<V>): Codec<V> {
- let instance: Codec<V> | undefined = undefined
+ let instance: Codec<V> | undefined = undefined;
return {
decode(x: any, c?: Context): V {
if (instance === undefined) {
- instance = innerCodec()
+ instance = innerCodec();
}
return instance.decode(x, c);
},
};
}
-
export type CodecType<T> = T extends Codec<infer X> ? X : any;
export function codecForEither<T extends Array<Codec<unknown>>>(
diff --git a/packages/taler-util/src/types-taler-aml.ts b/packages/taler-util/src/types-taler-aml.ts
@@ -0,0 +1,162 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/**
+ * @fileoverview Type and schema definitions and helpers for the Taler AML helpers.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ buildCodecForObject,
+ Codec,
+ codecForAny,
+ codecForList,
+} from "./index.js";
+import { TalerProtocolTimestamp } from "./time.js";
+import {
+ AccountProperties,
+ codecForAccountProperties,
+ LegitimizationRuleSet,
+} from "./types-taler-exchange.js";
+
+export interface AmlProgramInput {
+ // JSON object that was provided as
+ // part of the *measure*. This JSON object is
+ // provided under "context" in the main JSON object
+ // input to the AML program. This "context" should
+ // satify both the REQUIRES clause of the respective
+ // check and the output of "-r" from the
+ // AML program's command-line option.
+ context?: Record<string, unknown>;
+
+ // JSON object that captures the
+ // output of a ``[kyc-provider-]`` or (HTML) FORM.
+ // In the case of KYC data provided by providers,
+ // the keys in the JSON object will be the attribute
+ // names and the values must be strings representing
+ // the data. In the case of file uploads, the data
+ // MUST be base64-encoded.
+ // In the case of KYC data provided by HTML FORMs, the
+ // keys will match the HTML FORM field names and
+ // the values will use the `KycStructuredFormData`
+ // encoding.
+ attributes: Record<string, unknown>;
+
+ // JSON array with the results of historic
+ // AML desisions about the account.
+ aml_history: AmlHistoryEntry[];
+
+ // JSON array with the results of historic
+ // KYC data about the account.
+ kyc_history: KycHistoryEntry[];
+}
+
+export interface AmlHistoryEntry {
+ // When was the AML decision taken.
+ decision_time: TalerProtocolTimestamp;
+
+ // What was the justification given for the decision.
+ justification: string;
+
+ // Public key of the AML officer taking the decision.
+ decider_pub: string;
+
+ // Properties associated with the account by the decision.
+ properties: Object;
+
+ // New set of legitimization rules that was put in place.
+ new_rules: LegitimizationRuleSet;
+
+ // True if the account was flagged for (further)
+ // investigation.
+ to_investigate: boolean;
+
+ // True if this is the currently active decision.
+ is_active: boolean;
+}
+
+export interface KycHistoryEntry {
+ // Name of the provider
+ // which was used to collect the attributes. NULL if they were
+ // just uploaded via a form by the account owner.
+ provider_name?: string;
+
+ // True if the KYC process completed.
+ finished: boolean;
+
+ // Numeric `error code <error-codes>`, if the
+ // KYC process did not succeed; 0 on success.
+ code: number;
+
+ // Human-readable description of ``code``. Optional.
+ hint?: string;
+
+ // Optional detail given when the KYC process failed.
+ error_message?: string;
+
+ // Identifier of the user at the KYC provider. Optional.
+ provider_user_id?: string;
+
+ // Identifier of the KYC process at the KYC provider. Optional.
+ provider_legitimization_id?: string;
+
+ // The collected KYC data.
+ // NULL if the attribute data could not
+ // be decrypted or was not yet collected.
+ attributes?: Record<string, unknown>;
+
+ // Time when the KYC data was collected
+ collection_time: TalerProtocolTimestamp;
+
+ // Time when the KYC data will expire.
+ expiration_time: TalerProtocolTimestamp;
+}
+
+export interface AmlOutcome {
+ // Should the client's account be investigated
+ // by AML staff?
+ // Defaults to false.
+ to_investigate?: boolean;
+
+ // Free-form properties about the account.
+ // Can be used to store properties such as PEP,
+ // risk category, type of business, hits on
+ // sanctions lists, etc.
+ properties?: AccountProperties;
+
+ // Types of events to add to the KYC events table.
+ // (for statistics).
+ events?: string[];
+
+ // KYC rules to apply. Note that this
+ // overrides *all* of the default rules
+ // until the ``expiration_time`` and specifies
+ // the successor measure to apply after the
+ // expiration time.
+ new_rules: LegitimizationRuleSet;
+}
+
+export const codecForAmlProgramInput = (): Codec<AmlProgramInput> =>
+ buildCodecForObject<AmlProgramInput>()
+ .property("aml_history", codecForList(codecForAny()))
+ .property("kyc_history", codecForList(codecForAny()))
+ .property("attributes", codecForAccountProperties())
+ .property("context", codecForAny())
+ .build("AmlProgramInput");
diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts
@@ -892,8 +892,14 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
.property("global_fees", codecForList(codecForGlobalFees()))
.property("accounts", codecForList(codecForExchangeWireAccount()))
.property("wire_fees", codecForMap(codecForList(codecForWireFeesJson())))
- .property("zero_limits", codecOptional(codecForList(codecForZeroLimitedOperation())))
- .property("hard_limits", codecOptional(codecForList(codecForAccountLimit())))
+ .property(
+ "zero_limits",
+ codecOptional(codecForList(codecForZeroLimitedOperation())),
+ )
+ .property(
+ "hard_limits",
+ codecOptional(codecForList(codecForAccountLimit())),
+ )
.property("denominations", codecForList(codecForNgDenominations))
.property(
"wallet_balance_limit_without_kyc",
@@ -1705,7 +1711,15 @@ export interface AccountKycStatus {
limits?: AccountLimit[];
}
-export type LimitOperationType = "WITHDRAW" | "DEPOSIT" | "MERGE" | "AGGREGATE" | "BALANCE" | "REFUND" | "CLOSE" | "TRANSACTION";
+export type LimitOperationType =
+ | "WITHDRAW"
+ | "DEPOSIT"
+ | "MERGE"
+ | "AGGREGATE"
+ | "BALANCE"
+ | "REFUND"
+ | "CLOSE"
+ | "TRANSACTION";
export interface AccountLimit {
// Operation that is limited.
@@ -2453,6 +2467,7 @@ export const codecForAccountProperties = (): Codec<AccountProperties> =>
.property("business_domain", codecOptional(codecForString()))
.property("is_frozen", codecOptional(codecForBoolean()))
.property("was_reported", codecOptional(codecForBoolean()))
+ .allowExtra()
.build("TalerExchangeApi.AccountProperties");
export const codecForLegitimizationRuleSet = (): Codec<LegitimizationRuleSet> =>
@@ -2526,10 +2541,7 @@ export const codecForOperationType = codecForEither(
export const codecForAccountLimit = (): Codec<AccountLimit> =>
buildCodecForObject<AccountLimit>()
- .property(
- "operation_type",
- codecForOperationType,
- )
+ .property("operation_type", codecForOperationType)
.property("timeframe", codecForDuration)
.property("threshold", codecForAmountString())
.property("soft_limit", codecOptional(codecForBoolean()))
@@ -2537,10 +2549,7 @@ export const codecForAccountLimit = (): Codec<AccountLimit> =>
export const codecForZeroLimitedOperation = (): Codec<ZeroLimitedOperation> =>
buildCodecForObject<ZeroLimitedOperation>()
- .property(
- "operation_type",
- codecForOperationType
- )
+ .property("operation_type", codecForOperationType)
.build("TalerExchangeApi.ZeroLimitedOperation");
export const codecForKycCheckPublicInformation =