diff options
author | Christian Grothoff <christian@grothoff.org> | 2022-08-11 23:35:33 +0200 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2022-08-11 23:35:33 +0200 |
commit | 1009084e94b8e8cf19e3b5568c3cccaba2bd2209 (patch) | |
tree | a346997dedd05f685ba7addc59e288dfa550ad0e /src/exchange | |
parent | b061ea85c84facfc78c34edface367c5f040bc9c (diff) |
major rework of the KYC logic, making it more configurable, not complete, but tests pass again
Diffstat (limited to 'src/exchange')
-rw-r--r-- | src/exchange/Makefile.am | 1 | ||||
-rw-r--r-- | src/exchange/taler-exchange-aggregator.c | 779 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd.c | 15 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_batch-withdraw.c | 146 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_deposit.c | 6 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_deposits_get.c | 12 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_keys.c | 45 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_kyc-check.c | 355 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_kyc-proof.c | 671 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_kyc-wallet.c | 119 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_purses_merge.c | 16 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_withdraw.c | 170 |
12 files changed, 1344 insertions, 991 deletions
diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am index 64db3899..881a3f36 100644 --- a/src/exchange/Makefile.am +++ b/src/exchange/Makefile.am @@ -30,6 +30,7 @@ taler_exchange_aggregator_SOURCES = \ taler-exchange-aggregator.c taler_exchange_aggregator_LDADD = \ $(LIBGCRYPT_LIBS) \ + $(top_builddir)/src/kyclogic/libtalerkyclogic.la \ $(top_builddir)/src/json/libtalerjson.la \ $(top_builddir)/src/util/libtalerutil.la \ $(top_builddir)/src/bank-lib/libtalerbank.la \ diff --git a/src/exchange/taler-exchange-aggregator.c b/src/exchange/taler-exchange-aggregator.c index 3d30ccd0..2c279535 100644 --- a/src/exchange/taler-exchange-aggregator.c +++ b/src/exchange/taler-exchange-aggregator.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2016-2021 Taler Systems SA + Copyright (C) 2016-2022 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 @@ -26,6 +26,7 @@ #include "taler_exchangedb_lib.h" #include "taler_exchangedb_plugin.h" #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_bank_service.h" @@ -43,6 +44,12 @@ struct AggregationUnit struct TALER_MerchantPublicKeyP merchant_pub; /** + * Transient amount already found aggregated, + * set only if @e have_transient is true. + */ + struct TALER_Amount trans; + + /** * Total amount to be transferred, before subtraction of @e fees.wire and rounding down. */ struct TALER_Amount total_amount; @@ -84,6 +91,19 @@ struct AggregationUnit */ const struct TALER_EXCHANGEDB_AccountInfo *wa; + /** + * Set to #GNUNET_OK during transient checking + * while everything is OK. Otherwise see return + * value of #do_aggregate(). + */ + enum GNUNET_GenericReturnValue ret; + + /** + * Do we have an entry in the transient table for + * this aggregation? + */ + bool have_transient; + }; @@ -151,7 +171,6 @@ static struct TALER_EXCHANGEDB_Plugin *db_plugin; */ static struct GNUNET_SCHEDULER_Task *task; - /** * How long should we sleep when idle before trying to find more work? */ @@ -186,12 +205,12 @@ run_aggregation (void *cls); /** - * Select a shard to work on. + * Work on transactions unlocked by KYC. * * @param cls NULL */ static void -run_shard (void *cls); +drain_kyc_alerts (void *cls); /** @@ -226,6 +245,7 @@ shutdown_task (void *cls) GNUNET_SCHEDULER_cancel (task); task = NULL; } + TALER_KYCLOGIC_kyc_done (); TALER_EXCHANGEDB_plugin_unload (db_plugin); db_plugin = NULL; TALER_EXCHANGEDB_unload_accounts (); @@ -353,109 +373,170 @@ release_shard (struct Shard *s) } +/** + * Trigger the wire transfer for the @a au_active + * and delete the record of the aggregation. + * + * @param au_active information about the aggregation + */ +static enum GNUNET_DB_QueryStatus +trigger_wire_transfer (const struct AggregationUnit *au_active) +{ + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Preparing wire transfer of %s to %s\n", + TALER_amount2s (&au_active->final_amount), + TALER_B2S (&au_active->merchant_pub)); + { + void *buf; + size_t buf_size; + + TALER_BANK_prepare_transfer (au_active->payto_uri, + &au_active->final_amount, + exchange_base_url, + &au_active->wtid, + &buf, + &buf_size); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Storing %u bytes of wire prepare data\n", + (unsigned int) buf_size); + /* Commit our intention to execute the wire transfer! */ + qs = db_plugin->wire_prepare_data_insert (db_plugin->cls, + au_active->wa->method, + buf, + buf_size); + GNUNET_free (buf); + } + /* Commit the WTID data to 'wire_out' */ + if (qs >= 0) + qs = db_plugin->store_wire_transfer_out (db_plugin->cls, + au_active->execution_time, + &au_active->wtid, + &au_active->h_payto, + au_active->wa->section_name, + &au_active->final_amount); + + if ( (qs >= 0) && + au_active->have_transient) + qs = db_plugin->delete_aggregation_transient (db_plugin->cls, + &au_active->h_payto, + &au_active->wtid); + return qs; +} + + +/** + * Callback to return all applicable amounts for the KYC + * decision to @ a cb. + * + * @param cls a `struct AggregationUnit *` + * @param limit time limit for the iteration + * @param cb function to call with the amounts + * @param cb_cls closure for @a cb + */ static void -run_aggregation (void *cls) +return_relevant_amounts (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) { - struct Shard *s = cls; - struct AggregationUnit au_active; + const struct AggregationUnit *au_active = cls; enum GNUNET_DB_QueryStatus qs; - struct TALER_Amount trans; - bool have_transient = true; /* squash compiler warning */ - task = NULL; GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Checking for ready deposits to aggregate\n"); - /* make sure we have current fees */ - memset (&au_active, - 0, - sizeof (au_active)); - au_active.execution_time = GNUNET_TIME_timestamp_get (); + "Returning amount %s in KYC check\n", + TALER_amount2s (&au_active->total_amount)); if (GNUNET_OK != - db_plugin->start_deferred_wire_out (db_plugin->cls)) + cb (cb_cls, + &au_active->total_amount, + GNUNET_TIME_absolute_get ())) + return; + qs = db_plugin->select_aggregation_amounts_for_kyc_check ( + db_plugin->cls, + &au_active->h_payto, + limit, + cb, + cb_cls); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to start database transaction!\n"); - global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - release_shard (s); - return; + "Failed to select aggregation amounts for KYC limit check!\n"); } - qs = db_plugin->get_ready_deposit ( +} + + +/** + * Test if KYC is required for a transfer to @a h_payto. + * + * @param au_active aggregation unit to check for + * @return true if KYC checks are satisfied + */ +static bool +kyc_satisfied (const struct AggregationUnit *au_active) +{ + const char *requirement; + uint64_t legi_row; + enum GNUNET_DB_QueryStatus qs; + + requirement = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT, + &au_active->h_payto, + db_plugin->select_satisfied_kyc_processes, db_plugin->cls, - s->shard_start, - s->shard_end, - kyc_off ? true : false, - &au_active.merchant_pub, - &au_active.payto_uri); - switch (qs) + &return_relevant_amounts, + (void *) au_active); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC requirement for %s is %s\n", + TALER_amount2s (&au_active->total_amount), + requirement); + if (NULL == requirement) + return true; + qs = db_plugin->insert_kyc_requirement_for_account ( + db_plugin->cls, + requirement, + &au_active->h_payto, + &legi_row); + if (qs < 0) { - case GNUNET_DB_STATUS_HARD_ERROR: - cleanup_au (&au_active); - db_plugin->rollback (db_plugin->cls); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to begin deposit iteration!\n"); - global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - release_shard (s); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - cleanup_au (&au_active); - db_plugin->rollback (db_plugin->cls); - GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&run_aggregation, - s); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - { - uint64_t counter = s->work_counter; - struct GNUNET_TIME_Relative duration - = GNUNET_TIME_absolute_get_duration (s->start_time.abs_time); - - cleanup_au (&au_active); - db_plugin->rollback (db_plugin->cls); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Completed shard [%u,%u] after %s with %llu deposits\n", - (unsigned int) s->shard_start, - (unsigned int) s->shard_end, - GNUNET_TIME_relative2s (duration, - true), - (unsigned long long) counter); - release_shard (s); - if ( (GNUNET_YES == test_mode) && - (0 == counter) ) - { - /* in test mode, shutdown after a shard is done with 0 work */ - GNUNET_SCHEDULER_shutdown (); - return; - } - GNUNET_assert (NULL == task); - /* If we ended up doing zero work, sleep a bit */ - if (0 == counter) - task = GNUNET_SCHEDULER_add_delayed (aggregator_idle_sleep_interval, - &run_shard, - NULL); - else - task = GNUNET_SCHEDULER_add_now (&run_shard, - NULL); - return; - } - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - s->work_counter++; - /* continued below */ - break; + "Failed to persist KYC requirement `%s' in DB!\n", + requirement); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "New legitimization process %llu started\n", + (unsigned long long) legi_row); } - au_active.wa = TALER_EXCHANGEDB_find_account_by_payto_uri ( - au_active.payto_uri); - if (NULL == au_active.wa) + return false; +} + + +/** + * Perform the main aggregation work for @a au. Expects to be in + * a working transaction, which the caller must also ultimately commit + * (or rollback) depending on our return value. + * + * @param[in,out] au aggregation unit to work on + * @return #GNUNET_OK if aggregation succeeded, + * #GNUNET_NO to rollback and try again (serialization issue) + * #GNUNET_SYSERR hard error, terminate aggregator process + */ +static enum GNUNET_GenericReturnValue +do_aggregate (struct AggregationUnit *au) +{ + enum GNUNET_DB_QueryStatus qs; + + au->wa = TALER_EXCHANGEDB_find_account_by_payto_uri ( + au->payto_uri); + if (NULL == au->wa) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "No exchange account configured for `%s', please fix your setup to continue!\n", - au_active.payto_uri); + au->payto_uri); global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - db_plugin->rollback (db_plugin->cls); - release_shard (s); - return; + return GNUNET_SYSERR; } { @@ -464,234 +545,265 @@ run_aggregation (void *cls) struct TALER_MasterSignatureP master_sig; qs = db_plugin->get_wire_fee (db_plugin->cls, - au_active.wa->method, - au_active.execution_time, + au->wa->method, + au->execution_time, &start_date, &end_date, - &au_active.fees, + &au->fees, &master_sig); if (0 >= qs) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Could not get wire fees for %s at %s. Aborting run.\n", - au_active.wa->method, - GNUNET_TIME_timestamp2s (au_active.execution_time)); + au->wa->method, + GNUNET_TIME_timestamp2s (au->execution_time)); global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - db_plugin->rollback (db_plugin->cls); - release_shard (s); - return; + return GNUNET_SYSERR; } } - /* Now try to find other deposits to aggregate */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Found ready deposit for %s, aggregating by target %s\n", - TALER_B2S (&au_active.merchant_pub), - au_active.payto_uri); - TALER_payto_hash (au_active.payto_uri, - &au_active.h_payto); - + TALER_B2S (&au->merchant_pub), + au->payto_uri); qs = db_plugin->select_aggregation_transient (db_plugin->cls, - &au_active.h_payto, - au_active.wa->section_name, - &au_active.wtid, - &trans); + &au->h_payto, + &au->merchant_pub, + au->wa->section_name, + &au->wtid, + &au->trans); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to lookup transient aggregates!\n"); - cleanup_au (&au_active); - db_plugin->rollback (db_plugin->cls); global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - release_shard (s); - return; + return GNUNET_SYSERR; case GNUNET_DB_STATUS_SOFT_ERROR: /* serializiability issue, try again */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Serialization issue, trying again later!\n"); - db_plugin->rollback (db_plugin->cls); - cleanup_au (&au_active); - GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&run_aggregation, - s); - return; + return GNUNET_NO; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, - &au_active.wtid, - sizeof (au_active.wtid)); - have_transient = false; + &au->wtid, + sizeof (au->wtid)); + au->have_transient = false; break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - have_transient = true; + au->have_transient = true; break; } qs = db_plugin->aggregate (db_plugin->cls, - &au_active.h_payto, - &au_active.merchant_pub, - &au_active.wtid, - &au_active.total_amount); + &au->h_payto, + &au->merchant_pub, + &au->wtid, + &au->total_amount); if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to execute aggregation!\n"); - cleanup_au (&au_active); - db_plugin->rollback (db_plugin->cls); global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - release_shard (s); - return; + return GNUNET_SYSERR; } if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { /* serializiability issue, try again */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Serialization issue, trying again later!\n"); - db_plugin->rollback (db_plugin->cls); - cleanup_au (&au_active); - GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&run_aggregation, - s); - return; + return GNUNET_NO; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Aggregation total is %s.\n", - TALER_amount2s (&au_active.total_amount)); + TALER_amount2s (&au->total_amount)); /* Subtract wire transfer fee and round to the unit supported by the wire transfer method; Check if after rounding down, we still have an amount to transfer, and if not mark as 'tiny'. */ - if (have_transient) + if (au->have_transient) GNUNET_assert (0 <= - TALER_amount_add (&au_active.total_amount, - &au_active.total_amount, - &trans)); + TALER_amount_add (&au->total_amount, + &au->total_amount, + &au->trans)); + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Rounding aggregate of %s\n", - TALER_amount2s (&au_active.total_amount)); + TALER_amount2s (&au->total_amount)); if ( (0 >= - TALER_amount_subtract (&au_active.final_amount, - &au_active.total_amount, - &au_active.fees.wire)) || + TALER_amount_subtract (&au->final_amount, + &au->total_amount, + &au->fees.wire)) || (GNUNET_SYSERR == - TALER_amount_round_down (&au_active.final_amount, + TALER_amount_round_down (&au->final_amount, ¤cy_round_unit)) || - (TALER_amount_is_zero (&au_active.final_amount)) ) + (TALER_amount_is_zero (&au->final_amount)) || + (! kyc_satisfied (au)) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Aggregate value too low for transfer (%d/%s)\n", + "Not ready for wire transfer (%d/%s)\n", qs, - TALER_amount2s (&au_active.final_amount)); - if (have_transient) + TALER_amount2s (&au->final_amount)); + if (au->have_transient) qs = db_plugin->update_aggregation_transient (db_plugin->cls, - &au_active.h_payto, - &au_active.wtid, - &au_active.total_amount); + &au->h_payto, + &au->wtid, + &au->total_amount); else qs = db_plugin->create_aggregation_transient (db_plugin->cls, - &au_active.h_payto, - au_active.wa->section_name, - &au_active.wtid, - &au_active.total_amount); + &au->h_payto, + au->wa->section_name, + &au->merchant_pub, + &au->wtid, + &au->total_amount); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Serialization issue, trying again later!\n"); - db_plugin->rollback (db_plugin->cls); - cleanup_au (&au_active); - /* start again */ - GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&run_aggregation, - s); - return; + return GNUNET_NO; } if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_break (0); - db_plugin->rollback (db_plugin->cls); - cleanup_au (&au_active); global_ret = EXIT_FAILURE; - GNUNET_SCHEDULER_shutdown (); - release_shard (s); - return; + return GNUNET_SYSERR; } /* commit */ - (void) commit_or_warn (); - cleanup_au (&au_active); - - /* start again */ - GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&run_aggregation, - s); - return; + return GNUNET_OK; } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Preparing wire transfer of %s to %s\n", - TALER_amount2s (&au_active.final_amount), - TALER_B2S (&au_active.merchant_pub)); - { - void *buf; - size_t buf_size; - TALER_BANK_prepare_transfer (au_active.payto_uri, - &au_active.final_amount, - exchange_base_url, - &au_active.wtid, - &buf, - &buf_size); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Storing %u bytes of wire prepare data\n", - (unsigned int) buf_size); - /* Commit our intention to execute the wire transfer! */ - qs = db_plugin->wire_prepare_data_insert (db_plugin->cls, - au_active.wa->method, - buf, - buf_size); - GNUNET_free (buf); + qs = trigger_wire_transfer (au); + switch (qs) + { + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Serialization issue during aggregation; trying again later!\n"); + return GNUNET_NO; + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + global_ret = EXIT_FAILURE; + return GNUNET_SYSERR; + default: + return GNUNET_OK; } - /* Commit the WTID data to 'wire_out' */ - if (qs >= 0) - qs = db_plugin->store_wire_transfer_out (db_plugin->cls, - au_active.execution_time, - &au_active.wtid, - &au_active.h_payto, - au_active.wa->section_name, - &au_active.final_amount); +} - if ( (qs >= 0) && - have_transient) - qs = db_plugin->delete_aggregation_transient (db_plugin->cls, - &au_active.h_payto, - &au_active.wtid); - cleanup_au (&au_active); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) +static void +run_aggregation (void *cls) +{ + struct Shard *s = cls; + struct AggregationUnit au_active; + enum GNUNET_DB_QueryStatus qs; + enum GNUNET_GenericReturnValue ret; + + task = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checking for ready deposits to aggregate\n"); + /* make sure we have current fees */ + memset (&au_active, + 0, + sizeof (au_active)); + au_active.execution_time = GNUNET_TIME_timestamp_get (); + if (GNUNET_OK != + db_plugin->start_deferred_wire_out (db_plugin->cls)) { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Serialization issue for prepared wire data; trying again later!\n"); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start database transaction!\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + release_shard (s); + return; + } + qs = db_plugin->get_ready_deposit ( + db_plugin->cls, + s->shard_start, + s->shard_end, + &au_active.merchant_pub, + &au_active.payto_uri); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + cleanup_au (&au_active); + db_plugin->rollback (db_plugin->cls); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to begin deposit iteration!\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + release_shard (s); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + cleanup_au (&au_active); db_plugin->rollback (db_plugin->cls); - /* start again */ GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_now (&run_aggregation, s); return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + { + uint64_t counter = s->work_counter; + struct GNUNET_TIME_Relative duration + = GNUNET_TIME_absolute_get_duration (s->start_time.abs_time); + + cleanup_au (&au_active); + db_plugin->rollback (db_plugin->cls); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Completed shard [%u,%u] after %s with %llu deposits\n", + (unsigned int) s->shard_start, + (unsigned int) s->shard_end, + GNUNET_TIME_relative2s (duration, + true), + (unsigned long long) counter); + release_shard (s); + if ( (GNUNET_YES == test_mode) && + (0 == counter) ) + { + /* in test mode, shutdown after a shard is done with 0 work */ + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_assert (NULL == task); + /* If we ended up doing zero work, sleep a bit */ + if (0 == counter) + task = GNUNET_SCHEDULER_add_delayed (aggregator_idle_sleep_interval, + &drain_kyc_alerts, + NULL); + else + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + } + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + s->work_counter++; + /* continued below */ + break; } - if (GNUNET_DB_STATUS_HARD_ERROR == qs) + + TALER_payto_hash (au_active.payto_uri, + &au_active.h_payto); + ret = do_aggregate (&au_active); + cleanup_au (&au_active); + switch (ret) { - GNUNET_break (0); - db_plugin->rollback (db_plugin->cls); - /* die hard */ - global_ret = EXIT_FAILURE; + case GNUNET_SYSERR: GNUNET_SCHEDULER_shutdown (); + db_plugin->rollback (db_plugin->cls); release_shard (s); return; + case GNUNET_NO: + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&run_aggregation, + s); + return; + case GNUNET_OK: + /* continued below */ + break; } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Stored wire transfer out instructions\n"); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Committing aggregation result\n"); /* Now we can finally commit the overall transaction, as we are again consistent if all of this passes. */ @@ -699,8 +811,8 @@ run_aggregation (void *cls) { case GNUNET_DB_STATUS_SOFT_ERROR: /* try again */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Commit issue for prepared wire data; trying again later!\n"); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Serialization issue on commit; trying again later!\n"); GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_now (&run_aggregation, s); @@ -714,7 +826,7 @@ run_aggregation (void *cls) return; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Preparation complete, going again\n"); + "Commit complete, going again\n"); GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_now (&run_aggregation, s); @@ -792,6 +904,197 @@ run_shard (void *cls) /** + * Function called on transient aggregations matching + * a particular hash of a payto URI. + * + * @param cls + * @param payto_uri corresponding payto URI + * @param wtid wire transfer identifier of transient aggregation + * @param merchant_pub public key of the merchant + * @param total amount aggregated so far + * @return true to continue to iterate + */ +static bool +handle_transient_cb ( + void *cls, + const char *payto_uri, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct TALER_Amount *total) +{ + struct AggregationUnit *au = cls; + + if (GNUNET_OK != au->ret) + { + GNUNET_break (0); + return false; + } + au->payto_uri = GNUNET_strdup (payto_uri); + au->wtid = *wtid; + au->merchant_pub = *merchant_pub; + au->trans = *total; + au->have_transient = true; + au->ret = do_aggregate (au); + GNUNET_free (au->payto_uri); + return (GNUNET_OK == au->ret); +} + + +static void +drain_kyc_alerts (void *cls) +{ + enum GNUNET_DB_QueryStatus qs; + struct AggregationUnit au; + + (void) cls; + task = NULL; + memset (&au, + 0, + sizeof (au)); + au.execution_time = GNUNET_TIME_timestamp_get (); + if (GNUNET_SYSERR == + db_plugin->preflight (db_plugin->cls)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to obtain database connection!\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + db_plugin->start (db_plugin->cls, + "handle kyc alerts")) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start database transaction!\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + while (1) + { + qs = db_plugin->drain_kyc_alert (db_plugin->cls, + 1, + &au.h_payto); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + qs = db_plugin->commit (db_plugin->cls); + if (qs < 0) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to commit KYC drain\n"); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&run_shard, + NULL); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* handled below */ + break; + } + + au.ret = GNUNET_OK; + qs = db_plugin->find_aggregation_transient (db_plugin->cls, + &au.h_payto, + &handle_transient_cb, + &au); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to lookup transient aggregates!\n"); + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + /* serializiability issue, try again */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Serialization issue, trying again later!\n"); + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + continue; /* while (1) */ + default: + break; + } + break; + } /* while(1) */ + + { + enum GNUNET_GenericReturnValue ret; + + ret = au.ret; + cleanup_au (&au); + switch (ret) + { + case GNUNET_SYSERR: + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + db_plugin->rollback (db_plugin->cls); /* just in case */ + return; + case GNUNET_NO: + db_plugin->rollback (db_plugin->cls); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_OK: + /* continued below */ + break; + } + } + + switch (commit_or_warn ()) + { + case GNUNET_DB_STATUS_SOFT_ERROR: + /* try again */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Serialization issue on commit; trying again later!\n"); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + db_plugin->rollback (db_plugin->cls); /* just in case */ + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Commit complete, going again\n"); + GNUNET_assert (NULL == task); + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, + NULL); + return; + default: + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + db_plugin->rollback (db_plugin->cls); /* just in case */ + return; + } +} + + +/** * First task. * * @param cls closure, NULL @@ -811,7 +1114,8 @@ run (void *cls, (void) cfgfile; cfg = c; - if (GNUNET_OK != parse_aggregator_config ()) + if (GNUNET_OK != + parse_aggregator_config ()) { cfg = NULL; global_ret = EXIT_NOTCONFIGURED; @@ -832,11 +1136,18 @@ run (void *cls, shard_size = 1U + INT32_MAX; else shard_size = (uint32_t) ass; + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_init (cfg)) + { + cfg = NULL; + global_ret = EXIT_NOTCONFIGURED; + return; + } + GNUNET_SCHEDULER_add_shutdown (&shutdown_task, + NULL); GNUNET_assert (NULL == task); - task = GNUNET_SCHEDULER_add_now (&run_shard, + task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, NULL); - GNUNET_SCHEDULER_add_shutdown (&shutdown_task, - cls); } diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c index fefded57..e7d07043 100644 --- a/src/exchange/taler-exchange-httpd.c +++ b/src/exchange/taler-exchange-httpd.c @@ -27,6 +27,7 @@ #include <sched.h> #include <sys/resource.h> #include <limits.h> +#include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_auditors.h" #include "taler-exchange-httpd_batch-deposit.h" @@ -1215,13 +1216,14 @@ handle_mhd_request (void *cls, .url = "kyc-check", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_kyc_check, - .nargs = 1 + .nargs = 2 }, { .url = "kyc-proof", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_kyc_proof, - .nargs = 1 + .nargs = 128, + .nargs_is_upper_bound = true }, { .url = "kyc-wallet", @@ -1680,6 +1682,11 @@ parse_kyc_oauth_cfg (void) static enum GNUNET_GenericReturnValue exchange_serve_process_config (void) { + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_init (TEH_cfg)) + { + return GNUNET_SYSERR; + } { char *kyc_mode; @@ -2094,8 +2101,12 @@ do_shutdown (void *cls) TEH_purses_get_cleanup (); TEH_kyc_check_cleanup (); TEH_kyc_proof_cleanup (); + TALER_KYCLOGIC_kyc_done (); if (NULL != mhd) + { MHD_stop_daemon (mhd); + mhd = NULL; + } TEH_wire_done (); TEH_extensions_done (); TEH_keys_finished (); diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.c b/src/exchange/taler-exchange-httpd_batch-withdraw.c index d306838b..9e24ff4c 100644 --- a/src/exchange/taler-exchange-httpd_batch-withdraw.c +++ b/src/exchange/taler-exchange-httpd_batch-withdraw.c @@ -27,6 +27,7 @@ #include <gnunet/gnunet_util_lib.h> #include <jansson.h> #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_batch-withdraw.h" #include "taler-exchange-httpd_responses.h" @@ -87,6 +88,17 @@ struct BatchWithdrawContext struct PlanchetContext *planchets; /** + * Hash of the payto-URI representing the reserve + * from which we are withdrawing. + */ + struct TALER_PaytoHashP h_payto; + + /** + * Current time for the DB transaction. + */ + struct GNUNET_TIME_Timestamp now; + + /** * Total amount from all coins with fees. */ struct TALER_Amount batch_total; @@ -100,6 +112,45 @@ struct BatchWithdrawContext /** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Called within a database transaction, so must + * not start a new one. + * + * @param cls closure, identifies the event type and + * account to iterate over events for + * @param limit maximum time-range for which events + * should be fetched (timestamp in the past) + * @param cb function to call on each event found, + * events must be returned in reverse chronological + * order + * @param cb_cls closure for @a cb + */ +static void +batch_withdraw_amount_cb (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct BatchWithdrawContext *wc = cls; + enum GNUNET_DB_QueryStatus qs; + + if (GNUNET_OK != + cb (cb_cls, + &wc->batch_total, + wc->now.abs_time)) + return; + qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( + TEH_plugin->cls, + &wc->h_payto, + limit, + cb, + cb_cls); + GNUNET_break (qs >= 0); +} + + +/** * Function implementing withdraw transaction. 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, @@ -127,15 +178,46 @@ batch_withdraw_transaction (void *cls, enum GNUNET_DB_QueryStatus qs; bool balance_ok = false; bool found = false; + const char *kyc_required; - now = GNUNET_TIME_timestamp_get (); + wc->now = GNUNET_TIME_timestamp_get (); + qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls, + wc->reserve_pub, + &wc->h_payto); + if (qs < 0) + return qs; + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + kyc_required = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, + &wc->h_payto, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &batch_withdraw_amount_cb, + wc); + if (NULL != kyc_required) + { + /* insert KYC requirement into DB! */ + wc->kyc.ok = false; + return TEH_plugin->insert_kyc_requirement_for_account ( + TEH_plugin->cls, + kyc_required, + &wc->h_payto, + &wc->kyc.payment_target_uuid); + } + wc->kyc.ok = true; qs = TEH_plugin->do_batch_withdraw (TEH_plugin->cls, now, wc->reserve_pub, &wc->batch_total, &found, &balance_ok, - &wc->kyc, &ruuid); if (0 > qs) { @@ -164,55 +246,6 @@ batch_withdraw_transaction (void *cls, return GNUNET_DB_STATUS_HARD_ERROR; } - if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && - (! wc->kyc.ok) && - (TALER_EXCHANGEDB_KYC_W2W == wc->kyc.type) ) - { - /* Wallet-to-wallet payments _always_ require KYC */ - *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, - GNUNET_JSON_pack_uint64 ("payment_target_uuid", - wc->kyc.payment_target_uuid)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && - (! wc->kyc.ok) && - (TALER_EXCHANGEDB_KYC_WITHDRAW == wc->kyc.type) && - (! GNUNET_TIME_relative_is_zero (TEH_kyc_config.withdraw_period)) ) - { - /* Withdraws require KYC if above threshold */ - enum GNUNET_DB_QueryStatus qs2; - bool below_limit; - - qs2 = TEH_plugin->do_withdraw_limit_check ( - TEH_plugin->cls, - ruuid, - GNUNET_TIME_absolute_subtract (now.abs_time, - TEH_kyc_config.withdraw_period), - &TEH_kyc_config.withdraw_limit, - &below_limit); - if (0 > qs2) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs2); - if (GNUNET_DB_STATUS_HARD_ERROR == qs2) - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_withdraw_limit_check"); - return qs2; - } - if (! below_limit) - { - *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, - GNUNET_JSON_pack_uint64 ("payment_target_uuid", - wc->kyc.payment_target_uuid)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - /* Add information about each planchet in the batch */ for (unsigned int i = 0; i<wc->planchets_length; i++) { @@ -291,6 +324,16 @@ generate_reply_success (const struct TEH_RequestContext *rc, { json_t *sigs; + if (! wc->kyc.ok) + { + /* KYC required */ + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + GNUNET_JSON_pack_uint64 ("payment_target_uuid", + wc->kyc.payment_target_uuid)); + } + sigs = json_array (); GNUNET_assert (NULL != sigs); for (unsigned int i = 0; i<wc->planchets_length; i++) @@ -633,7 +676,6 @@ TEH_handler_batch_withdraw (struct TEH_RequestContext *rc, TALER_amount_set_zero (TEH_currency, &wc.batch_total)); wc.reserve_pub = reserve_pub; - { enum GNUNET_GenericReturnValue res; diff --git a/src/exchange/taler-exchange-httpd_deposit.c b/src/exchange/taler-exchange-httpd_deposit.c index b7466939..0484ab07 100644 --- a/src/exchange/taler-exchange-httpd_deposit.c +++ b/src/exchange/taler-exchange-httpd_deposit.c @@ -184,7 +184,7 @@ deposit_transaction (void *cls, } if (in_conflict) { - /* FIXME #7267: conficting contract != insufficient funds */ + /* FIXME #7267: conflicting contract != insufficient funds */ *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, @@ -426,7 +426,7 @@ TEH_handler_deposit (struct MHD_Connection *connection, &h_wire, &deposit.h_contract_terms, &deposit.coin.h_age_commitment, - NULL /* h_extensions! */, + NULL /* FIXME: h_extensions! */, &deposit.coin.denom_pub_hash, deposit.timestamp, &deposit.merchant_pub, @@ -481,7 +481,7 @@ TEH_handler_deposit (struct MHD_Connection *connection, res = reply_deposit_success (connection, &deposit.coin.coin_pub, &h_wire, - NULL /* h_extensions! */, + NULL /* FIXME: h_extensions! */, &deposit.h_contract_terms, dc.exchange_timestamp, deposit.refund_deadline, diff --git a/src/exchange/taler-exchange-httpd_deposits_get.c b/src/exchange/taler-exchange-httpd_deposits_get.c index 97618a94..eec81569 100644 --- a/src/exchange/taler-exchange-httpd_deposits_get.c +++ b/src/exchange/taler-exchange-httpd_deposits_get.c @@ -40,12 +40,12 @@ struct DepositWtidContext /** * Hash over the proposal data of the contract for which this deposit is made. */ - struct TALER_PrivateContractHashP h_contract_terms GNUNET_PACKED; + struct TALER_PrivateContractHashP h_contract_terms; /** * Hash over the wiring information of the merchant. */ - struct TALER_MerchantWireHashP h_wire GNUNET_PACKED; + struct TALER_MerchantWireHashP h_wire; /** * The Merchant's public key. The deposit inquiry request is to be @@ -251,8 +251,12 @@ handle_track_transaction_request ( return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_uint64 ("payment_target_uuid", - ctx->kyc.payment_target_uuid), + GNUNET_JSON_pack_allow_null ( + (0 == ctx->kyc.payment_target_uuid) + ? GNUNET_JSON_pack_string ("legitimization_uuid", + NULL) + : GNUNET_JSON_pack_uint64 ("legitimization_uuid", + ctx->kyc.payment_target_uuid)), GNUNET_JSON_pack_bool ("kyc_ok", ctx->kyc.ok), GNUNET_JSON_pack_timestamp ("execution_time", diff --git a/src/exchange/taler-exchange-httpd_keys.c b/src/exchange/taler-exchange-httpd_keys.c index f9c35eee..729e627e 100644 --- a/src/exchange/taler-exchange-httpd_keys.c +++ b/src/exchange/taler-exchange-httpd_keys.c @@ -21,6 +21,7 @@ #include "platform.h" #include "taler_json_lib.h" #include "taler_mhd_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_dbevents.h" #include "taler-exchange-httpd.h" #include "taler-exchange-httpd_keys.h" @@ -1722,6 +1723,27 @@ setup_general_response_headers (struct TEH_KeyStateHandle *ksh, /** + * Function called with wallet balance threshholds. + * + * @param[in,out] cls a `json **` where to put the array of json amounts discovered + * @param threshold another threshold amount to add + */ +static void +wallet_threshold_cb (void *cls, + const struct TALER_Amount *threshold) +{ + json_t **ret = cls; + + if (NULL == *ret) + *ret = json_array (); + GNUNET_assert (0 == + json_array_append_new (*ret, + TALER_JSON_from_amount ( + threshold))); +} + + +/** * Initialize @a krd using the given values for @a signkeys, * @a recoup and @a denoms. * @@ -1854,17 +1876,20 @@ create_krd (struct TEH_KeyStateHandle *ksh, GNUNET_assert (NULL != keys); /* Set wallet limit if KYC is configured */ - if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && - (GNUNET_OK == - TALER_amount_is_valid (&TEH_kyc_config.wallet_balance_limit)) ) { - GNUNET_assert ( - 0 == - json_object_set_new ( - keys, - "wallet_balance_limit_without_kyc", - TALER_JSON_from_amount ( - &TEH_kyc_config.wallet_balance_limit))); + json_t *wblwk = NULL; + + TALER_KYCLOGIC_kyc_iterate_thresholds ( + TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE, + &wallet_threshold_cb, + &wblwk); + if (NULL != wblwk) + GNUNET_assert ( + 0 == + json_object_set_new ( + keys, + "wallet_balance_limit_without_kyc", + wblwk)); } /* Signal support for the configured, enabled extensions. */ diff --git a/src/exchange/taler-exchange-httpd_kyc-check.c b/src/exchange/taler-exchange-httpd_kyc-check.c index b4ec4197..61d1abf8 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-2022 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,6 +25,7 @@ #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" @@ -54,6 +55,17 @@ struct KycPoller 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. */ @@ -62,12 +74,7 @@ struct KycPoller /** * UUID being checked. */ - uint64_t auth_payment_target_uuid; - - /** - * Current KYC status. - */ - struct TALER_EXCHANGEDB_KycStatus kyc; + uint64_t legitimization_uuid; /** * Hash of the payto:// URI we are confirming to @@ -76,20 +83,45 @@ struct KycPoller struct TALER_PaytoHashP h_payto; /** - * Payto URL as a string, as given to us by t + * When will this request time out? */ - const char *hps; + struct GNUNET_TIME_Absolute timeout; /** - * When will this request time out? + * Type of KYC check required for this client. */ - struct GNUNET_TIME_Absolute timeout; + char *required; + + /** + * 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; + + /** + * Set to error encountered with KYC logic, if any. + */ + enum TALER_ErrorCode ec; /** * True if we are still suspended. */ bool suspended; + /** + * True if KYC was required but is fully satisfied. + */ + bool found; + + /** + * True if we once tried the KYC initiation. + */ + bool ih_done; + }; @@ -114,6 +146,11 @@ TEH_kyc_check_cleanup () 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; @@ -143,11 +180,84 @@ kyp_cleanup (struct TEH_RequestContext *rc) kyp->eh); kyp->eh = NULL; } + if (NULL != kyp->ih) + { + kyp->ih_logic->initiate_cancel (kyp->ih); + kyp->ih = NULL; + } + GNUNET_free (kyp->kyc_url); + GNUNET_free (kyp->hint); + GNUNET_free (kyp->required); 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 completed with status %d (%s)\n", + 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_requirement_by_row ( + TEH_plugin->cls, + kyp->legitimization_uuid, + kyp->required, + &kyp->h_payto, + provider_user_id, + provider_legitimization_id, + 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 @@ -168,21 +278,82 @@ kyc_check (void *cls, { struct KycPoller *kyp = cls; enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->select_kyc_status (TEH_plugin->cls, - &kyp->h_payto, - &kyp->kyc); + struct TALER_KYCLOGIC_ProviderDetails *pd; + enum GNUNET_GenericReturnValue ret; + struct TALER_PaytoHashP h_payto; + struct GNUNET_TIME_Absolute expiration; + char *provider_account_id; + char *provider_legitimization_id; + + qs = TEH_plugin->lookup_kyc_requirement_by_row ( + TEH_plugin->cls, + kyp->legitimization_uuid, + &kyp->required, + &h_payto, + &expiration, + &provider_account_id, + &provider_legitimization_id); + 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->legitimization_uuid); + return qs; + } if (qs < 0) { - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - GNUNET_break (0); + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + return qs; + } + GNUNET_free (provider_account_id); + GNUNET_free (provider_legitimization_id); + if (0 != + GNUNET_memcmp (&kyp->h_payto, + &h_payto)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Account %llu provided, but h_payto does not match\n", + (unsigned long long) kyp->legitimization_uuid); + GNUNET_break_op (0); *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "inselect_wallet_status"); + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED, + "h_payto"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + kyp->found = true; + if (GNUNET_TIME_absolute_is_future (expiration)) + { + /* kyc not required, we are done */ return qs; } + + ret = TALER_KYCLOGIC_kyc_get_logic (kyp->required, + &kyp->ih_logic, + &pd); + if (GNUNET_OK != ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC logic for `%s' not configured but used in database!\n", + kyp->required); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_GONE, + kyp->required); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (kyp->ih_done) + return qs; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Initiating KYC check with logic %s\n", + kyp->required); + kyp->ih = kyp->ih_logic->initiate (kyp->ih_logic->cls, + pd, + &h_payto, + kyp->legitimization_uuid, + &initiate_cb, + kyp); + GNUNET_break (NULL != kyp->ih); return qs; } @@ -230,7 +401,7 @@ db_event_cb (void *cls, MHD_RESULT TEH_handler_kyc_check ( struct TEH_RequestContext *rc, - const char *const args[]) + const char *const args[2]) { struct KycPoller *kyp = rc->rh_ctx; MHD_RESULT res; @@ -245,24 +416,37 @@ TEH_handler_kyc_check ( rc->rh_cleaner = &kyp_cleanup; { - // FIXME: now 'legitimization_uuid'! - unsigned long long payment_target_uuid; + unsigned long long legitimization_uuid; char dummy; if (1 != sscanf (args[0], "%llu%c", - &payment_target_uuid, + &legitimization_uuid, &dummy)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payment_target_uuid"); + "legitimization_uuid"); } - kyp->auth_payment_target_uuid = (uint64_t) payment_target_uuid; + kyp->legitimization_uuid = (uint64_t) legitimization_uuid; + } + + 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"); } + { const char *ts; @@ -291,41 +475,8 @@ TEH_handler_kyc_check ( tms)); } } - - // FIXME: replace with args[1]! - kyp->hps = MHD_lookup_connection_value (rc->connection, - MHD_GET_ARGUMENT_KIND, - "h_payto"); - if (NULL == kyp->hps) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "h_payto"); - } - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (kyp->hps, - strlen (kyp->hps), - &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 (TEH_KYC_NONE == TEH_kyc_config.mode) - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - if ( (NULL == kyp->eh) && GNUNET_TIME_absolute_is_future (kyp->timeout) ) { @@ -353,27 +504,48 @@ TEH_handler_kyc_check ( &kyc_check, kyp); if (GNUNET_SYSERR == ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Transaction failed.\n"); return res; + } - if (kyp->auth_payment_target_uuid != - kyp->kyc.payment_target_uuid) + if ( (NULL == kyp->ih) && + (! kyp->found) ) { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Account %llu provided, but payto %s is for %llu\n", - (unsigned long long) kyp->auth_payment_target_uuid, - kyp->hps, - (unsigned long long) kyp->kyc.payment_target_uuid); - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED, - "h_payto"); + /* KYC not required */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC not required %llu\n", + (unsigned long long) kyp->legitimization_uuid); + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + + if (NULL != kyp->ih) + { + 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; } /* long polling? */ - if ( (! kyp->kyc.ok) && + if ( (NULL != kyp->required) && 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_duration ( + kyp->timeout), + true)); GNUNET_assert (NULL != kyp->eh); kyp->suspended = true; GNUNET_CONTAINER_DLL_insert (kyp_head, @@ -383,37 +555,24 @@ TEH_handler_kyc_check ( return MHD_YES; } - /* KYC failed? */ - if (! kyp->kyc.ok) + /* KYC plugin generated reply? */ + if (NULL != kyp->kyc_url) { - 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/%s", - TEH_base_url, - kyp->hps); - redirect_uri_encoded = TALER_urlencode (redirect_uri); - GNUNET_free (redirect_uri); - GNUNET_asprintf (&url, - "%s?client_id=%s&redirect_uri=%s", - TEH_kyc_config.details.oauth2.login_url, - TEH_kyc_config.details.oauth2.client_id, - redirect_uri_encoded); - GNUNET_free (redirect_uri_encoded); - - res = TALER_MHD_REPLY_JSON_PACK ( + return TALER_MHD_REPLY_JSON_PACK ( rc->connection, MHD_HTTP_ACCEPTED, GNUNET_JSON_pack_string ("kyc_url", - url)); - GNUNET_free (url); - return res; + kyp->kyc_url)); + } + + if (TALER_EC_NONE != kyp->ec) + { + return TALER_MHD_reply_with_ec (rc->connection, + kyp->ec, + kyp->hint); } - /* KYC succeeded! */ + /* KYC must have succeeded! */ { struct TALER_ExchangePublicKeyP pub; struct TALER_ExchangeSignatureP sig; diff --git a/src/exchange/taler-exchange-httpd_kyc-proof.c b/src/exchange/taler-exchange-httpd_kyc-proof.c index 75ff81e9..64694d28 100644 --- a/src/exchange/taler-exchange-httpd_kyc-proof.c +++ b/src/exchange/taler-exchange-httpd_kyc-proof.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2021 Taler Systems SA + Copyright (C) 2021-2022 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,6 +25,7 @@ #include <microhttpd.h> #include <pthread.h> #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_kyc-proof.h" #include "taler-exchange-httpd_responses.h" @@ -52,30 +53,36 @@ struct KycProofContext struct TEH_RequestContext *rc; /** - * Handle for the OAuth 2.0 CURL request. + * Proof logic to run. */ - struct GNUNET_CURL_Job *job; + struct TALER_KYCLOGIC_Plugin *logic; /** - * OAuth 2.0 authorization code. + * Configuration for @a logic. */ - const char *authorization_code; + struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Asynchronous operation with the proof system. + */ + struct TALER_KYCLOGIC_ProofHandle *ph; /** - * OAuth 2.0 token URL we are using for the - * request. + * Process information about the user for the plugin from the database, can + * be NULL. */ - char *token_url; + char *provider_user_id; /** - * Body of the POST request. + * Process information about the legitimization process for the plugin from the + * database, can be NULL. */ - char *post_body; + char *provider_legitimization_id; /** - * User ID extracted from the OAuth 2.0 service, or NULL. + * OAuth 2.0 authorization code. */ - char *id; + const char *authorization_code; /** * Hash of payment target URI this is about. @@ -88,16 +95,24 @@ struct KycProofContext struct MHD_Response *response; /** + * Configuration section for the logic we are running. + */ + char *provider_section; + + /** + * Row in the database for this legitimization operation. + */ + uint64_t legi_row; + + /** * HTTP response code to return. */ unsigned int response_code; /** - * #GNUNET_YES if we are suspended, - * #GNUNET_NO if not. - * #GNUNET_SYSERR if we had some error. + * True if we are suspended, */ - enum GNUNET_GenericReturnValue suspended; + bool suspended; }; @@ -122,7 +137,7 @@ static void kpc_resume (struct KycProofContext *kpc) { GNUNET_assert (GNUNET_YES == kpc->suspended); - kpc->suspended = GNUNET_NO; + kpc->suspended = false; GNUNET_CONTAINER_DLL_remove (kpc_head, kpc_tail, kpc); @@ -138,10 +153,10 @@ TEH_kyc_proof_cleanup (void) while (NULL != (kpc = kpc_head)) { - if (NULL != kpc->job) + if (NULL != kpc->ph) { - GNUNET_CURL_job_cancel (kpc->job); - kpc->job = NULL; + kpc->logic->proof_cancel (kpc->ph); + kpc->ph = NULL; } kpc_resume (kpc); } @@ -149,348 +164,69 @@ TEH_kyc_proof_cleanup (void) /** - * Function implementing database transaction to check proof'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 - * transaction logic MUST queue a MHD response and set @a mhd_ret. IF it - * returns the soft error code, the function MAY be called again to retry and - * MUST not queue a MHD response. + * Function called with the result of a proof check operation. * - * @param cls closure with a `struct KycProofContext *` - * @param connection MHD proof which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!) - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -persist_kyc_ok (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct KycProofContext *kpc = cls; - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->set_kyc_ok (TEH_plugin->cls, - &kpc->h_payto, - kpc->id); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_STORE_FAILED, - "set_kyc_ok"); - } - return qs; -} - - -/** - * The request for @a kpc failed. We may have gotten a useful error - * message in @a j. Generate a failure response. + * Note that the "decref" for the @a response + * will be done by the callee and MUST NOT be done by the plugin. * - * @param[in,out] kpc request that failed - * @param j reply from the server (or NULL) + * @param cls closure + * @param status KYC status + * @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 http_status HTTP status code of @a response + * @param[in] response to return to the HTTP client */ static void -handle_error (struct KycProofContext *kpc, - const json_t *j) +proof_cb ( + void *cls, + enum TALER_KYCLOGIC_KycStatus status, + const char *provider_user_id, + const char *provider_legitimization_id, + struct GNUNET_TIME_Absolute expiration, + unsigned int http_status, + struct MHD_Response *response) { - const char *msg; - const char *desc; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("error", - &msg), - GNUNET_JSON_spec_string ("error_description", - &desc), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - const char *emsg; - unsigned int line; - - res = GNUNET_JSON_parse (j, - spec, - &emsg, - &line); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - return; - } - } - /* case TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_AUTHORZATION_FAILED, - we MAY want to in the future look at the requested content type - and possibly respond in JSON if indicated. */ - { - char *reply; - - GNUNET_asprintf (&reply, - "<html><head><title>%s</title></head><body><h1>%s</h1><p>%s</p></body></html>", - msg, - msg, - desc); - kpc->response - = MHD_create_response_from_buffer (strlen (reply), - reply, - MHD_RESPMEM_MUST_COPY); - GNUNET_assert (NULL != kpc->response); - GNUNET_free (reply); - } - kpc->response_code = MHD_HTTP_FORBIDDEN; -} + struct KycProofContext *kpc = cls; + struct TEH_RequestContext *rc = kpc->rc; + struct GNUNET_AsyncScopeSave old_scope; + kpc->ph = NULL; + GNUNET_async_scope_enter (&rc->async_scope_id, + &old_scope); -/** - * The request for @a kpc succeeded (presumably). - * Parse the user ID and store it in @a kpc (if possible). - * - * @param[in,out] kpc request that succeeded - * @param j reply from the server - */ -static void -parse_success_reply (struct KycProofContext *kpc, - const json_t *j) -{ - const char *state; - json_t *data; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("status", - &state), - GNUNET_JSON_spec_json ("data", - &data), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - const char *emsg; - unsigned int line; - - res = GNUNET_JSON_parse (j, - spec, - &emsg, - &line); - if (GNUNET_OK != res) + if (TALER_KYCLOGIC_STATUS_SUCCESS == status) { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - return; - } - if (0 != strcasecmp (state, - "success")) - { - GNUNET_break_op (0); - handle_error (kpc, - j); - return; - } - { - const char *id; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("id", - &id), - GNUNET_JSON_spec_end () - }; - - res = GNUNET_JSON_parse (data, - ispec, - &emsg, - &line); - if (GNUNET_OK != res) + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->update_kyc_requirement_by_row (TEH_plugin->cls, + kpc->legi_row, + kpc->provider_section, + &kpc->h_payto, + provider_user_id, + provider_legitimization_id, + expiration); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; + GNUNET_break (0); + kpc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + kpc->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "set_kyc_ok"); + GNUNET_async_scope_restore (&old_scope); return; } - kpc->id = GNUNET_strdup (id); - } -} - - -/** - * After we are done with the CURL interaction we - * need to update our database state with the information - * retrieved. - * - * @param cls our `struct KycProofContext` - * @param response_code HTTP response code from server, 0 on hard error - * @param response in JSON, NULL if response was not in JSON format - */ -static void -handle_curl_fetch_finished (void *cls, - long response_code, - const void *response) -{ - struct KycProofContext *kpc = cls; - const json_t *j = response; - - kpc->job = NULL; - switch (response_code) - { - case MHD_HTTP_OK: - parse_success_reply (kpc, - j); - break; - default: - handle_error (kpc, - j); - break; } - kpc_resume (kpc); -} - - -/** - * After we are done with the CURL interaction we - * need to fetch the user's account details. - * - * @param cls our `struct KycProofContext` - * @param response_code HTTP response code from server, 0 on hard error - * @param response in JSON, NULL if response was not in JSON format - */ -static void -handle_curl_login_finished (void *cls, - long response_code, - const void *response) -{ - struct KycProofContext *kpc = cls; - const json_t *j = response; - - kpc->job = NULL; - switch (response_code) + else { - case MHD_HTTP_OK: - { - const char *access_token; - const char *token_type; - uint64_t expires_in_s; - const char *refresh_token; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("access_token", - &access_token), - GNUNET_JSON_spec_string ("token_type", - &token_type), - GNUNET_JSON_spec_uint64 ("expires_in", - &expires_in_s), - GNUNET_JSON_spec_string ("refresh_token", - &refresh_token), - GNUNET_JSON_spec_end () - }; - CURL *eh; - - { - enum GNUNET_GenericReturnValue res; - const char *emsg; - unsigned int line; - - res = GNUNET_JSON_parse (j, - spec, - &emsg, - &line); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - break; - } - } - if (0 != strcasecmp (token_type, - "bearer")) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected token type in response from KYC gateway"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - break; - } - - /* We guard against a few characters that could - conceivably be abused to mess with the HTTP header */ - if ( (NULL != strchr (access_token, - '\n')) || - (NULL != strchr (access_token, - '\r')) || - (NULL != strchr (access_token, - ' ')) || - (NULL != strchr (access_token, - ';')) ) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Illegal character in access token"); - kpc->response_code - = MHD_HTTP_BAD_GATEWAY; - break; - } - - eh = curl_easy_init (); - if (NULL == eh) - { - GNUNET_break_op (0); - kpc->response - = TALER_MHD_make_error ( - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "curl_easy_init"); - kpc->response_code - = MHD_HTTP_INTERNAL_SERVER_ERROR; - break; - } - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_URL, - TEH_kyc_config.details.oauth2.info_url)); - { - char *hdr; - struct curl_slist *slist; - - GNUNET_asprintf (&hdr, - "%s: Bearer %s", - MHD_HTTP_HEADER_AUTHORIZATION, - access_token); - slist = curl_slist_append (NULL, - hdr); - kpc->job = GNUNET_CURL_job_add2 (TEH_curl_ctx, - eh, - slist, - &handle_curl_fetch_finished, - kpc); - curl_slist_free_all (slist); - GNUNET_free (hdr); - } - return; - } - default: - handle_error (kpc, - j); - break; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC logic #%llu failed with status %d\n", + (unsigned long long) kpc->legi_row, + status); } + kpc->response_code = http_status; + kpc->response = response; kpc_resume (kpc); + GNUNET_async_scope_restore (&old_scope); } @@ -504,19 +240,19 @@ clean_kpc (struct TEH_RequestContext *rc) { struct KycProofContext *kpc = rc->rh_ctx; - if (NULL != kpc->job) + if (NULL != kpc->ph) { - GNUNET_CURL_job_cancel (kpc->job); - kpc->job = NULL; + kpc->logic->proof_cancel (kpc->ph); + kpc->ph = NULL; } if (NULL != kpc->response) { MHD_destroy_response (kpc->response); kpc->response = NULL; } - GNUNET_free (kpc->post_body); - GNUNET_free (kpc->token_url); - GNUNET_free (kpc->id); + GNUNET_free (kpc->provider_user_id); + GNUNET_free (kpc->provider_legitimization_id); + GNUNET_free (kpc->provider_section); GNUNET_free (kpc); } @@ -529,7 +265,18 @@ TEH_handler_kyc_proof ( struct KycProofContext *kpc = rc->rh_ctx; if (NULL == kpc) - { /* first time */ + { + /* first time */ + if ( (NULL == args[0]) || + (NULL == args[1]) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + "'/kyc-proof/$H_PATYO/$LOGIC' required"); + } + kpc = GNUNET_new (struct KycProofContext); kpc->rc = rc; rc->rh_ctx = kpc; @@ -546,166 +293,98 @@ TEH_handler_kyc_proof ( TALER_EC_GENERIC_PARAMETER_MALFORMED, "h_payto"); } - kpc->authorization_code - = MHD_lookup_connection_value (rc->connection, - MHD_GET_ARGUMENT_KIND, - "code"); - if (NULL == kpc->authorization_code) + kpc->provider_section = GNUNET_strdup (args[1]); + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_get_logic (kpc->provider_section, + &kpc->logic, + &kpc->pd)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "code"); + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, + kpc->provider_section); } - if (TEH_KYC_NONE == TEH_kyc_config.mode) - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); { - CURL *eh; - - eh = curl_easy_init (); - if (NULL == eh) + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute expiration; + + qs = TEH_plugin->lookup_kyc_requirement_by_account ( + TEH_plugin->cls, + kpc->provider_section, + &kpc->h_payto, + &kpc->legi_row, + &expiration, + &kpc->provider_user_id, + &kpc->provider_legitimization_id); + switch (qs) { - GNUNET_break (0); + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + return TALER_MHD_reply_with_ec (rc->connection, + TALER_EC_GENERIC_DB_STORE_FAILED, + "lookup_kyc_requirement_by_account"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "curl_easy_init"); + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, + kpc->provider_section); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; } - GNUNET_asprintf (&kpc->token_url, - "%s", - TEH_kyc_config.details.oauth2.auth_url); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_URL, - kpc->token_url)); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_POST, - 1)); + if (GNUNET_TIME_absolute_is_future (expiration)) { - char *client_id; - char *redirect_uri; - char *client_secret; - char *authorization_code; - - client_id = curl_easy_escape (eh, - TEH_kyc_config.details.oauth2.client_id, - 0); - GNUNET_assert (NULL != client_id); - { - char *request_uri; - - GNUNET_asprintf (&request_uri, - "%s?client_id=%s", - TEH_kyc_config.details.oauth2.login_url, - TEH_kyc_config.details.oauth2.client_id); - redirect_uri = curl_easy_escape (eh, - request_uri, - 0); - GNUNET_free (request_uri); - } - GNUNET_assert (NULL != redirect_uri); - client_secret = curl_easy_escape (eh, - TEH_kyc_config.details.oauth2. - client_secret, - 0); - GNUNET_assert (NULL != client_secret); - authorization_code = curl_easy_escape (eh, - kpc->authorization_code, - 0); - GNUNET_assert (NULL != authorization_code); - GNUNET_asprintf (&kpc->post_body, - "client_id=%s&redirect_uri=%s&client_secret=%s&code=%s&grant_type=authorization_code", - client_id, - redirect_uri, - client_secret, - authorization_code); - curl_free (authorization_code); - curl_free (client_secret); - curl_free (redirect_uri); - curl_free (client_id); + /* KYC not required */ + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); } - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_POSTFIELDS, - kpc->post_body)); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_FOLLOWLOCATION, - 1L)); - /* limit MAXREDIRS to 5 as a simple security measure against - a potential infinite loop caused by a malicious target */ - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_MAXREDIRS, - 5L)); - - kpc->job = GNUNET_CURL_job_add (TEH_curl_ctx, - eh, - &handle_curl_login_finished, - kpc); - kpc->suspended = GNUNET_YES; - GNUNET_CONTAINER_DLL_insert (kpc_head, - kpc_tail, - kpc); - MHD_suspend_connection (rc->connection); - return MHD_YES; } - } + kpc->ph = kpc->logic->proof (kpc->logic->cls, + kpc->pd, + &args[2], + rc->connection, + &kpc->h_payto, + kpc->legi_row, + kpc->provider_user_id, + kpc->provider_legitimization_id, + &proof_cb, + kpc); + if (NULL == kpc->ph) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "could not start proof with KYC logic"); + } - if (NULL != kpc->response) - { - /* handle _failed_ resumed cases */ - return MHD_queue_response (rc->connection, - kpc->response_code, - kpc->response); - } - /* _successfully_ resumed case */ - { - MHD_RESULT res; - enum GNUNET_GenericReturnValue ret; - - ret = TEH_DB_run_transaction (kpc->rc->connection, - "check proof kyc", - TEH_MT_REQUEST_OTHER, - &res, - &persist_kyc_ok, - kpc); - if (GNUNET_SYSERR == ret) - return res; + kpc->suspended = true; + GNUNET_CONTAINER_DLL_insert (kpc_head, + kpc_tail, + kpc); + MHD_suspend_connection (rc->connection); + return MHD_YES; } + if (NULL == kpc->response) { - struct MHD_Response *response; - MHD_RESULT res; - - response = MHD_create_response_from_buffer (0, - "", - MHD_RESPMEM_PERSISTENT); - if (NULL == response) - { - GNUNET_break (0); - return MHD_NO; - } - GNUNET_break (MHD_YES == - MHD_add_response_header ( - response, - MHD_HTTP_HEADER_LOCATION, - TEH_kyc_config.details.oauth2.post_kyc_redirect_url)); - res = MHD_queue_response (rc->connection, - MHD_HTTP_SEE_OTHER, - response); - MHD_destroy_response (response); - return res; + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "handler resumed without response"); } + + /* return response from KYC logic */ + return MHD_queue_response (rc->connection, + kpc->response_code, + kpc->response); } diff --git a/src/exchange/taler-exchange-httpd_kyc-wallet.c b/src/exchange/taler-exchange-httpd_kyc-wallet.c index 0d92efd3..a043de6f 100644 --- a/src/exchange/taler-exchange-httpd_kyc-wallet.c +++ b/src/exchange/taler-exchange-httpd_kyc-wallet.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2021 Taler Systems SA + Copyright (C) 2021, 2022 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 @@ -26,6 +26,7 @@ #include <pthread.h> #include "taler_json_lib.h" #include "taler_mhd_lib.h" +#include "taler_kyclogic_lib.h" #include "taler-exchange-httpd_kyc-wallet.h" #include "taler-exchange-httpd_responses.h" @@ -38,16 +39,55 @@ struct KycRequestContext /** * Public key of the reserve/wallet this is about. */ - struct TALER_ReservePublicKeyP reserve_pub; + struct TALER_PaytoHashP h_payto; /** - * Current KYC status. + * Row with the legitimization requirement. */ - struct TALER_EXCHANGEDB_KycStatus kyc; + uint64_t legi_row; + + /** + * Balance threshold crossed by the wallet. + */ + struct TALER_Amount balance; + + /** + * Name of the required check. + */ + const char *required; + }; /** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Returns the wallet balance. + * + * @param cls closure, a `struct KycRequestContext` + * @param limit maximum time-range for which events + * should be fetched (timestamp in the past) + * @param cb function to call on each event found, + * events must be returned in reverse chronological + * order + * @param cb_cls closure for @a cb + */ +static void +balance_iterator (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct KycRequestContext *krc = cls; + + (void) limit; + cb (cb_cls, + &krc->balance, + GNUNET_TIME_absolute_get ()); +} + + +/** * 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 @@ -69,9 +109,23 @@ wallet_kyc_check (void *cls, struct KycRequestContext *krc = cls; enum GNUNET_DB_QueryStatus qs; - qs = TEH_plugin->inselect_wallet_kyc_status (TEH_plugin->cls, - &krc->reserve_pub, - &krc->kyc); + krc->required = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE, + &krc->h_payto, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &balance_iterator, + krc); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "KYC check required at %s is `%s'\n", + TALER_amount2s (&krc->balance), + krc->required); + if (NULL == krc->required) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + qs = TEH_plugin->insert_kyc_requirement_for_account (TEH_plugin->cls, + krc->required, + &krc->h_payto, + &krc->legi_row); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -80,9 +134,14 @@ wallet_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"); + "insert_kyc_requirement_for_account"); return qs; } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC requirement inserted for wallet %s (%llu, %d)\n", + TALER_B2S (&krc->h_payto), + (unsigned long long) krc->legi_row, + qs); return qs; } @@ -95,11 +154,17 @@ TEH_handler_kyc_wallet ( { struct TALER_ReserveSignatureP reserve_sig; struct KycRequestContext krc; + struct TALER_ReservePublicKeyP reserve_pub; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("reserve_sig", &reserve_sig), GNUNET_JSON_spec_fixed_auto ("reserve_pub", - &krc.reserve_pub), + &reserve_pub), + // FIXME: add balance threshold crossed to the request + // to spec and client API! + TALER_JSON_spec_amount ("balance", + TEH_currency, + &krc.balance), GNUNET_JSON_spec_end () }; MHD_RESULT res; @@ -115,8 +180,10 @@ TEH_handler_kyc_wallet ( return MHD_YES; /* failure */ TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + // FIXME: add balance threshold crossed to + // what the wallet signs over! if (GNUNET_OK != - TALER_wallet_account_setup_verify (&krc.reserve_pub, + TALER_wallet_account_setup_verify (&reserve_pub, &reserve_sig)) { GNUNET_break_op (0); @@ -126,13 +193,19 @@ TEH_handler_kyc_wallet ( TALER_EC_EXCHANGE_KYC_WALLET_SIGNATURE_INVALID, NULL); } - if (TEH_KYC_NONE == TEH_kyc_config.mode) - return TALER_MHD_reply_static ( - rc->connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); + { + char *payto_uri; + + payto_uri = TALER_reserve_make_payto (TEH_base_url, + &reserve_pub); + TALER_payto_hash (payto_uri, + &krc.h_payto); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "h_payto of wallet %s is %s\n", + payto_uri, + TALER_B2S (&krc.h_payto)); + GNUNET_free (payto_uri); + } ret = TEH_DB_run_transaction (rc->connection, "check wallet kyc", TEH_MT_REQUEST_OTHER, @@ -141,11 +214,21 @@ TEH_handler_kyc_wallet ( &krc); if (GNUNET_SYSERR == ret) return res; + if (NULL == krc.required) + { + /* KYC not required or already satisfied */ + return TALER_MHD_reply_static ( + rc->connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } return TALER_MHD_REPLY_JSON_PACK ( rc->connection, MHD_HTTP_OK, GNUNET_JSON_pack_uint64 ("payment_target_uuid", - krc.kyc.payment_target_uuid)); + krc.legi_row)); } diff --git a/src/exchange/taler-exchange-httpd_purses_merge.c b/src/exchange/taler-exchange-httpd_purses_merge.c index 07bcff17..25d91e1b 100644 --- a/src/exchange/taler-exchange-httpd_purses_merge.c +++ b/src/exchange/taler-exchange-httpd_purses_merge.c @@ -421,11 +421,11 @@ TEH_handler_purses_merge ( "Received payto: `%s'\n", pcc.payto_uri); if ( (0 != strncmp (pcc.payto_uri, - "payto://taler/", - strlen ("payto://taler/"))) && + "payto://taler-reserve/", + strlen ("payto://taler-reserve/"))) && (0 != strncmp (pcc.payto_uri, - "payto://taler+http/", - strlen ("payto://taler+http/"))) ) + "payto://taler-reserve+http/", + strlen ("payto://taler-reserve+http/"))) ) { GNUNET_break_op (0); return TALER_MHD_reply_with_error ( @@ -436,13 +436,13 @@ TEH_handler_purses_merge ( } http = (0 == strncmp (pcc.payto_uri, - "payto://taler+http/", - strlen ("payto://taler+http/"))); + "payto://taler-reserve+http/", + strlen ("payto://taler-reserve+http/"))); { const char *host = &pcc.payto_uri[http - ? strlen ("payto://taler+http/") - : strlen ("payto://taler/")]; + ? strlen ("payto://taler-reserve+http/") + : strlen ("payto://taler-reserve/")]; const char *slash = strchr (host, '/'); diff --git a/src/exchange/taler-exchange-httpd_withdraw.c b/src/exchange/taler-exchange-httpd_withdraw.c index ee2f410d..255f7d94 100644 --- a/src/exchange/taler-exchange-httpd_withdraw.c +++ b/src/exchange/taler-exchange-httpd_withdraw.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2021 Taler Systems SA + Copyright (C) 2014-2022 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 @@ -27,6 +27,7 @@ #include <gnunet/gnunet_util_lib.h> #include <jansson.h> #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_withdraw.h" #include "taler-exchange-httpd_responses.h" @@ -45,15 +46,6 @@ struct WithdrawContext struct TALER_BlindedCoinHashP h_coin_envelope; /** - * Value of the coin being exchanged (matching the denomination key) - * plus the transaction fee. We include this in what is being - * signed so that we can verify a reserve's remaining total balance - * without needing to access the respective denomination key - * information each time. - */ - struct TALER_Amount amount_with_fee; - - /** * Blinded planchet. */ struct TALER_BlindedPlanchet blinded_planchet; @@ -68,10 +60,67 @@ struct WithdrawContext */ struct TALER_EXCHANGEDB_KycStatus kyc; + /** + * Hash of the payto-URI representing the reserve + * from which we are withdrawing. + */ + struct TALER_PaytoHashP h_payto; + + /** + * Current time for the DB transaction. + */ + struct GNUNET_TIME_Timestamp now; + }; /** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Called within a database transaction, so must + * not start a new one. + * + * @param cls closure, identifies the event type and + * account to iterate over events for + * @param limit maximum time-range for which events + * should be fetched (timestamp in the past) + * @param cb function to call on each event found, + * events must be returned in reverse chronological + * order + * @param cb_cls closure for @a cb + */ +static void +withdraw_amount_cb (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct WithdrawContext *wc = cls; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Signaling amount %s for KYC check\n", + TALER_amount2s (&wc->collectable.amount_with_fee)); + if (GNUNET_OK != + cb (cb_cls, + &wc->collectable.amount_with_fee, + wc->now.abs_time)) + return; + qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( + TEH_plugin->cls, + &wc->h_payto, + limit, + cb, + cb_cls); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got %d additional transactions for this withdrawal and limit %llu\n", + qs, + (unsigned long long) limit.abs_value_us); + GNUNET_break (qs >= 0); +} + + +/** * Function implementing withdraw transaction. 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, @@ -98,25 +147,55 @@ withdraw_transaction (void *cls, bool found = false; bool balance_ok = false; bool nonce_ok = false; - struct GNUNET_TIME_Timestamp now; uint64_t ruuid; const struct TALER_CsNonce *nonce; const struct TALER_BlindedPlanchet *bp; + const char *kyc_required; - now = GNUNET_TIME_timestamp_get (); + wc->now = GNUNET_TIME_timestamp_get (); + qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls, + &wc->collectable.reserve_pub, + &wc->h_payto); + if (qs < 0) + return qs; + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + kyc_required = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, + &wc->h_payto, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &withdraw_amount_cb, + wc); + if (NULL != kyc_required) + { + /* insert KYC requirement into DB! */ + wc->kyc.ok = false; + return TEH_plugin->insert_kyc_requirement_for_account ( + TEH_plugin->cls, + kyc_required, + &wc->h_payto, + &wc->kyc.payment_target_uuid); + } + wc->kyc.ok = true; bp = &wc->blinded_planchet; - nonce = - (TALER_DENOMINATION_CS == bp->cipher) + nonce = (TALER_DENOMINATION_CS == bp->cipher) ? &bp->details.cs_blinded_planchet.nonce : NULL; qs = TEH_plugin->do_withdraw (TEH_plugin->cls, nonce, &wc->collectable, - now, + wc->now, &found, &balance_ok, &nonce_ok, - &wc->kyc, &ruuid); if (0 > qs) { @@ -153,56 +232,8 @@ withdraw_transaction (void *cls, NULL); return GNUNET_DB_STATUS_HARD_ERROR; } - if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && - (! wc->kyc.ok) && - (TALER_EXCHANGEDB_KYC_W2W == wc->kyc.type) ) - { - /* Wallet-to-wallet payments _always_ require KYC */ - *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, - GNUNET_JSON_pack_uint64 ("payment_target_uuid", - wc->kyc.payment_target_uuid)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && - (! wc->kyc.ok) && - (TALER_EXCHANGEDB_KYC_WITHDRAW == wc->kyc.type) && - (! GNUNET_TIME_relative_is_zero (TEH_kyc_config.withdraw_period)) ) - { - /* Withdraws require KYC if above threshold */ - enum GNUNET_DB_QueryStatus qs2; - bool below_limit; - - qs2 = TEH_plugin->do_withdraw_limit_check ( - TEH_plugin->cls, - ruuid, - GNUNET_TIME_absolute_subtract (now.abs_time, - TEH_kyc_config.withdraw_period), - &TEH_kyc_config.withdraw_limit, - &below_limit); - if (0 > qs2) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs2); - if (GNUNET_DB_STATUS_HARD_ERROR == qs2) - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_withdraw_limit_check"); - return qs2; - } - if (! below_limit) - { - *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, - GNUNET_JSON_pack_uint64 ("payment_target_uuid", - wc->kyc.payment_target_uuid)); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - TEH_METRICS_num_success[TEH_MT_SUCCESS_BATCH_WITHDRAW]++; + TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW]++; return qs; } @@ -274,7 +305,6 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc, 0, sizeof (wc)); wc.collectable.reserve_pub = *reserve_pub; - { enum GNUNET_GenericReturnValue res; @@ -459,6 +489,14 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc, /* Clean up and send back final response */ GNUNET_JSON_parse_free (spec); + if (! wc.kyc.ok) + { + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + GNUNET_JSON_pack_uint64 ("payment_target_uuid", + wc.kyc.payment_target_uuid)); + } { MHD_RESULT ret; |