exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 2d3b6763b1ecb75faaa6622765c19a082113c3f3
parent 749aec3207ec3d8b203b3f0d6720a1e1354c2d04
Author: Florian Dold <dold@taler.net>
Date:   Mon, 31 Aug 2026 11:34:46 +0200

exchange: expose complete AML decision audit details

Diffstat:
Msrc/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-decisions.c | 74++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/exchangedb/iterate_aml_decisions.c | 48++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/exchangedb/test_aml_history.c | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/include/exchange-database/iterate_aml_decisions.h | 16++++++++++++++++
Msrc/include/taler/exchange/get-aml-OFFICER_PUB-decisions.h | 22++++++++++++++++++++++
Msrc/lib/exchange_api_get-aml-OFFICER_PUB-decisions.c | 16++++++++++++++++
6 files changed, 289 insertions(+), 30 deletions(-)

diff --git a/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-decisions.c b/src/exchange/taler-exchange-httpd_get-aml-OFFICER_PUB-decisions.c @@ -41,6 +41,10 @@ * @param records closure * @param row_id current row in AML status table * @param justification human-readable reason for the decision + * @param new_measures names of measures requested immediately + * @param decider_pub public key of the deciding officer, NULL for automated outcomes + * @param decider_name name of the deciding officer at decision time + * @param kyc_attributes_rowid row of submitted attributes linked to the decision * @param h_payto account for which the attribute data is stored * @param decision_time when was the decision taken * @param expiration_time when will the rules expire @@ -57,6 +61,10 @@ record_cb ( json_t *records, uint64_t row_id, const char *justification, + const char *new_measures, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + const char *decider_name, + const uint64_t *kyc_attributes_rowid, const struct TALER_NormalizedPaytoHashP *h_payto, struct GNUNET_TIME_Timestamp decision_time, struct GNUNET_TIME_Absolute expiration_time, @@ -67,34 +75,52 @@ record_cb ( struct TALER_FullPayto payto, const json_t *account_rules) { + json_t *record; + + record = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_payto", + h_payto), + TALER_JSON_pack_full_payto ("full_payto", + payto), + GNUNET_JSON_pack_bool ("is_wallet", + is_wallet), + GNUNET_JSON_pack_uint64 ("rowid", + row_id), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("justification", + justification)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("new_measures", + new_measures)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("decider_pub", + decider_pub)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("decider_name", + decider_name)), + GNUNET_JSON_pack_timestamp ("decision_time", + decision_time), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("properties", + (json_t *) jproperties)), + GNUNET_JSON_pack_object_incref ("limits", + (json_t *) account_rules), + GNUNET_JSON_pack_bool ("to_investigate", + to_investigate), + GNUNET_JSON_pack_bool ("is_active", + is_active)); + GNUNET_assert (NULL != record); + if (NULL != kyc_attributes_rowid) + GNUNET_assert (0 == + json_object_set_new ( + record, + "kyc_attributes_rowid", + json_integer ((json_int_t) *kyc_attributes_rowid))); GNUNET_assert ( 0 == json_array_append_new ( records, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("h_payto", - h_payto), - TALER_JSON_pack_full_payto ("full_payto", - payto), - GNUNET_JSON_pack_bool ("is_wallet", - is_wallet), - GNUNET_JSON_pack_uint64 ("rowid", - row_id), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("justification", - justification)), - GNUNET_JSON_pack_timestamp ("decision_time", - decision_time), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("properties", - (json_t *) jproperties)), - GNUNET_JSON_pack_object_incref ("limits", - (json_t *) account_rules), - GNUNET_JSON_pack_bool ("to_investigate", - to_investigate), - GNUNET_JSON_pack_bool ("is_active", - is_active) - ))); + record)); } diff --git a/src/exchangedb/iterate_aml_decisions.c b/src/exchangedb/iterate_aml_decisions.c @@ -78,7 +78,13 @@ handle_aml_result (void *cls, { struct TALER_NormalizedPaytoHashP h_payto; uint64_t rowid; + uint64_t kyc_attributes_rowid; char *justification = NULL; + char *new_measures = NULL; + struct TALER_AmlOfficerPublicKeyP decider_pub; + char *decider_name = NULL; + bool no_decider_pub; + bool no_kyc_attributes_rowid; struct GNUNET_TIME_Timestamp decision_time; struct GNUNET_TIME_Absolute expiration_time; json_t *jproperties = NULL; @@ -99,6 +105,22 @@ handle_aml_result (void *cls, GNUNET_PQ_result_spec_string ("justification", &justification), NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("new_measure_name", + &new_measures), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("decider_pub", + &decider_pub), + &no_decider_pub), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("decider_name", + &decider_name), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint64 ("kyc_attributes_serial_id", + &kyc_attributes_rowid), + &no_kyc_attributes_rowid), GNUNET_PQ_result_spec_timestamp ("decision_time", &decision_time), GNUNET_PQ_result_spec_absolute_time ("expiration_time", @@ -149,6 +171,10 @@ handle_aml_result (void *cls, ctx->cb (ctx->cb_cls, rowid, justification, + new_measures, + no_decider_pub ? NULL : &decider_pub, + decider_name, + no_kyc_attributes_rowid ? NULL : &kyc_attributes_rowid, &h_payto, decision_time, expiration_time, @@ -211,6 +237,10 @@ TALER_EXCHANGEDB_iterate_aml_decisions ( " lo.outcome_serial_id" ",lo.h_payto" ",ah.justification" + ",lo.new_measure_name" + ",ah.decider_pub" + ",astaff.decider_name" + ",ah.kyc_attributes_serial_id" ",lo.decision_time" ",lo.expiration_time" ",lo.jproperties::TEXT" @@ -226,6 +256,13 @@ TALER_EXCHANGEDB_iterate_aml_decisions ( " ON (lo.h_payto = wt.h_normalized_payto)" " LEFT JOIN aml_history ah" " USING (outcome_serial_id)" + " LEFT JOIN LATERAL" + " (SELECT s.decider_name" + " FROM aml_staff s" + " WHERE (s.decider_pub = ah.decider_pub)" + " AND (s.last_change <= lo.decision_time)" + " ORDER BY s.last_change DESC" + " LIMIT 1) astaff ON TRUE" " WHERE (outcome_serial_id > $7)" " AND ($1 OR (lo.h_payto = $2))" " AND ($3 OR (lo.to_investigate = $4))" @@ -238,6 +275,10 @@ TALER_EXCHANGEDB_iterate_aml_decisions ( " lo.outcome_serial_id" ",lo.h_payto" ",ah.justification" + ",lo.new_measure_name" + ",ah.decider_pub" + ",astaff.decider_name" + ",ah.kyc_attributes_serial_id" ",lo.decision_time" ",lo.expiration_time" ",lo.jproperties::TEXT" @@ -253,6 +294,13 @@ TALER_EXCHANGEDB_iterate_aml_decisions ( " ON (lo.h_payto = wt.h_normalized_payto)" " LEFT JOIN aml_history ah" " USING (outcome_serial_id)" + " LEFT JOIN LATERAL" + " (SELECT s.decider_name" + " FROM aml_staff s" + " WHERE (s.decider_pub = ah.decider_pub)" + " AND (s.last_change <= lo.decision_time)" + " ORDER BY s.last_change DESC" + " LIMIT 1) astaff ON TRUE" " WHERE lo.outcome_serial_id < $7" " AND ($1 OR (lo.h_payto = $2))" " AND ($3 OR (lo.to_investigate = $4))" diff --git a/src/exchangedb/test_aml_history.c b/src/exchangedb/test_aml_history.c @@ -123,6 +123,21 @@ struct HistoryContext * Whether the last row named an officer. */ bool have_officer; + + /** + * Whether the last decision exposed the officer's historical name. + */ + char *officer_name; + + /** + * Whether the last decision linked submitted attributes. + */ + bool have_attributes; + + /** + * Immediate measures of the last decision, owned by this struct. + */ + char *new_measures; }; @@ -133,6 +148,10 @@ struct HistoryContext * @param outcome_serial_id row of the outcome the decision produced * @param decision_time when the decision was taken * @param justification why it was taken + * @param new_measures measures requested immediately + * @param decider_pub officer who made the decision + * @param decider_name officer name at decision time + * @param kyc_attributes_rowid linked submitted attributes * @param decider_pub which officer took it * @param jproperties new account properties * @param jnew_rules new account rules @@ -187,6 +206,10 @@ static void decision_cb (void *cls, uint64_t row_id, const char *justification, + const char *new_measures, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + const char *decider_name, + const uint64_t *kyc_attributes_rowid, const struct TALER_NormalizedPaytoHashP *h_payto, struct GNUNET_TIME_Timestamp decision_time, struct GNUNET_TIME_Absolute expiration_time, @@ -212,8 +235,18 @@ decision_cb (void *cls, ctx->justification = (NULL == justification) ? NULL : GNUNET_strdup (justification); + GNUNET_free (ctx->new_measures); + ctx->new_measures = (NULL == new_measures) + ? NULL + : GNUNET_strdup (new_measures); ctx->to_investigate = to_investigate; ctx->is_active = is_active; + ctx->have_officer = (NULL != decider_pub); + GNUNET_free (ctx->officer_name); + ctx->officer_name = (NULL == decider_name) + ? NULL + : GNUNET_strdup (decider_name); + ctx->have_attributes = (NULL != kyc_attributes_rowid); } @@ -316,6 +349,8 @@ struct DecisionStatus * @param seed seed for the officer's signature * @param when when the decision is taken, in seconds since the epoch * @param justification why + * @param new_measures measures to request immediately + * @param add_attributes whether to link submitted attributes * @param to_investigate whether staff should investigate * @param[out] st set to the outcome * @return transaction status @@ -326,10 +361,14 @@ decide (struct TALER_EXCHANGEDB_PostgresContext *pg, uint32_t seed, uint64_t when, const char *justification, + const char *new_measures, + bool add_attributes, bool to_investigate, struct DecisionStatus *st) { struct TALER_AmlOfficerSignatureP decider_sig; + struct GNUNET_HashCode attributes_hash; + const char encrypted_attributes[] = "encrypted attributes"; struct TALER_FullPayto null_payto = { NULL }; json_t *new_rules = make_rules ("decided"); /* the argument is declared `const char *[static 0]', so it must not @@ -340,6 +379,9 @@ decide (struct TALER_EXCHANGEDB_PostgresContext *pg, TDB_fill (&decider_sig, sizeof (decider_sig), seed); + TDB_fill (&attributes_hash, + sizeof (attributes_hash), + seed); memset (st, 0, sizeof (*st)); @@ -352,18 +394,18 @@ decide (struct TALER_EXCHANGEDB_PostgresContext *pg, NULL, new_rules, to_investigate, - NULL, + new_measures, NULL, justification, decider, (NULL == decider) ? NULL : &decider_sig, 0, no_events, - NULL, - 0, - NULL, - NULL, - GNUNET_TIME_UNIT_ZERO_TS, + add_attributes ? "test-form" : NULL, + add_attributes ? sizeof (encrypted_attributes) : 0, + add_attributes ? encrypted_attributes : NULL, + add_attributes ? &attributes_hash : NULL, + add_attributes ? ts (when + 3600) : GNUNET_TIME_UNIT_ZERO_TS, &st->invalid_officer, &st->unknown_account, &st->last_date, @@ -434,6 +476,8 @@ check_invalid_officer (struct TALER_EXCHANGEDB_PostgresContext *pg) 20, 1600000000, "no such officer", + NULL, + false, false, &st)); FAILIF (! st.invalid_officer); @@ -475,6 +519,8 @@ check_decide (struct TALER_EXCHANGEDB_PostgresContext *pg) 20, 1600000000, "looks fine", + "review-a", + false, false, &st)); FAILIF (st.invalid_officer); @@ -529,6 +575,8 @@ check_supersede (struct TALER_EXCHANGEDB_PostgresContext *pg) 21, 1500000000, "too late", + NULL, + false, false, &st)); FAILIF (GNUNET_TIME_timestamp_cmp (st.last_date, @@ -544,6 +592,8 @@ check_supersede (struct TALER_EXCHANGEDB_PostgresContext *pg) 22, 1600003600, "investigate this", + "+review-a review-b", + true, true, &st)); FAILIF (2 != TDB_count (pg, @@ -580,8 +630,23 @@ check_supersede (struct TALER_EXCHANGEDB_PostgresContext *pg) static int check_decisions (struct TALER_EXCHANGEDB_PostgresContext *pg) { + struct TALER_MasterSignatureP master_sig; + struct GNUNET_TIME_Timestamp previous_change; struct HistoryContext ctx; + /* A later rename must not rewrite the name shown on older decisions. */ + TDB_FILL (master_sig, + 30); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_insert_aml_officer (pg, + &officer_pub, + &master_sig, + "Alex Renamed", + true, + false, + ts (1700000000), + &previous_change)); + memset (&ctx, 0, sizeof (ctx)); @@ -596,7 +661,23 @@ check_decisions (struct TALER_EXCHANGEDB_PostgresContext *pg) &ctx)); FAILIF_C (2 != ctx.total, GNUNET_free (ctx.justification)); + FAILIF_C (NULL == ctx.new_measures, + GNUNET_free (ctx.justification)); + FAILIF_C (0 != strcmp (ctx.new_measures, + "+review-a review-b"), + GNUNET_free (ctx.justification)); + FAILIF_C (NULL == ctx.officer_name, + GNUNET_free (ctx.justification)); + FAILIF_C (0 != strcmp (ctx.officer_name, + "Alex Officer"), + GNUNET_free (ctx.justification)); + FAILIF_C (! ctx.have_officer, + GNUNET_free (ctx.justification)); + FAILIF_C (! ctx.have_attributes, + GNUNET_free (ctx.justification)); GNUNET_free (ctx.justification); + GNUNET_free (ctx.new_measures); + GNUNET_free (ctx.officer_name); /* only the newer decision asked for an investigation */ memset (&ctx, @@ -617,6 +698,8 @@ check_decisions (struct TALER_EXCHANGEDB_PostgresContext *pg) "investigate this"), GNUNET_free (ctx.justification)); GNUNET_free (ctx.justification); + GNUNET_free (ctx.new_measures); + GNUNET_free (ctx.officer_name); /* and only one of them is the active outcome */ memset (&ctx, @@ -636,6 +719,8 @@ check_decisions (struct TALER_EXCHANGEDB_PostgresContext *pg) FAILIF_C (! ctx.is_active, GNUNET_free (ctx.justification)); GNUNET_free (ctx.justification); + GNUNET_free (ctx.new_measures); + GNUNET_free (ctx.officer_name); /* an account nobody decided about has nothing */ { @@ -748,6 +833,50 @@ check_history_builder (struct TALER_EXCHANGEDB_PostgresContext *pg) /** + * Automated outcomes have no AML officer provenance. + * + * @param pg the database context + * @return 0 on success + */ +static int +check_automated_outcome (struct TALER_EXCHANGEDB_PostgresContext *pg) +{ + struct DecisionStatus st; + struct HistoryContext ctx = { 0 }; + + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + decide (pg, + NULL, + 40, + 1800000000, + NULL, + NULL, + false, + false, + &st)); + FAILIF (0 >= + TALER_EXCHANGEDB_iterate_aml_decisions (pg, + &account.h_normalized, + TALER_EXCHANGE_YNA_ALL, + TALER_EXCHANGE_YNA_YES, + 0, + 10, + &decision_cb, + &ctx)); + FAILIF_C (1 != ctx.total, + GNUNET_free (ctx.justification)); + FAILIF_C (ctx.have_officer, + GNUNET_free (ctx.justification)); + FAILIF_C (NULL != ctx.officer_name, + GNUNET_free (ctx.justification)); + GNUNET_free (ctx.justification); + GNUNET_free (ctx.new_measures); + GNUNET_free (ctx.officer_name); + return 0; +} + + +/** * The checks to run, in order. */ static const struct TDB_Test tests[] = { @@ -765,6 +894,8 @@ static const struct TDB_Test tests[] = { &check_serial }, { "aml-history-history-builder", &check_history_builder }, + { "aml-history-automated-outcome", + &check_automated_outcome }, { NULL, NULL } }; diff --git a/src/include/exchange-database/iterate_aml_decisions.h b/src/include/exchange-database/iterate_aml_decisions.h @@ -32,6 +32,10 @@ * @param cls closure * @param row_id current row in legitimization outcomes table * @param justification human-readable reason for the decision, NULL if none is available + * @param new_measures names of measures requested immediately, NULL if none + * @param decider_pub public key of the deciding AML officer, NULL for automated outcomes + * @param decider_name name of the deciding AML officer at decision time, NULL if unavailable + * @param kyc_attributes_rowid row of submitted attributes linked to the decision, NULL if none * @param h_payto account for which the attribute data is stored * @param decision_time when was the decision taken * @param expiration_time when will the rules expire @@ -54,6 +58,10 @@ typedef void TALER_EXCHANGEDB_AML_DECISION_RESULT_CLOSURE *cls, uint64_t row_id, const char *justification, + const char *new_measures, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + const char *decider_name, + const uint64_t *kyc_attributes_rowid, const struct TALER_NormalizedPaytoHashP *h_payto, struct GNUNET_TIME_Timestamp decision_time, struct GNUNET_TIME_Absolute expiration_time, @@ -72,6 +80,10 @@ typedef void * @param cls closure * @param row_id current row in legitimization outcomes table * @param justification human-readable reason for the decision, NULL if none is available + * @param new_measures names of measures requested immediately, NULL if none + * @param decider_pub public key of the deciding AML officer, NULL for automated outcomes + * @param decider_name name of the deciding AML officer at decision time, NULL if unavailable + * @param kyc_attributes_rowid row of submitted attributes linked to the decision, NULL if none * @param h_payto account for which the attribute data is stored * @param decision_time when was the decision taken * @param expiration_time when will the rules expire @@ -88,6 +100,10 @@ typedef void TALER_EXCHANGEDB_AML_DECISION_RESULT_CLOSURE *cls, uint64_t row_id, const char *justification, + const char *new_measures, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + const char *decider_name, + const uint64_t *kyc_attributes_rowid, const struct TALER_NormalizedPaytoHashP *h_payto, struct GNUNET_TIME_Timestamp decision_time, struct GNUNET_TIME_Absolute expiration_time, diff --git a/src/include/taler/exchange/get-aml-OFFICER_PUB-decisions.h b/src/include/taler/exchange/get-aml-OFFICER_PUB-decisions.h @@ -246,6 +246,28 @@ struct TALER_EXCHANGE_GetAmlDecisionsDecision const char *justification; /** + * Space-separated names of measures requested immediately. A leading + * plus sign means all listed measures are required. Can be NULL. + */ + const char *new_measures; + + /** + * Public key of the AML officer who made the decision. All zeros for an + * automated outcome. + */ + struct TALER_AmlOfficerPublicKeyP decider_pub; + + /** + * Name of the AML officer at the time of the decision. Can be NULL. + */ + const char *decider_name; + + /** + * Row ID of submitted attributes linked to this decision. Zero if none. + */ + uint64_t kyc_attributes_rowid; + + /** * When was the decision made? */ struct GNUNET_TIME_Timestamp decision_time; diff --git a/src/lib/exchange_api_get-aml-OFFICER_PUB-decisions.c b/src/lib/exchange_api_get-aml-OFFICER_PUB-decisions.c @@ -301,6 +301,22 @@ parse_aml_decisions ( GNUNET_JSON_spec_string ("justification", &decision->justification), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("new_measures", + &decision->new_measures), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("decider_pub", + &decision->decider_pub), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("decider_name", + &decision->decider_name), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("kyc_attributes_rowid", + &decision->kyc_attributes_rowid), + NULL), GNUNET_JSON_spec_timestamp ("decision_time", &decision->decision_time), GNUNET_JSON_spec_mark_optional (