diff options
Diffstat (limited to 'src/exchange/taler-exchange-httpd_kyc-check.c')
-rw-r--r-- | src/exchange/taler-exchange-httpd_kyc-check.c | 701 |
1 files changed, 602 insertions, 99 deletions
diff --git a/src/exchange/taler-exchange-httpd_kyc-check.c b/src/exchange/taler-exchange-httpd_kyc-check.c index ae1ab34f2..362c20a2e 100644 --- a/src/exchange/taler-exchange-httpd_kyc-check.c +++ b/src/exchange/taler-exchange-httpd_kyc-check.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2021 Taler Systems SA + Copyright (C) 2021-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 @@ -25,37 +25,261 @@ #include <microhttpd.h> #include <pthread.h> #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" #include "taler_signatures.h" +#include "taler_dbevents.h" #include "taler-exchange-httpd_keys.h" #include "taler-exchange-httpd_kyc-wallet.h" #include "taler-exchange-httpd_responses.h" /** - * Context for the request. + * Reserve GET request that is long-polling. */ -struct KycCheckContext +struct KycPoller { /** - * UUID being checked. + * Kept in a DLL. */ - uint64_t payment_target_uuid; + struct KycPoller *next; /** - * Current KYC status. + * Kept in a DLL. */ - struct TALER_EXCHANGEDB_KycStatus kyc; + struct KycPoller *prev; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Logic for @e ih + */ + struct TALER_KYCLOGIC_Plugin *ih_logic; + + /** + * Handle to asynchronously running KYC initiation + * request. + */ + struct TALER_KYCLOGIC_InitiateHandle *ih; + + /** + * Subscription for the database event we are + * waiting for. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Row of the requirement being checked. + */ + uint64_t requirement_row; + + /** + * Row of KYC process being initiated. + */ + uint64_t process_row; /** * Hash of the payto:// URI we are confirming to * have finished the KYC for. */ - struct GNUNET_HashCode h_payto; + struct TALER_PaytoHashP h_payto; + + /** + * When will this request time out? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * If the KYC complete, what kind of data was collected? + */ + json_t *kyc_details; + + /** + * Set to starting URL of KYC process if KYC is required. + */ + char *kyc_url; + + /** + * Set to error details, on error (@ec not TALER_EC_NONE). + */ + char *hint; + + /** + * Name of the section of the provider in the configuration. + */ + const char *section_name; + + /** + * Set to AML status of the account. + */ + enum TALER_AmlDecisionState aml_status; + + /** + * Set to error encountered with KYC logic, if any. + */ + enum TALER_ErrorCode ec; + + /** + * What kind of entity is doing the KYC check? + */ + enum TALER_KYCLOGIC_KycUserType ut; + + /** + * True if we are still suspended. + */ + bool suspended; + + /** + * False if KYC is not required. + */ + bool kyc_required; + + /** + * True if we once tried the KYC initiation. + */ + bool ih_done; + }; /** + * Head of list of requests in long polling. + */ +static struct KycPoller *kyp_head; + +/** + * Tail of list of requests in long polling. + */ +static struct KycPoller *kyp_tail; + + +void +TEH_kyc_check_cleanup () +{ + struct KycPoller *kyp; + + while (NULL != (kyp = kyp_head)) + { + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + if (NULL != kyp->ih) + { + kyp->ih_logic->initiate_cancel (kyp->ih); + kyp->ih = NULL; + } + if (kyp->suspended) + { + kyp->suspended = false; + MHD_resume_connection (kyp->connection); + } + } +} + + +/** + * Function called once a connection is done to + * clean up the `struct ReservePoller` state. + * + * @param rc context to clean up for + */ +static void +kyp_cleanup (struct TEH_RequestContext *rc) +{ + struct KycPoller *kyp = rc->rh_ctx; + + GNUNET_assert (! kyp->suspended); + if (NULL != kyp->eh) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Cancelling DB event listening\n"); + TEH_plugin->event_listen_cancel (TEH_plugin->cls, + kyp->eh); + kyp->eh = NULL; + } + if (NULL != kyp->ih) + { + kyp->ih_logic->initiate_cancel (kyp->ih); + kyp->ih = NULL; + } + json_decref (kyp->kyc_details); + GNUNET_free (kyp->kyc_url); + GNUNET_free (kyp->hint); + GNUNET_free (kyp); +} + + +/** + * Function called with the result of a KYC initiation + * operation. + * + * @param cls closure with our `struct KycPoller *` + * @param ec #TALER_EC_NONE on success + * @param redirect_url set to where to redirect the user on success, NULL on failure + * @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 error_msg_hint set to additional details to return to user, NULL on success + */ +static void +initiate_cb ( + void *cls, + enum TALER_ErrorCode ec, + const char *redirect_url, + const char *provider_user_id, + const char *provider_legitimization_id, + const char *error_msg_hint) +{ + struct KycPoller *kyp = cls; + enum GNUNET_DB_QueryStatus qs; + + kyp->ih = NULL; + kyp->ih_done = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC initiation `%s' completed with ec=%d (%s)\n", + provider_legitimization_id, + ec, + (TALER_EC_NONE == ec) + ? redirect_url + : error_msg_hint); + kyp->ec = ec; + if (TALER_EC_NONE == ec) + { + kyp->kyc_url = GNUNET_strdup (redirect_url); + } + else + { + kyp->hint = GNUNET_strdup (error_msg_hint); + } + qs = TEH_plugin->update_kyc_process_by_row ( + TEH_plugin->cls, + kyp->process_row, + kyp->section_name, + &kyp->h_payto, + provider_user_id, + provider_legitimization_id, + redirect_url, + GNUNET_TIME_UNIT_ZERO_ABS); + if (qs <= 0) + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYC requirement update failed for %s with status %d at %s:%u\n", + TALER_B2S (&kyp->h_payto), + qs, + __FILE__, + __LINE__); + GNUNET_assert (kyp->suspended); + kyp->suspended = false; + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + MHD_resume_connection (kyp->connection); + TALER_MHD_daemon_trigger (); +} + + +/** * Function implementing database transaction to check wallet's KYC status. * Runs the transaction logic; IF it returns a non-error code, the transaction * logic MUST NOT queue a MHD response. IF it returns an hard error, the @@ -63,7 +287,7 @@ struct KycCheckContext * returns the soft error code, the function MAY be called again to retry and * MUST not queue a MHD response. * - * @param cls closure with a `struct KycCheckContext *` + * @param cls closure with a `struct KycPoller *` * @param connection MHD request which triggered the transaction * @param[out] mhd_ret set to MHD response status for @a connection, * if transaction failed (!) @@ -74,13 +298,55 @@ kyc_check (void *cls, struct MHD_Connection *connection, MHD_RESULT *mhd_ret) { - struct KycCheckContext *kcc = cls; + struct KycPoller *kyp = cls; enum GNUNET_DB_QueryStatus qs; + struct TALER_KYCLOGIC_ProviderDetails *pd; + enum GNUNET_GenericReturnValue ret; + struct TALER_PaytoHashP h_payto; + char *requirements; + char *redirect_url; + bool satisfied; - qs = TEH_plugin->select_kyc_status (TEH_plugin->cls, - kcc->payment_target_uuid, - &kcc->h_payto, - &kcc->kyc); + qs = TEH_plugin->lookup_kyc_requirement_by_row ( + TEH_plugin->cls, + kyp->requirement_row, + &requirements, + &kyp->aml_status, + &h_payto); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No KYC requirements open for %llu\n", + (unsigned long long) kyp->requirement_row); + return qs; + } + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + return qs; + } + if (0 != + GNUNET_memcmp (&kyp->h_payto, + &h_payto)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Requirement %llu provided, but h_payto does not match\n", + (unsigned long long) kyp->requirement_row); + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED, + "h_payto"); + GNUNET_free (requirements); + return GNUNET_DB_STATUS_HARD_ERROR; + } + qs = TALER_KYCLOGIC_check_satisfied ( + &requirements, + &h_payto, + &kyp->kyc_details, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &satisfied); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -89,117 +355,354 @@ kyc_check (void *cls, *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, - "inselect_wallet_status"); + "kyc_test_required"); + GNUNET_free (requirements); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (satisfied) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC requirements `%s' already satisfied\n", + requirements); + GNUNET_free (requirements); + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + + kyp->kyc_required = true; + ret = TALER_KYCLOGIC_requirements_to_logic (requirements, + kyp->ut, + &kyp->ih_logic, + &pd, + &kyp->section_name); + if (GNUNET_OK != ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYC requirements `%s' cannot be checked, but are set as required in database!\n", + requirements); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_GONE, + requirements); + GNUNET_free (requirements); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_free (requirements); + + if (kyp->ih_done) + return qs; + qs = TEH_plugin->get_pending_kyc_requirement_process ( + TEH_plugin->cls, + &h_payto, + kyp->section_name, + &redirect_url); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_process"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if ( (qs > 0) && + (NULL != redirect_url) ) + { + kyp->kyc_url = redirect_url; return qs; } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* set up new requirement process */ + qs = TEH_plugin->insert_kyc_requirement_process ( + TEH_plugin->cls, + &h_payto, + kyp->section_name, + NULL, + NULL, + &kyp->process_row); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_process"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Initiating KYC check with logic %s\n", + kyp->ih_logic->name); + kyp->ih = kyp->ih_logic->initiate (kyp->ih_logic->cls, + pd, + &h_payto, + kyp->process_row, + &initiate_cb, + kyp); + GNUNET_break (NULL != kyp->ih); return qs; } +/** + * Function called on events received from Postgres. + * Wakes up long pollers. + * + * @param cls the `struct TEH_RequestContext *` + * @param extra additional event data provided + * @param extra_size number of bytes in @a extra + */ +static void +db_event_cb (void *cls, + const void *extra, + size_t extra_size) +{ + struct TEH_RequestContext *rc = cls; + struct KycPoller *kyp = rc->rh_ctx; + struct GNUNET_AsyncScopeSave old_scope; + + (void) extra; + (void) extra_size; + if (! kyp->suspended) + return; /* event triggered while main transaction + was still running, or got multiple wake-up events */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received KYC update event\n"); + kyp->suspended = false; + GNUNET_async_scope_enter (&rc->async_scope_id, + &old_scope); + TEH_check_invariants (); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming from long-polling on KYC status\n"); + GNUNET_CONTAINER_DLL_remove (kyp_head, + kyp_tail, + kyp); + MHD_resume_connection (kyp->connection); + TALER_MHD_daemon_trigger (); + TEH_check_invariants (); + GNUNET_async_scope_restore (&old_scope); +} + + MHD_RESULT TEH_handler_kyc_check ( struct TEH_RequestContext *rc, - const char *const args[]) + const char *const args[3]) { - unsigned long long payment_target_uuid; + struct KycPoller *kyp = rc->rh_ctx; MHD_RESULT res; enum GNUNET_GenericReturnValue ret; - char dummy; + struct GNUNET_TIME_Timestamp now; - if (1 != - sscanf (args[0], - "%llu%c", - &payment_target_uuid, - &dummy)) + if (NULL == kyp) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payment_target_uuid"); + kyp = GNUNET_new (struct KycPoller); + kyp->connection = rc->connection; + rc->rh_ctx = kyp; + rc->rh_cleaner = &kyp_cleanup; + + { + unsigned long long requirement_row; + char dummy; + + if (1 != + sscanf (args[0], + "%llu%c", + &requirement_row, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "requirement_row"); + } + kyp->requirement_row = (uint64_t) requirement_row; + } + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[1], + strlen (args[1]), + &kyp->h_payto, + sizeof (kyp->h_payto))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "h_payto"); + } + + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_user_type_from_string (args[2], + &kyp->ut)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "usertype"); + } + + TALER_MHD_parse_request_timeout (rc->connection, + &kyp->timeout); + } + /* KYC plugin generated reply? */ + if (NULL != kyp->kyc_url) + { + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_ACCEPTED, + GNUNET_JSON_pack_uint64 ("aml_status", + kyp->aml_status), + GNUNET_JSON_pack_string ("kyc_url", + kyp->kyc_url)); } - if (TEH_KYC_NONE == TEH_kyc_config.mode) + if ( (NULL == kyp->eh) && + GNUNET_TIME_absolute_is_future (kyp->timeout) ) + { + struct TALER_KycCompletedEventP rep = { + .header.size = htons (sizeof (rep)), + .header.type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED), + .h_payto = kyp->h_payto + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting DB event listening\n"); + kyp->eh = TEH_plugin->event_listen ( + TEH_plugin->cls, + GNUNET_TIME_absolute_get_remaining (kyp->timeout), + &rep.header, + &db_event_cb, + rc); + } + + now = GNUNET_TIME_timestamp_get (); + ret = TEH_DB_run_transaction (rc->connection, + "kyc check", + TEH_MT_REQUEST_OTHER, + &res, + &kyc_check, + kyp); + if (GNUNET_SYSERR == ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Transaction failed.\n"); + return res; + } + /* KYC plugin generated reply? */ + if (NULL != kyp->kyc_url) + { + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_ACCEPTED, + GNUNET_JSON_pack_uint64 ("aml_status", + kyp->aml_status), + GNUNET_JSON_pack_string ("kyc_url", + kyp->kyc_url)); + } + + if ( (NULL == kyp->ih) && + (! kyp->kyc_required) ) + { + if (TALER_AML_NORMAL != kyp->aml_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC is OK, but AML active: %d\n", + (int) kyp->aml_status); + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + GNUNET_JSON_pack_uint64 ("aml_status", + kyp->aml_status)); + } + /* KYC not required */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC not required %llu\n", + (unsigned long long) kyp->requirement_row); return TALER_MHD_reply_static ( rc->connection, MHD_HTTP_NO_CONTENT, NULL, NULL, 0); + } + + if (NULL != kyp->ih) { - struct GNUNET_TIME_Absolute now; - struct KycCheckContext kcc = { - .payment_target_uuid = payment_target_uuid - }; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending HTTP request on KYC logic...\n"); + kyp->suspended = true; + GNUNET_CONTAINER_DLL_insert (kyp_head, + kyp_tail, + kyp); + MHD_suspend_connection (kyp->connection); + return MHD_YES; + } - now = GNUNET_TIME_absolute_get (); - (void) GNUNET_TIME_round_abs (&now); - ret = TEH_DB_run_transaction (rc->connection, - "kyc check", - &res, - &kyc_check, - &kcc); - if (GNUNET_SYSERR == ret) - return res; - if (! kcc.kyc.ok) - { - char *url; - char *redirect_uri; - char *redirect_uri_encoded; - - GNUNET_assert (TEH_KYC_OAUTH2 == TEH_kyc_config.mode); - GNUNET_asprintf (&redirect_uri, - "%s/kyc-proof/%llu", - TEH_base_url, - payment_target_uuid); - redirect_uri_encoded = TALER_urlencode (redirect_uri); - GNUNET_free (redirect_uri); - GNUNET_asprintf (&url, - "%s/login?client_id=%s&redirect_uri=%s", - TEH_kyc_config.details.oauth2.url, - TEH_kyc_config.details.oauth2.client_id, - redirect_uri_encoded); - GNUNET_free (redirect_uri_encoded); - - res = TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_string ("kyc_url", - url)); - GNUNET_free (url); - return res; - } + /* long polling? */ + if ( (NULL != kyp->section_name) && + GNUNET_TIME_absolute_is_future (kyp->timeout)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending HTTP request on timeout (%s) now...\n", + GNUNET_TIME_relative2s (GNUNET_TIME_absolute_get_remaining ( + kyp->timeout), + true)); + GNUNET_assert (NULL != kyp->eh); + kyp->suspended = true; + kyp->section_name = NULL; + GNUNET_CONTAINER_DLL_insert (kyp_head, + kyp_tail, + kyp); + MHD_suspend_connection (kyp->connection); + return MHD_YES; + } + + if (TALER_EC_NONE != kyp->ec) + { + return TALER_MHD_reply_with_ec (rc->connection, + kyp->ec, + kyp->hint); + } + + /* KYC must have succeeded! */ + { + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec; + + if (TALER_EC_NONE != + (ec = TALER_exchange_online_account_setup_success_sign ( + &TEH_keys_exchange_sign_, + &kyp->h_payto, + kyp->kyc_details, + now, + &pub, + &sig))) { - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - struct TALER_ExchangeAccountSetupSuccessPS as = { - .purpose.purpose = htonl ( - TALER_SIGNATURE_EXCHANGE_ACCOUNT_SETUP_SUCCESS), - .purpose.size = htonl (sizeof (as)), - .h_payto = kcc.h_payto, - .timestamp = GNUNET_TIME_absolute_hton (now) - }; - enum TALER_ErrorCode ec; - - if (TALER_EC_NONE != - (ec = TEH_keys_exchange_sign (&as, - &pub, - &sig))) - { - return TALER_MHD_reply_with_ec (rc->connection, - ec, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub), - GNUNET_JSON_pack_time_abs ("now", - now)); + return TALER_MHD_reply_with_ec (rc->connection, + ec, + NULL); } + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub), + GNUNET_JSON_pack_uint64 ("aml_status", + kyp->aml_status), + GNUNET_JSON_pack_object_incref ("kyc_details", + kyp->kyc_details), + GNUNET_JSON_pack_timestamp ("now", + now)); } } |