exchange

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

commit a7cb67227df85d63c9433981358dea406fd0346c
parent 3ecec2cb8a0e7def733025a8b2d08243aa7bff39
Author: Florian Dold <florian@dold.me>
Date:   Mon, 21 Oct 2024 15:06:28 +0200

run immediate measures on POSTing AML decision

Diffstat:
Msrc/exchange/taler-exchange-httpd_aml-decision.c | 274+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/exchange/taler-exchange-httpd_common_kyc.c | 28+---------------------------
Msrc/exchange/taler-exchange-httpd_common_kyc.h | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler_kyclogic_lib.h | 16++++++++++++++++
Msrc/kyclogic/kyclogic_api.c | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 351 insertions(+), 60 deletions(-)

diff --git a/src/exchange/taler-exchange-httpd_aml-decision.c b/src/exchange/taler-exchange-httpd_aml-decision.c @@ -17,6 +17,7 @@ * @file taler-exchange-httpd_aml-decision.c * @brief Handle POST request about an AML decision. * @author Christian Grothoff + * @author Florian Dold */ #include "platform.h" #include <gnunet/gnunet_util_lib.h> @@ -28,10 +29,127 @@ #include "taler_mhd_lib.h" #include "taler_kyclogic_lib.h" #include "taler_signatures.h" +#include "taler-exchange-httpd_common_kyc.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_aml-decision.h" +/** + * Context used for processing the AML decision request. + */ +struct AmlDecisionContext +{ + + /** + * Kept in a DLL. + */ + struct AmlDecisionContext *next; + + /** + * Kept in a DLL. + */ + struct AmlDecisionContext *prev; + + /** + * HTTP status code to use with @e response. + */ + unsigned int response_code; + + /** + * Response to return, NULL if none yet. + */ + struct MHD_Response *response; + + /** + * Request we are processing. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for async KYC processing. + */ + struct TEH_KycAmlTrigger *kat; + +}; + +/** + * Kept in a DLL. + */ +static struct AmlDecisionContext *adc_head; + +/** + * Kept in a DLL. + */ +static struct AmlDecisionContext *adc_tail; + + +void +TEH_aml_decision_cleanup () +{ + struct AmlDecisionContext *adc; + + while (NULL != (adc = adc_head)) + { + MHD_resume_connection (adc->rc->connection); + GNUNET_CONTAINER_DLL_remove (adc_head, + adc_tail, + adc); + } +} + + +/** + * Function called to clean up aml decision context. + * + * @param[in,out] rc context to clean up + */ +static void +aml_decision_cleaner (struct TEH_RequestContext *rc) +{ + struct AmlDecisionContext *adc = rc->rh_ctx; + + if (NULL != adc->kat) + { + TEH_kyc_finished_cancel (adc->kat); + adc->kat = NULL; + } + if (NULL != adc->response) + { + MHD_destroy_response (adc->response); + adc->response = NULL; + } + GNUNET_free (adc); +} + + +/** + * Function called after the KYC-AML trigger is done. + * + * @param cls closure + * @param http_status final HTTP status to return + * @param[in] response final HTTP ro return + */ +static void +aml_trigger_callback ( + void *cls, + unsigned int http_status, + struct MHD_Response *response) +{ + struct AmlDecisionContext *adc = cls; + + adc->kat = NULL; + GNUNET_assert (NULL == adc->response); + GNUNET_assert (NULL != response); + adc->response_code = http_status; + adc->response = response; + MHD_resume_connection (adc->rc->connection); + GNUNET_CONTAINER_DLL_remove (adc_head, + adc_tail, + adc); + TALER_MHD_daemon_trigger (); +} + + MHD_RESULT TEH_handler_post_aml_decision ( struct TEH_RequestContext *rc, @@ -39,6 +157,7 @@ TEH_handler_post_aml_decision ( const json_t *root) { struct MHD_Connection *connection = rc->connection; + struct AmlDecisionContext *adc = rc->rh_ctx; const char *justification; const char *new_measures = NULL; bool to_investigate; @@ -48,6 +167,8 @@ TEH_handler_post_aml_decision ( const char *payto_uri = NULL; struct TALER_PaytoHashP h_payto; struct TALER_AmlOfficerSignatureP officer_sig; + struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs = NULL; + MHD_RESULT ret; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ( @@ -79,6 +200,23 @@ TEH_handler_post_aml_decision ( struct GNUNET_TIME_Timestamp expiration_time; json_t *jmeasures = NULL; + if (NULL == adc) + { + /* Initialize context */ + adc = GNUNET_new (struct AmlDecisionContext); + adc->rc = rc; + rc->rh_ctx = adc; + rc->rh_cleaner = aml_decision_cleaner; + } + + if (NULL != adc->response) + { + ret = MHD_queue_response (rc->connection, + adc->response_code, + adc->response); + goto done; + } + { enum GNUNET_GenericReturnValue res; @@ -86,11 +224,15 @@ TEH_handler_post_aml_decision ( root, spec); if (GNUNET_SYSERR == res) - return MHD_NO; /* hard failure */ + { + ret = MHD_NO; /* hard failure */ + goto done; + } if (GNUNET_NO == res) { GNUNET_break_op (0); - return MHD_YES; /* failure */ + ret = MHD_YES /* failure */; + goto done; } } if (NULL != payto_uri) @@ -104,11 +246,12 @@ TEH_handler_post_aml_decision ( &h_payto2)) { GNUNET_break (0); - return TALER_MHD_reply_with_error ( + ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "payto_uri"); + goto done; } } @@ -126,45 +269,44 @@ TEH_handler_post_aml_decision ( &officer_sig)) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( + ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_AML_DECISION_ADD_SIGNATURE_INVALID, NULL); + goto done; } + lrs = TALER_KYCLOGIC_rules_parse (new_rules); + if (NULL == lrs) { - struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs; + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "legitimization rule malformed"); + goto done; + } - lrs = TALER_KYCLOGIC_rules_parse (new_rules); - if (NULL == lrs) + expiration_time = TALER_KYCLOGIC_rules_get_expiration (lrs); + if (NULL != new_measures) + { + jmeasures + = TALER_KYCLOGIC_get_measures (lrs, + new_measures); + if (NULL == jmeasures) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( + /* Request specified a new_measure for which the given + rule set does not work as it does not define the measure */ + ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "legitimization rule malformed"); + "new_measures/new_rules"); + goto done; } - expiration_time = TALER_KYCLOGIC_rules_get_expiration (lrs); - if (NULL != new_measures) - { - jmeasures - = TALER_KYCLOGIC_get_measures (lrs, - new_measures); - if (NULL == jmeasures) - { - GNUNET_break_op (0); - /* Request specified a new_measure for which the given - rule set does not work as it does not define the measure */ - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "new_measures/new_rules"); - } - } - TALER_KYCLOGIC_rules_free (lrs); } { @@ -195,48 +337,114 @@ TEH_handler_post_aml_decision ( if (qs <= 0) { GNUNET_break (0); - return TALER_MHD_reply_with_error ( + ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "insert_aml_decision"); + goto done; } if (invalid_officer) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( + ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_AML_DECISION_INVALID_OFFICER, NULL); + goto done; } if (unknown_account) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( + ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_GENERIC_BANK_ACCOUNT_UNKNOWN, "h_payto"); + goto done; } if (GNUNET_TIME_timestamp_cmp (last_date, >=, decision_time)) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( + ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, TALER_EC_EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT, NULL); + goto done; } } - return TALER_MHD_reply_static ( + /* Run instant measure if necessary */ + { + const struct TALER_KYCLOGIC_Measure *instant_ms; + struct MHD_Response *empty_response; + + instant_ms = TALER_KYCLOGIC_get_instant_measure (lrs, new_measures); + if (NULL != instant_ms) + { + /* We have an 'instant' measure which means we must run the + AML program immediately instead of waiting for the account owner + to select some measure and contribute their KYC data. */ + json_t *attributes + = json_object (); /* instant: empty attributes */ + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Running instant measure after AML decision\n"); + + GNUNET_assert (NULL != attributes); + empty_response + = MHD_create_response_from_buffer_static (0, + ""); + GNUNET_assert (NULL != empty_response); + adc->kat + = TEH_kyc_finished2 ( + &rc->async_scope_id, + 0LL, + instant_ms, + &h_payto, + "SKIP", /* provider */ + NULL, + NULL, + GNUNET_TIME_UNIT_FOREVER_ABS, + attributes, + MHD_HTTP_NO_CONTENT, /* http status */ + empty_response, /* MHD_Response */ + &aml_trigger_callback, + adc); + json_decref (attributes); + if (NULL == adc->kat) + { + GNUNET_break (0); + ret = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG, + "TEH_kyc_finished"); + goto done; + } + + MHD_suspend_connection (adc->rc->connection); + GNUNET_CONTAINER_DLL_insert (adc_head, + adc_tail, + adc); + ret = MHD_YES; + goto done; + } + } + ret = TALER_MHD_reply_static ( connection, MHD_HTTP_NO_CONTENT, NULL, NULL, 0); + goto done; + +done: + TALER_KYCLOGIC_rules_free (lrs); + return ret; } diff --git a/src/exchange/taler-exchange-httpd_common_kyc.c b/src/exchange/taler-exchange-httpd_common_kyc.c @@ -477,33 +477,7 @@ add_kyc_history_entry ( } -/** - * We have finished a KYC process and obtained new - * @a attributes for a given @a account_id. - * Check with the KYC-AML trigger to see if we need - * to initiate an AML process, and store the attributes - * in the database. Then call @a cb. - * - * @param scope the HTTP request logging scope - * @param process_row legitimization process the data provided is about, - * or must be 0 if instant_ms is given - * @param instant_ms instant measure to run, used if @a process_row is 0, - * otherwise must be NULL - * @param account_id account the webhook was about - * @param provider_name name of the provider with the logic that was run - * @param provider_user_id set to user ID at the provider, or - * NULL if not supported or unknown - * @param provider_legitimization_id set to legitimization process ID at the provider, - * or NULL if not supported or unknown - * @param expiration until when is the KYC check valid - * @param attributes user attributes returned by the provider - * @param http_status HTTP status code of @a response - * @param[in] response to return to the HTTP client, can be NULL - * @param cb function to call with the result - * @param cb_cls closure for @a cb - * @return handle to cancel the operation - */ -static struct TEH_KycAmlTrigger * +struct TEH_KycAmlTrigger * TEH_kyc_finished2 ( const struct GNUNET_AsyncScopeId *scope, uint64_t process_row, diff --git a/src/exchange/taler-exchange-httpd_common_kyc.h b/src/exchange/taler-exchange-httpd_common_kyc.h @@ -87,6 +87,48 @@ TEH_kyc_finished ( TEH_KycAmlTriggerCallback cb, void *cb_cls); +/** + * We have finished a KYC process and obtained new + * @a attributes for a given @a account_id. + * Check with the KYC-AML trigger to see if we need + * to initiate an AML process, and store the attributes + * in the database. Then call @a cb. + * + * @param scope the HTTP request logging scope + * @param process_row legitimization process the data provided is about, + * or must be 0 if instant_ms is given + * @param instant_ms instant measure to run, used if @a process_row is 0, + * otherwise must be NULL + * @param account_id account the webhook was about + * @param provider_name name of the provider with the logic that was run + * @param provider_user_id set to user ID at the provider, or + * NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, + * or NULL if not supported or unknown + * @param expiration until when is the KYC check valid + * @param attributes user attributes returned by the provider + * @param http_status HTTP status code of @a response + * @param[in] response to return to the HTTP client, can be NULL + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel the operation + */ +struct TEH_KycAmlTrigger * +TEH_kyc_finished2 ( + const struct GNUNET_AsyncScopeId *scope, + uint64_t process_row, + const struct TALER_KYCLOGIC_Measure *instant_ms, + const struct TALER_PaytoHashP *account_id, + const char *provider_name, + const char *provider_user_id, + const char *provider_legitimization_id, + struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, + unsigned int http_status, + struct MHD_Response *response, + TEH_KycAmlTriggerCallback cb, + void *cb_cls); + /** * Cancel KYC finish operation. diff --git a/src/include/taler_kyclogic_lib.h b/src/include/taler_kyclogic_lib.h @@ -772,6 +772,22 @@ TALER_KYCLOGIC_rule_get_instant_measure ( /** + * Check if there is a measure in @a lrs + * that is included in @a measure_spec + * and a SKIP measure, and thus should be immediately + * executed. + * + * @param rls legitimization rule set + * @param measures_spec measures spec + * @returns NULL if there is no instant measure + */ +const struct TALER_KYCLOGIC_Measure * +TALER_KYCLOGIC_get_instant_measure ( + const struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs, + const char *measures_spec); + + +/** * Handle to manage a running AML program. */ struct TALER_KYCLOGIC_AmlProgramRunnerHandle; diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c @@ -1172,6 +1172,57 @@ TALER_KYCLOGIC_voluntary_measures ( } +const struct TALER_KYCLOGIC_Measure * +TALER_KYCLOGIC_get_instant_measure ( + const struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs, + const char *measures_spec) +{ + char *nm; + const struct TALER_KYCLOGIC_Measure *ret = NULL; + + if ('+' == measures_spec[0]) + { + nm = GNUNET_strdup (&measures_spec[1]); + } + else + { + nm = GNUNET_strdup (measures_spec); + } + for (const char *tok = strtok (nm, " "); + NULL != tok; + tok = strtok (NULL, " ")) + { + const struct TALER_KYCLOGIC_Measure *ms; + + if (0 == strcasecmp ("verboten", + tok)) + { + continue; + } + ms = find_measure (lrs, + tok); + if (NULL == ms) + { + GNUNET_break (0); + continue; + } + if (0 == strcasecmp ("verboten", + ms->check_name)) + { + continue; + } + if (0 == strcasecmp ("SKIP", ms->check_name)) + { + ret = ms; + goto done; + } + } +done: + GNUNET_free (nm); + return ret; +} + + json_t * TALER_KYCLOGIC_get_measures ( const struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs,