From 915542e69c5a481b8885661171880446d4ef009d Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Thu, 2 Feb 2023 11:40:44 +0100 Subject: [PATCH] first draft of implementation of GET AML decisions endpoint --- src/exchange/Makefile.am | 1 + src/exchange/taler-exchange-httpd.c | 6 +- .../taler-exchange-httpd_aml-decision.h | 18 +- .../taler-exchange-httpd_aml-decisions-get.c | 228 ++++++++++++++++++ src/exchangedb/pg_select_aml_process.c | 8 +- src/exchangedb/pg_select_aml_process.h | 2 + src/include/taler_exchange_service.h | 10 +- src/include/taler_exchangedb_plugin.h | 1 + src/include/taler_util.h | 8 +- src/lib/Makefile.am | 2 + src/lib/exchange_api_lookup_aml_decision.c | 45 +++- src/lib/exchange_api_lookup_aml_decisions.c | 49 +++- 12 files changed, 346 insertions(+), 32 deletions(-) create mode 100644 src/exchange/taler-exchange-httpd_aml-decisions-get.c diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am index ffcfc5e99..bf6b1b1a9 100644 --- a/src/exchange/Makefile.am +++ b/src/exchange/Makefile.am @@ -124,6 +124,7 @@ taler_exchange_httpd_SOURCES = \ taler-exchange-httpd.c taler-exchange-httpd.h \ taler-exchange-httpd_auditors.c taler-exchange-httpd_auditors.h \ taler-exchange-httpd_aml-decision.c taler-exchange-httpd_aml-decision.h \ + taler-exchange-httpd_aml-decisions-get.c \ taler-exchange-httpd_batch-deposit.c taler-exchange-httpd_batch-deposit.h \ taler-exchange-httpd_batch-withdraw.c taler-exchange-httpd_batch-withdraw.h \ taler-exchange-httpd_common_deposit.c taler-exchange-httpd_common_deposit.h \ diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c index 7f49955dc..11b2d35fa 100644 --- a/src/exchange/taler-exchange-httpd.c +++ b/src/exchange/taler-exchange-httpd.c @@ -446,14 +446,14 @@ handle_get_aml (struct TEH_RequestContext *rc, AmlOpGetHandler handler; } h[] = { -#if FIXME_AML_GET_DECISIONS_NOT_IMPLEMENTED { .op = "decisions", - .handler = &TEH_handler_get_aml_decisions + .handler = &TEH_handler_aml_decisions_get }, +#if FIXME_AML_GET_DECISIONS_NOT_IMPLEMENTED { .op = "decision", - .handler = &TEH_handler_get_aml_decision + .handler = &TEH_handler_aml_decision_get }, #endif { diff --git a/src/exchange/taler-exchange-httpd_aml-decision.h b/src/exchange/taler-exchange-httpd_aml-decision.h index 8dd3bb3fc..e31cfdfad 100644 --- a/src/exchange/taler-exchange-httpd_aml-decision.h +++ b/src/exchange/taler-exchange-httpd_aml-decision.h @@ -26,7 +26,7 @@ /** - * Handle an "/aml/$OFFICER_PUB/decision" request. Parses the decision + * Handle a POST "/aml/$OFFICER_PUB/decision" request. Parses the decision * details, checks the signatures and if appropriately authorized executes * the decision. * @@ -42,4 +42,20 @@ TEH_handler_post_aml_decision ( const json_t *root); +/** + * Handle a GET "/aml/$OFFICER_PUB/decisions" request. Parses the request + * details, checks the signatures and if appropriately authorized returns + * the matching decisions. + * + * @param rc request context + * @param officer_pub public key of the AML officer who made the request + * @param args GET arguments (should be none) + * @return MHD result code + */ +MHD_RESULT +TEH_handler_aml_decisions_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]); + #endif diff --git a/src/exchange/taler-exchange-httpd_aml-decisions-get.c b/src/exchange/taler-exchange-httpd_aml-decisions-get.c new file mode 100644 index 000000000..091e7a674 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_aml-decisions-get.c @@ -0,0 +1,228 @@ +/* + This file is part of TALER + Copyright (C) 2023 Taler Systems SA + + 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. + + 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file taler-exchange-httpd_aml-decisions-get.c + * @brief Return summary information about AML decisions + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler_signatures.h" +#include "taler-exchange-httpd.h" +#include "taler_exchangedb_plugin.h" +#include "taler-exchange-httpd_aml-decision.h" +#include "taler-exchange-httpd_metrics.h" + + +/** + * Maximum number of records we return per request. + */ +#define MAX_RECORDS 1024 + +/** + * Return AML status. + * + * @param cls closure + * @param row_id current row in AML status table + * @param h_payto account for which the attribute data is stored + * @param threshold currently monthly threshold that would trigger an AML check + * @param status what is the current AML decision + */ +static void +record_cb ( + void *cls, + uint64_t row_id, + const struct TALER_PaytoHashP *h_payto, + const struct TALER_Amount *threshold, + enum TALER_AmlDecisionState status) +{ + json_t *records = cls; + + GNUNET_assert ( + 0 == + json_array_append ( + records, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("h_payto", + h_payto), + TALER_JSON_pack_amount ("threshold", + threshold), + GNUNET_JSON_pack_int64 ("current_state", + status), + GNUNET_JSON_pack_int64 ("rowid", + row_id) + ))); +} + + +MHD_RESULT +TEH_handler_aml_decisions_get ( + struct TEH_RequestContext *rc, + const struct TALER_AmlOfficerPublicKeyP *officer_pub, + const char *const args[]) +{ + enum GNUNET_GenericReturnValue res; + const char *sig_hdr; + struct TALER_AmlOfficerSignatureP officer_sig; + bool frozen = false; + bool pending = false; + bool normal = false; + int delta = -20; + unsigned long long start = INT64_MAX; + + if (NULL != args[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + args[0]); + } + sig_hdr = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + TALER_AML_OFFICER_SIGNATURE_HEADER); + if ( (NULL == sig_hdr) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (sig_hdr, + strlen (sig_hdr), + &officer_sig, + sizeof (officer_sig))) || + (GNUNET_OK != + TALER_officer_aml_query_verify (officer_pub, + &officer_sig)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_AML_OFFICER_GET_SIGNATURE_INVALID, + sig_hdr); + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + + { + const char *p; + + p = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "frozen"); + if (NULL != p) + frozen = (0 == strcasecmp (p, + "yes")); + p = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "pending"); + if (NULL != p) + pending = (0 == strcasecmp (p, + "yes")); + p = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "normal"); + if (NULL != p) + normal = (0 == strcasecmp (p, + "yes")); + p = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "start"); + if (NULL != p) + { + char dummy; + + if (1 != sscanf (p, + "%llu%c", + &start, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "start"); + } + } + p = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "delta"); + if (NULL != p) + { + char dummy; + + if (1 != sscanf (p, + "%d%c", + &delta, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "delta"); + } + } + } + + { + json_t *records; + enum GNUNET_DB_QueryStatus qs; + enum TALER_AmlDecisionState decision = 42; // FIXME! + + records = json_array (); + GNUNET_assert (NULL != records); + if (INT_MIN == delta) + delta = INT_MIN + 1; + qs = TEH_plugin->select_aml_process (TEH_plugin->cls, + decision, + start, + delta > 0, + GNUNET_MIN (MAX_RECORDS, + delta > 0 ? delta : + -delta), + &record_cb, + records); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + json_decref (records); + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("records", + records)); + } +} + + +/* end of taler-exchange-httpd_deposits_get.c */ diff --git a/src/exchangedb/pg_select_aml_process.c b/src/exchangedb/pg_select_aml_process.c index 2105308fc..c165e230b 100644 --- a/src/exchangedb/pg_select_aml_process.c +++ b/src/exchangedb/pg_select_aml_process.c @@ -112,6 +112,7 @@ TEH_PG_select_aml_process ( void *cls, enum TALER_AmlDecisionState decision, uint64_t row_off, + uint64_t limit, bool forward, TALER_EXCHANGEDB_AmlStatusCallback cb, void *cb_cls) @@ -120,6 +121,7 @@ TEH_PG_select_aml_process ( struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint32 (&decision), GNUNET_PQ_query_param_uint64 (&row_off), + GNUNET_PQ_query_param_uint64 (&limit), GNUNET_PQ_query_param_end }; struct AmlProcessResultContext ctx = { @@ -144,7 +146,8 @@ TEH_PG_select_aml_process ( " FROM aml_status" " WHERE aml_status_serial_id > $2" " AND $1 = status & $1" - " ORDER BY aml_status_serial_id INC"); + " ORDER BY aml_status_serial_id INC" + " LIMIT $3"); PREPARE (pg, "select_aml_process_dec", "SELECT" @@ -156,7 +159,8 @@ TEH_PG_select_aml_process ( " FROM aml_status" " WHERE aml_status_serial_id < $2" " AND $1 = status & $1" - " ORDER BY aml_status_serial_id DESC"); + " ORDER BY aml_status_serial_id DESC" + " LIMIT $3"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, stmt, params, diff --git a/src/exchangedb/pg_select_aml_process.h b/src/exchangedb/pg_select_aml_process.h index 019f58b26..648cace2e 100644 --- a/src/exchangedb/pg_select_aml_process.h +++ b/src/exchangedb/pg_select_aml_process.h @@ -32,6 +32,7 @@ * @param cls closure * @param decision which decision states to filter by * @param row_off offset to start from + * @param limit how many rows to return at most * @param forward true to go forward in time, false to go backwards * @param cb callback to invoke on each match * @param cb_cls closure for @a cb @@ -42,6 +43,7 @@ TEH_PG_select_aml_process ( void *cls, enum TALER_AmlDecisionState decision, uint64_t row_off, + uint64_t limit, bool forward, TALER_EXCHANGEDB_AmlStatusCallback cb, void *cb_cls); diff --git a/src/include/taler_exchange_service.h b/src/include/taler_exchange_service.h index 2f3008e2a..c6391647e 100644 --- a/src/include/taler_exchange_service.h +++ b/src/include/taler_exchange_service.h @@ -4381,7 +4381,7 @@ struct TALER_EXCHANGE_LookupAmlDecisions; * Inform the exchange that an AML decision has been taken. * * @param ctx the context - * @param url HTTP base URL for the exchange + * @param exchange_url HTTP base URL for the exchange * @param start row number starting point (exclusive rowid) * @param delta number of records to return, negative for descending, positive for ascending from start * @param filter_frozen true to only return frozen accounts @@ -4395,7 +4395,7 @@ struct TALER_EXCHANGE_LookupAmlDecisions; struct TALER_EXCHANGE_LookupAmlDecisions * TALER_EXCHANGE_lookup_aml_decisions ( struct GNUNET_CURL_Context *ctx, - const char *url, + const char *exchange_url, uint64_t start, int delta, bool filter_frozen, @@ -4532,7 +4532,7 @@ struct TALER_EXCHANGE_LookupAmlDecision; * Inform the exchange that an AML decision has been taken. * * @param ctx the context - * @param url HTTP base URL for the exchange + * @param exchange_url HTTP base URL for the exchange * @param h_payto which account to return the decision history for * @param officer_priv private key of the deciding AML officer * @param cb function to call with the exchange's result @@ -4542,10 +4542,10 @@ struct TALER_EXCHANGE_LookupAmlDecision; struct TALER_EXCHANGE_LookupAmlDecision * TALER_EXCHANGE_lookup_aml_decision ( struct GNUNET_CURL_Context *ctx, - const char *url, + const char *exchange_url, const struct TALER_PaytoHashP *h_payto, const struct TALER_AmlOfficerPrivateKeyP *officer_priv, - TALER_EXCHANGE_LookupAmlDecisionsCallback cb, + TALER_EXCHANGE_LookupAmlDecisionCallback cb, void *cb_cls); diff --git a/src/include/taler_exchangedb_plugin.h b/src/include/taler_exchangedb_plugin.h index b1394b45e..3a8b88ae3 100644 --- a/src/include/taler_exchangedb_plugin.h +++ b/src/include/taler_exchangedb_plugin.h @@ -6693,6 +6693,7 @@ struct TALER_EXCHANGEDB_Plugin void *cls, enum TALER_AmlDecisionState decision, uint64_t row_off, + uint64_t limit, bool forward, TALER_EXCHANGEDB_AmlStatusCallback cb, void *cb_cls); diff --git a/src/include/taler_util.h b/src/include/taler_util.h index 5776d62c3..8192ed87c 100644 --- a/src/include/taler_util.h +++ b/src/include/taler_util.h @@ -32,7 +32,7 @@ * Version of the Taler API, in hex. * Thus 0.8.4-1 = 0x00080401. */ -#define TALER_API_VERSION 0x00080401 +#define TALER_API_VERSION 0x00090200 /** * Stringify operator. @@ -80,6 +80,12 @@ } while (0) +/** + * HTTP header with an AML officer signature to approve the inquiry. + * Used only in GET Requests. + */ +#define TALER_AML_OFFICER_SIGNATURE_HEADER "Taler-AML-Officer-Signature" + /** * Log an error message at log-level 'level' that indicates * a failure of the command 'cmd' with the message given diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 00b604acf..529a2d019 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -38,6 +38,8 @@ libtalerexchange_la_SOURCES = \ exchange_api_kyc_proof.c \ exchange_api_kyc_wallet.c \ exchange_api_link.c \ + exchange_api_lookup_aml_decision.c \ + exchange_api_lookup_aml_decisions.c \ exchange_api_management_add_partner.c \ exchange_api_management_auditor_disable.c \ exchange_api_management_auditor_enable.c \ diff --git a/src/lib/exchange_api_lookup_aml_decision.c b/src/lib/exchange_api_lookup_aml_decision.c index c10919165..04b1d52aa 100644 --- a/src/lib/exchange_api_lookup_aml_decision.c +++ b/src/lib/exchange_api_lookup_aml_decision.c @@ -56,6 +56,10 @@ struct TALER_EXCHANGE_LookupAmlDecision */ void *decision_cb_cls; + /** + * HTTP headers for the job. + */ + struct curl_slist *job_headers; }; @@ -118,7 +122,7 @@ handle_lookup_finished (void *cls, break; } GNUNET_assert (NULL == lh->decision_cb); - TALER_EXCHANGE_link_cancel (lh); + TALER_EXCHANGE_lookup_aml_decision_cancel (lh); return; case MHD_HTTP_BAD_REQUEST: lr.hr.ec = TALER_JSON_get_error_code (j); @@ -152,14 +156,14 @@ handle_lookup_finished (void *cls, if (NULL != lh->decision_cb) lh->decision_cb (lh->decision_cb_cls, &lr); - TALER_EXCHANGE_link_cancel (lh); + TALER_EXCHANGE_lookup_aml_decision_cancel (lh); } struct TALER_EXCHANGE_LookupAmlDecision * TALER_EXCHANGE_lookup_aml_decision ( struct GNUNET_CURL_Context *ctx, - const char *url, + const char *exchange_url, const struct TALER_PaytoHashP *h_payto, const struct TALER_AmlOfficerPrivateKeyP *officer_priv, TALER_EXCHANGE_LookupAmlDecisionCallback cb, @@ -202,7 +206,7 @@ TALER_EXCHANGE_lookup_aml_decision ( lh = GNUNET_new (struct TALER_EXCHANGE_LookupAmlDecision); lh->decision_cb = cb; lh->decision_cb_cls = cb_cls; - lh->url = TALER_URL_join (exchange_url, + lh->url = TALER_url_join (exchange_url, arg_str, NULL); if (NULL == lh->url) @@ -218,11 +222,33 @@ TALER_EXCHANGE_lookup_aml_decision ( GNUNET_free (lh); return NULL; } - // FIXME: add authorization header to 'eh' based on officer_sig! - lh->job = GNUNET_CURL_job_add_with_ct_json (ctx, - eh, - &handle_lookup_finished, - lh); + { + char *hdr; + char sig_str[sizeof (officer_sig) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &officer_sig, + sizeof (officer_sig), + sig_str, + sizeof (sig_str)); + *end = '\0'; + + GNUNET_asprintf (&hdr, + "%s: %s", + TALER_AML_OFFICER_SIGNATURE_HEADER, + sig_str); + lh->job_headers = curl_slist_append (NULL, + hdr); + GNUNET_free (hdr); + lh->job_headers = curl_slist_append (lh->job_headers, + "Content-type: application/json"); + lh->job = GNUNET_CURL_job_add2 (ctx, + eh, + lh->job_headers, + &handle_lookup_finished, + lh); + } return lh; } @@ -236,6 +262,7 @@ TALER_EXCHANGE_lookup_aml_decision_cancel ( GNUNET_CURL_job_cancel (lh->job); lh->job = NULL; } + curl_slist_free_all (lh->job_headers); GNUNET_free (lh->url); GNUNET_free (lh); } diff --git a/src/lib/exchange_api_lookup_aml_decisions.c b/src/lib/exchange_api_lookup_aml_decisions.c index 00c2eaab9..308c0f969 100644 --- a/src/lib/exchange_api_lookup_aml_decisions.c +++ b/src/lib/exchange_api_lookup_aml_decisions.c @@ -56,6 +56,10 @@ struct TALER_EXCHANGE_LookupAmlDecisions */ void *decisions_cb_cls; + /** + * HTTP headers for the job. + */ + struct curl_slist *job_headers; }; @@ -118,7 +122,7 @@ handle_lookup_finished (void *cls, break; } GNUNET_assert (NULL == lh->decisions_cb); - TALER_EXCHANGE_link_cancel (lh); + TALER_EXCHANGE_lookup_aml_decisions_cancel (lh); return; case MHD_HTTP_BAD_REQUEST: lr.hr.ec = TALER_JSON_get_error_code (j); @@ -152,14 +156,14 @@ handle_lookup_finished (void *cls, if (NULL != lh->decisions_cb) lh->decisions_cb (lh->decisions_cb_cls, &lr); - TALER_EXCHANGE_link_cancel (lh); + TALER_EXCHANGE_lookup_aml_decisions_cancel (lh); } struct TALER_EXCHANGE_LookupAmlDecisions * TALER_EXCHANGE_lookup_aml_decisions ( struct GNUNET_CURL_Context *ctx, - const char *url, + const char *exchange_url, uint64_t start, int delta, bool filter_frozen, @@ -180,7 +184,7 @@ TALER_EXCHANGE_lookup_aml_decisions ( TALER_officer_aml_query_sign (officer_priv, &officer_sig); { - char pub_str[sizeof (struct TALER_AmlOfficerPublicKeyP) * 2]; + char pub_str[sizeof (officer_pub) * 2]; char *end; end = GNUNET_STRINGS_data_to_string ( @@ -197,10 +201,10 @@ TALER_EXCHANGE_lookup_aml_decisions ( lh = GNUNET_new (struct TALER_EXCHANGE_LookupAmlDecisions); lh->decisions_cb = cb; lh->decisions_cb_cls = cb_cls; - lh->url = TALER_URL_join (exchange_url, + lh->url = TALER_url_join (exchange_url, arg_str, "frozen", - filter_fozen ? "yes" : NULL, + filter_frozen ? "yes" : NULL, "pending", filter_pending ? "yes" : NULL, "normal", @@ -219,11 +223,33 @@ TALER_EXCHANGE_lookup_aml_decisions ( GNUNET_free (lh); return NULL; } - // FIXME: add authorization header to 'eh' based on officer_sig! - lh->job = GNUNET_CURL_job_add_with_ct_json (ctx, - eh, - &handle_lookup_finished, - lh); + { + char *hdr; + char sig_str[sizeof (officer_sig) * 2]; + char *end; + + end = GNUNET_STRINGS_data_to_string ( + &officer_sig, + sizeof (officer_sig), + sig_str, + sizeof (sig_str)); + *end = '\0'; + + GNUNET_asprintf (&hdr, + "%s: %s", + TALER_AML_OFFICER_SIGNATURE_HEADER, + sig_str); + lh->job_headers = curl_slist_append (NULL, + hdr); + GNUNET_free (hdr); + lh->job_headers = curl_slist_append (lh->job_headers, + "Content-type: application/json"); + lh->job = GNUNET_CURL_job_add2 (ctx, + eh, + lh->job_headers, + &handle_lookup_finished, + lh); + } return lh; } @@ -237,6 +263,7 @@ TALER_EXCHANGE_lookup_aml_decisions_cancel ( GNUNET_CURL_job_cancel (lh->job); lh->job = NULL; } + curl_slist_free_all (lh->job_headers); GNUNET_free (lh->url); GNUNET_free (lh); }