exchange

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

commit b40237ea89dc17e952ff067f95679ddf3205d687
parent 7c8cc2e03068f420a425f4756cda78f4294199ed
Author: Florian Dold <dold@taler.net>
Date:   Mon, 24 Aug 2026 22:51:33 +0200

exchange KYC: restart expired provider processes

Diffstat:
Msrc/exchange/taler-exchange-httpd_post-kyc-start-ID.c | 327++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Asrc/exchangedb/complete_legitimization_process_start.c | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/do_reserve_legitimization_process.c | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/do_reserve_legitimization_process.sql | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/exchangedb/meson.build | 2++
Asrc/exchangedb/sql-schema/exchange-0015.sql | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/exchangedb/sql-schema/meson.build | 2++
Msrc/exchangedb/test_legitimization_processes.c | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/include/exchange-database/complete_legitimization_process_start.h | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/include/exchange-database/do_reserve_legitimization_process.h | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler/taler_kyclogic_lib.h | 12++++++++++++
Msrc/include/taler/taler_kyclogic_plugin.h | 14++++++++++++++
Msrc/kyclogic/kyclogic-kycaid.conf | 4++++
Msrc/kyclogic/kyclogic-oauth2.conf | 4++++
Msrc/kyclogic/kyclogic-persona.conf | 7+++++--
Msrc/kyclogic/kyclogic_api.c | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/kyclogic/plugin_kyclogic_oauth2.c | 37+++++++++++++++++++++++++++++++++++++
17 files changed, 1001 insertions(+), 86 deletions(-)

diff --git a/src/exchange/taler-exchange-httpd_post-kyc-start-ID.c b/src/exchange/taler-exchange-httpd_post-kyc-start-ID.c @@ -29,9 +29,8 @@ #include "taler-exchange-httpd_get-keys.h" #include "taler-exchange-httpd_post-kyc-start-ID.h" #include "taler-exchange-httpd_responses.h" -#include "exchange-database/update_legitimization_process_by_row.h" -#include "exchange-database/get_pending_legitimization_process.h" -#include "exchange-database/insert_legitimization_process.h" +#include "exchange-database/complete_legitimization_process_start.h" +#include "exchange-database/do_reserve_legitimization_process.h" #include "exchange-database/get_pending_legitimization.h" @@ -83,6 +82,26 @@ struct KycPoller struct TALER_KYCLOGIC_InitiateHandle *ih; /** + * Provider configuration for @e ih_logic. + */ + struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Provider selected for this process. + */ + const struct TALER_KYCLOGIC_KycProvider *provider; + + /** + * Context passed to the provider initiation. + */ + const json_t *context; + + /** + * Task used while another request owns the initiation lease. + */ + struct GNUNET_SCHEDULER_Task *retry_task; + + /** * Set of applicable KYC measures. */ json_t *jmeasures; @@ -113,6 +132,11 @@ struct KycPoller uint64_t process_row; /** + * Generation of the provider initiation stored in @e process_row. + */ + struct GNUNET_TIME_Absolute process_start_time; + + /** * Index of the measure this upload is for. */ unsigned int measure_index; @@ -128,6 +152,11 @@ struct KycPoller bool suspended; /** + * True while waiting to retry the atomic process reservation. + */ + bool waiting_for_process; + + /** * True if @e h_payto is for a wallet */ bool is_wallet; @@ -161,6 +190,11 @@ TEH_kyc_start_cleanup () kyp->ih_logic->initiate_cancel (kyp->ih); kyp->ih = NULL; } + if (NULL != kyp->retry_task) + { + GNUNET_SCHEDULER_cancel (kyp->retry_task); + kyp->retry_task = NULL; + } if (kyp->suspended) { kyp->suspended = false; @@ -187,6 +221,11 @@ kyp_cleanup (struct TEH_RequestContext *rc) kyp->ih_logic->initiate_cancel (kyp->ih); kyp->ih = NULL; } + if (NULL != kyp->retry_task) + { + GNUNET_SCHEDULER_cancel (kyp->retry_task); + kyp->retry_task = NULL; + } GNUNET_free (kyp->redirect_url); GNUNET_free (kyp->hint); json_decref (kyp->jmeasures); @@ -216,6 +255,44 @@ initiate_cb ( { struct KycPoller *kyp = cls; enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute process_expiration + = GNUNET_TIME_UNIT_ZERO_ABS; + + if ( (TALER_EC_NONE == ec) && + (NULL == redirect_url) ) + { + GNUNET_break_op (0); + ec = TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY; + error_msg_hint = "KYC provider returned no redirect URL"; + } + if ( (TALER_EC_NONE == ec) && + (NULL != kyp->ih_logic->initiate_get_expiration) ) + { + struct GNUNET_TIME_Timestamp provider_expiration; + + provider_expiration + = kyp->ih_logic->initiate_get_expiration (kyp->ih); + process_expiration = provider_expiration.abs_time; + } + if ( (TALER_EC_NONE == ec) && + GNUNET_TIME_absolute_is_zero (process_expiration) ) + { + process_expiration = GNUNET_TIME_absolute_add ( + kyp->process_start_time, + TALER_KYCLOGIC_provider_get_process_timeout (kyp->provider)); + } + if ( (TALER_EC_NONE == ec) && + ( (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us == + process_expiration.abs_value_us) || + GNUNET_TIME_absolute_cmp (process_expiration, + <=, + GNUNET_TIME_absolute_get ()) ) ) + { + GNUNET_break_op (0); + ec = TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY; + redirect_url = NULL; + error_msg_hint = "KYC provider returned an invalid process expiration"; + } kyp->ih = NULL; GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -225,34 +302,42 @@ initiate_cb ( (TALER_EC_NONE == ec) ? redirect_url : error_msg_hint); - kyp->ec = ec; - if (TALER_EC_NONE == ec) - { - kyp->redirect_url = GNUNET_strdup (redirect_url); - } - else - { - kyp->hint = GNUNET_strdup (error_msg_hint); - } - qs = TALER_EXCHANGEDB_update_legitimization_process_by_row ( + qs = TALER_EXCHANGEDB_complete_legitimization_process_start ( TEH_pg, kyp->process_row, + kyp->process_start_time, kyp->provider_name, &kyp->h_payto, provider_user_id, provider_legitimization_id, redirect_url, - GNUNET_TIME_UNIT_ZERO_ABS, + process_expiration, ec, error_msg_hint, TALER_EC_NONE != ec); - if (qs <= 0) + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "KYC requirement update failed for %s with status %d at %s:%u\n", + "KYC process start update failed or was superseded for %s with status %d at %s:%u\n", TALER_B2S (&kyp->h_payto), qs, __FILE__, __LINE__); + kyp->ec = (qs < 0) + ? TALER_EC_GENERIC_DB_STORE_FAILED + : TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT; + kyp->hint = GNUNET_strdup ((qs < 0) + ? "complete_legitimization_process_start" + : "KYC process initiation was superseded"); + } + else + { + kyp->ec = ec; + if (TALER_EC_NONE == ec) + kyp->redirect_url = GNUNET_strdup (redirect_url); + else + kyp->hint = GNUNET_strdup (error_msg_hint); + } GNUNET_assert (kyp->suspended); kyp->suspended = false; GNUNET_CONTAINER_DLL_remove (kyp_head, @@ -263,6 +348,139 @@ initiate_cb ( } +/** + * Resume a request that was waiting for another process initiation. + * + * @param cls a `struct KycPoller *` + */ +static void +retry_process_start (void *cls) +{ + struct KycPoller *kyp = cls; + + kyp->retry_task = NULL; + 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 (); +} + + +/** + * Atomically reserve, reuse or wait for a provider process. + * + * @param kyp request state + * @return MHD result + */ +static enum MHD_Result +reserve_and_start_process (struct KycPoller *kyp) +{ + enum GNUNET_DB_QueryStatus qs; + enum TALER_EXCHANGEDB_KycProcessReservationState state; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + struct GNUNET_TIME_Absolute lease_expiration + = GNUNET_TIME_absolute_add (now, + GNUNET_TIME_UNIT_MINUTES); + + GNUNET_free (kyp->redirect_url); + kyp->redirect_url = NULL; + qs = TALER_EXCHANGEDB_do_reserve_legitimization_process ( + TEH_pg, + &kyp->h_payto, + kyp->measure_index, + kyp->legitimization_measure_serial_id, + kyp->provider_name, + now, + lease_expiration, + &kyp->process_row, + &kyp->process_start_time, + &kyp->redirect_url, + &state); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + kyp->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "do_reserve_legitimization_process"); + } + switch (state) + { + case TALER_EXCHANGEDB_KPRS_READY: + GNUNET_assert (NULL != kyp->redirect_url); + return MHD_YES; + case TALER_EXCHANGEDB_KPRS_BUSY: + kyp->waiting_for_process = true; + kyp->retry_task = GNUNET_SCHEDULER_add_delayed ( + GNUNET_TIME_UNIT_SECONDS, + &retry_process_start, + kyp); + kyp->suspended = true; + GNUNET_CONTAINER_DLL_insert (kyp_head, + kyp_tail, + kyp); + MHD_suspend_connection (kyp->connection); + return MHD_YES; + case TALER_EXCHANGEDB_KPRS_OWNER: + break; + default: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + kyp->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "invalid KYC process reservation state"); + } + + kyp->ih = kyp->ih_logic->initiate ( + kyp->ih_logic->cls, + kyp->pd, + &kyp->h_payto, + kyp->process_row, + kyp->context, + &initiate_cb, + kyp); + if (NULL == kyp->ih) + { + qs = TALER_EXCHANGEDB_complete_legitimization_process_start ( + TEH_pg, + kyp->process_row, + kyp->process_start_time, + kyp->provider_name, + &kyp->h_payto, + NULL, + NULL, + NULL, + GNUNET_TIME_UNIT_ZERO_ABS, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_BUG, + "initiate KYC process", + true); + GNUNET_break (0); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + return TALER_MHD_reply_with_error ( + kyp->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "complete_legitimization_process_start"); + return TALER_MHD_reply_with_error ( + kyp->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_BUG, + "initiate KYC process"); + } + kyp->suspended = true; + GNUNET_CONTAINER_DLL_insert (kyp_head, + kyp_tail, + kyp); + MHD_suspend_connection (kyp->connection); + return MHD_YES; +} + + enum MHD_Result TEH_handler_kyc_start ( struct TEH_RequestContext *rc, @@ -277,7 +495,6 @@ TEH_handler_kyc_start ( const char *id = args[0]; enum GNUNET_DB_QueryStatus qs; const struct TALER_KYCLOGIC_KycProvider *provider; - struct TALER_KYCLOGIC_ProviderDetails *pd; bool is_finished; const json_t *context; @@ -440,75 +657,17 @@ TEH_handler_kyc_start ( TALER_KYCLOGIC_provider_to_logic ( provider, &kyp->ih_logic, - &pd, + &kyp->pd, &kyp->provider_name); + kyp->provider = provider; + kyp->context = context; + return reserve_and_start_process (kyp); + } - /* FIXME-#9419: the next two DB interactions should be ONE - transaction */ - /* Check if we already initiated this process */ - qs = TALER_EXCHANGEDB_get_pending_legitimization_process ( - TEH_pg, - &kyp->h_payto, - kyp->provider_name, - &kyp->redirect_url); - if (qs < 0) - { - GNUNET_break (0); - /* Simple query, never should be a soft error. */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_pending_kyc_requirement_process"); - } - if (NULL != kyp->redirect_url) - return MHD_YES; /* success, return the redirect URL - (in next call to this function) */ - - /* set up new requirement process */ - qs = TALER_EXCHANGEDB_insert_legitimization_process ( - TEH_pg, - &kyp->h_payto, - kyp->measure_index, - kyp->legitimization_measure_serial_id, - kyp->provider_name, - NULL, /* provider_account_id */ - NULL, /* provider_legitimziation_id */ - &kyp->process_row); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_kyc_requirement_process"); - } - - kyp->ih = kyp->ih_logic->initiate ( - kyp->ih_logic->cls, - pd, - &kyp->h_payto, - kyp->process_row, - context, - &initiate_cb, - kyp); - if (NULL == kyp->ih) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_BUG, - "initiate KYC process"); - } - kyp->suspended = true; - GNUNET_CONTAINER_DLL_insert (kyp_head, - kyp_tail, - kyp); - MHD_suspend_connection (kyp->connection); - return MHD_YES; + if (kyp->waiting_for_process) + { + kyp->waiting_for_process = false; + return reserve_and_start_process (kyp); } if ( (TALER_EC_NONE != kyp->ec) || diff --git a/src/exchangedb/complete_legitimization_process_start.c b/src/exchangedb/complete_legitimization_process_start.c @@ -0,0 +1,82 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file exchangedb/complete_legitimization_process_start.c + * @brief conditionally complete external KYC process initiation + */ +#include "taler/taler_pq_lib.h" +#include "exchange-database/complete_legitimization_process_start.h" +#include "helper.h" + + +enum GNUNET_DB_QueryStatus +TALER_EXCHANGEDB_complete_legitimization_process_start ( + struct TALER_EXCHANGEDB_PostgresContext *pg, + uint64_t process_row, + struct GNUNET_TIME_Absolute expected_start_time, + const char *provider_name, + const struct TALER_NormalizedPaytoHashP *h_payto, + const char *provider_account_id, + const char *provider_legitimization_id, + const char *redirect_url, + struct GNUNET_TIME_Absolute process_expiration, + enum TALER_ErrorCode ec, + const char *error_message_hint, + bool finished) +{ + uint32_t ec32 = (uint32_t) ec; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint64 (&process_row), + GNUNET_PQ_query_param_absolute_time (&expected_start_time), + GNUNET_PQ_query_param_string (provider_name), + GNUNET_PQ_query_param_auto_from_type (h_payto), + (NULL != provider_account_id) + ? GNUNET_PQ_query_param_string (provider_account_id) + : GNUNET_PQ_query_param_null (), + (NULL != provider_legitimization_id) + ? GNUNET_PQ_query_param_string (provider_legitimization_id) + : GNUNET_PQ_query_param_null (), + (NULL != redirect_url) + ? GNUNET_PQ_query_param_string (redirect_url) + : GNUNET_PQ_query_param_null (), + GNUNET_PQ_query_param_absolute_time (&process_expiration), + GNUNET_PQ_query_param_uint32 (&ec32), + (NULL != error_message_hint) + ? GNUNET_PQ_query_param_string (error_message_hint) + : GNUNET_PQ_query_param_null (), + GNUNET_PQ_query_param_bool (finished), + GNUNET_PQ_query_param_end + }; + + PREPARE (pg, + "complete_legitimization_process_start", + "UPDATE legitimization_processes" + " SET provider_user_id=$5" + " ,provider_legitimization_id=$6" + " ,redirect_url=$7" + " ,process_expiration_time=$8" + " ,error_code=$9" + " ,error_message=$10" + " ,finished=$11" + " WHERE legitimization_process_serial_id=$1" + " AND start_time=$2" + " AND provider_name=$3" + " AND h_payto=$4;"); + return GNUNET_PQ_eval_prepared_non_select ( + pg->conn, + "complete_legitimization_process_start", + params); +} diff --git a/src/exchangedb/do_reserve_legitimization_process.c b/src/exchangedb/do_reserve_legitimization_process.c @@ -0,0 +1,82 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file exchangedb/do_reserve_legitimization_process.c + * @brief atomically reserve or reuse an external KYC process + */ +#include "taler/taler_pq_lib.h" +#include "exchange-database/do_reserve_legitimization_process.h" +#include "helper.h" + + +enum GNUNET_DB_QueryStatus +TALER_EXCHANGEDB_do_reserve_legitimization_process ( + struct TALER_EXCHANGEDB_PostgresContext *pg, + const struct TALER_NormalizedPaytoHashP *h_payto, + uint32_t measure_index, + uint64_t legitimization_measure_serial_id, + const char *provider_name, + struct GNUNET_TIME_Absolute now, + struct GNUNET_TIME_Absolute lease_expiration, + uint64_t *process_row, + struct GNUNET_TIME_Absolute *start_time, + char **redirect_url, + enum TALER_EXCHANGEDB_KycProcessReservationState *state) +{ + uint32_t state32; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (h_payto), + GNUNET_PQ_query_param_uint32 (&measure_index), + GNUNET_PQ_query_param_uint64 (&legitimization_measure_serial_id), + GNUNET_PQ_query_param_string (provider_name), + GNUNET_PQ_query_param_absolute_time (&now), + GNUNET_PQ_query_param_absolute_time (&lease_expiration), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("process_row", + process_row), + GNUNET_PQ_result_spec_absolute_time ("start_time", + start_time), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("redirect_url", + redirect_url), + NULL), + GNUNET_PQ_result_spec_uint32 ("state", + &state32), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + + *redirect_url = NULL; + PREPARE (pg, + "do_reserve_legitimization_process", + "SELECT" + " out_process_row AS process_row" + ",out_start_time AS start_time" + ",out_redirect_url AS redirect_url" + ",out_state AS state" + " FROM exchange_do_reserve_legitimization_process" + " ($1,$2,$3,$4,$5,$6);"); + qs = GNUNET_PQ_eval_prepared_singleton_select ( + pg->conn, + "do_reserve_legitimization_process", + params, + rs); + if (qs > 0) + *state = (enum TALER_EXCHANGEDB_KycProcessReservationState) state32; + return qs; +} diff --git a/src/exchangedb/do_reserve_legitimization_process.sql b/src/exchangedb/do_reserve_legitimization_process.sql @@ -0,0 +1,121 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +DROP FUNCTION IF EXISTS exchange_do_reserve_legitimization_process; +CREATE FUNCTION exchange_do_reserve_legitimization_process( + IN in_h_payto BYTEA, + IN in_measure_index INT4, + IN in_legitimization_measure_serial_id INT8, + IN in_provider_name TEXT, + IN in_now INT8, + IN in_lease_expiration INT8, + OUT out_process_row INT8, + OUT out_start_time INT8, + OUT out_redirect_url TEXT, + OUT out_state INT4) +LANGUAGE plpgsql +AS $$ +DECLARE + my_rec RECORD; +BEGIN + -- Keep these INT4 values synchronized with + -- enum TALER_EXCHANGEDB_KycProcessReservationState: + -- 0 = KPRS_READY, 1 = KPRS_OWNER, 2 = KPRS_BUSY. + PERFORM FROM kyc_targets + WHERE h_normalized_payto=in_h_payto + FOR UPDATE; + + SELECT legitimization_process_serial_id, + start_time, + redirect_url + INTO my_rec + FROM legitimization_processes + WHERE h_payto=in_h_payto + AND provider_name=in_provider_name + AND NOT finished + AND process_expiration_time > in_now + ORDER BY start_time DESC + LIMIT 1 + FOR UPDATE; + + IF FOUND + THEN + out_process_row=my_rec.legitimization_process_serial_id; + out_start_time=my_rec.start_time; + out_redirect_url=my_rec.redirect_url; + IF out_redirect_url IS NULL + THEN + out_state=2; + ELSE + out_state=0; + END IF; + RETURN; + END IF; + + UPDATE legitimization_processes + SET finished=TRUE + WHERE h_payto=in_h_payto + AND provider_name=in_provider_name + AND NOT finished; + + INSERT INTO legitimization_processes + (h_payto, + start_time, + process_expiration_time, + expiration_time, + provider_name, + provider_user_id, + provider_legitimization_id, + redirect_url, + finished, + legitimization_measure_serial_id, + measure_index, + error_code, + error_message) + VALUES + (in_h_payto, + in_now, + in_lease_expiration, + 0, + in_provider_name, + NULL, + NULL, + NULL, + FALSE, + in_legitimization_measure_serial_id, + in_measure_index, + 0, + NULL) + ON CONFLICT (legitimization_measure_serial_id,measure_index) + DO UPDATE SET + h_payto=in_h_payto, + start_time=in_now, + process_expiration_time=in_lease_expiration, + expiration_time=0, + provider_name=in_provider_name, + provider_user_id=NULL, + provider_legitimization_id=NULL, + redirect_url=NULL, + finished=FALSE, + error_code=0, + error_message=NULL + RETURNING legitimization_process_serial_id, + start_time + INTO out_process_row, + out_start_time; + out_redirect_url=NULL; + out_state=1; +END $$; diff --git a/src/exchangedb/meson.build b/src/exchangedb/meson.build @@ -251,6 +251,8 @@ libtalerexchangedb = library( 'template.c', 'get_exists_aml_officer.c', 'do_trigger_kyc_rule_for_account.c', + 'do_reserve_legitimization_process.c', + 'complete_legitimization_process_start.c', 'update_aggregation_transient.c', 'update_auditor.c', 'update_legitimization_process_by_row.c', diff --git a/src/exchangedb/sql-schema/exchange-0015.sql b/src/exchangedb/sql-schema/exchange-0015.sql @@ -0,0 +1,54 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +BEGIN; + +SELECT _v.register_patch('exchange-0015', NULL, NULL); + +SET search_path TO exchange; + +CREATE FUNCTION alter_table_legitimization_processes15() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + ALTER TABLE legitimization_processes + ADD COLUMN process_expiration_time INT8 NOT NULL DEFAULT (0); + COMMENT ON COLUMN legitimization_processes.process_expiration_time + IS 'time until which an unfinished external provider process may be reused; zero means expired'; + CREATE INDEX legitimization_processes_active_by_account_provider + ON legitimization_processes + (h_payto, provider_name, start_time DESC) + WHERE NOT finished; + COMMENT ON INDEX legitimization_processes_active_by_account_provider + IS 'finds the newest reusable or currently initiating KYC process for an account and provider'; +END +$$; + +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) + VALUES + ('legitimization_processes15' + ,'exchange-0015' + ,'alter' + ,TRUE + ,FALSE); + +COMMIT; diff --git a/src/exchangedb/sql-schema/meson.build b/src/exchangedb/sql-schema/meson.build @@ -35,6 +35,7 @@ procedures_sql = [ # semi-dead... 'exchange_do_get_link_data.sql', '../do_trigger_kyc_rule_for_account.sql', + '../do_reserve_legitimization_process.sql', '../get_legitimization_requirement_by_row.sql', '../insert_active_legitimization_measure.sql', '../iterate_aggregations_above_serial_id.sql', @@ -188,6 +189,7 @@ generated_sql = [ ['exchange-0012.sql', exchange_0012_sql], ['exchange-0013.sql', exchange_0013_sql], ['exchange-0014.sql', ['exchange-0014.sql']], + ['exchange-0015.sql', ['exchange-0015.sql']], ['tops-0001.sql', ['tops-0001.sql']], ] diff --git a/src/exchangedb/test_legitimization_processes.c b/src/exchangedb/test_legitimization_processes.c @@ -21,6 +21,8 @@ * * Covers #TALER_EXCHANGEDB_insert_legitimization_process(), * #TALER_EXCHANGEDB_update_legitimization_process_by_row(), + * #TALER_EXCHANGEDB_do_reserve_legitimization_process(), + * #TALER_EXCHANGEDB_complete_legitimization_process_start(), * #TALER_EXCHANGEDB_insert_kyc_failure(), * #TALER_EXCHANGEDB_insert_aml_program_failure(), * #TALER_EXCHANGEDB_get_legitimization_process_by_account(), @@ -35,6 +37,8 @@ * "redirect known" to "finished". */ #include "test_common.h" +#include "exchange-database/complete_legitimization_process_start.h" +#include "exchange-database/do_reserve_legitimization_process.h" #include "exchange-database/do_trigger_kyc_rule_for_account.h" #include "exchange-database/get_kyc_provider_account.h" #include "exchange-database/get_legitimization_process_by_account.h" @@ -574,6 +578,153 @@ check_references (struct TALER_EXCHANGEDB_PostgresContext *pg) /** + * Process initiation is reserved atomically, completed only by the owner, + * and restarted after its provider-side expiration. + * + * @param pg the database context + * @return 0 on success + */ +static int +check_reservation (struct TALER_EXCHANGEDB_PostgresContext *pg) +{ + const char *provider = "reservation-provider"; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + struct GNUNET_TIME_Absolute lease_expiration + = GNUNET_TIME_absolute_add (now, + GNUNET_TIME_UNIT_MINUTES); + struct GNUNET_TIME_Absolute process_expiration + = GNUNET_TIME_absolute_add (now, + GNUNET_TIME_UNIT_HOURS); + struct GNUNET_TIME_Absolute start_time; + struct GNUNET_TIME_Absolute other_start_time; + struct GNUNET_TIME_Absolute restart_time; + struct GNUNET_TIME_Absolute restart_lease; + enum TALER_EXCHANGEDB_KycProcessReservationState state; + uint64_t row; + uint64_t other_row; + char *redirect_url = NULL; + + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_do_reserve_legitimization_process ( + pg, + &account.h_normalized, + 3, + measure_row, + provider, + now, + lease_expiration, + &row, + &start_time, + &redirect_url, + &state)); + FAILIF (TALER_EXCHANGEDB_KPRS_OWNER != state); + FAILIF (NULL != redirect_url); + + /* A concurrent request must wait for the reservation owner. */ + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_do_reserve_legitimization_process ( + pg, + &account.h_normalized, + 3, + measure_row, + provider, + now, + lease_expiration, + &other_row, + &other_start_time, + &redirect_url, + &state)); + FAILIF (TALER_EXCHANGEDB_KPRS_BUSY != state); + FAILIF (row != other_row); + FAILIF (start_time.abs_value_us != other_start_time.abs_value_us); + FAILIF (NULL != redirect_url); + + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_complete_legitimization_process_start ( + pg, + row, + start_time, + provider, + &account.h_normalized, + "provider-account-reservation", + "provider-process-reservation", + "https://kyc.example/reserved", + process_expiration, + TALER_EC_NONE, + NULL, + false)); + + /* Once initiation completed, all requests reuse its redirect. */ + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_do_reserve_legitimization_process ( + pg, + &account.h_normalized, + 3, + measure_row, + provider, + now, + lease_expiration, + &other_row, + &other_start_time, + &redirect_url, + &state)); + FAILIF (TALER_EXCHANGEDB_KPRS_READY != state); + FAILIF (row != other_row); + FAILIF (NULL == redirect_url); + FAILIF_C (0 != strcmp (redirect_url, + "https://kyc.example/reserved"), + GNUNET_free (redirect_url)); + GNUNET_free (redirect_url); + redirect_url = NULL; + + /* A callback from a superseded owner cannot overwrite the process. */ + other_start_time = GNUNET_TIME_absolute_add ( + start_time, + GNUNET_TIME_UNIT_SECONDS); + FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != + TALER_EXCHANGEDB_complete_legitimization_process_start ( + pg, + row, + other_start_time, + provider, + &account.h_normalized, + NULL, + NULL, + "https://kyc.example/late", + process_expiration, + TALER_EC_NONE, + NULL, + false)); + + /* Expiration makes the next request the owner of a fresh start. */ + restart_time = GNUNET_TIME_absolute_add ( + process_expiration, + GNUNET_TIME_UNIT_SECONDS); + restart_lease = GNUNET_TIME_absolute_add ( + restart_time, + GNUNET_TIME_UNIT_MINUTES); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_do_reserve_legitimization_process ( + pg, + &account.h_normalized, + 3, + measure_row, + provider, + restart_time, + restart_lease, + &other_row, + &other_start_time, + &redirect_url, + &state)); + FAILIF (TALER_EXCHANGEDB_KPRS_OWNER != state); + FAILIF (row != other_row); + FAILIF (start_time.abs_value_us == other_start_time.abs_value_us); + FAILIF (NULL != redirect_url); + return 0; +} + + +/** * The checks to run, in order. */ static const struct TDB_Test tests[] = { @@ -589,6 +740,8 @@ static const struct TDB_Test tests[] = { &check_aml_program_failure }, { "legitimization-processes-references", &check_references }, + { "legitimization-processes-reservation", + &check_reservation }, { NULL, NULL } }; diff --git a/src/include/exchange-database/complete_legitimization_process_start.h b/src/include/exchange-database/complete_legitimization_process_start.h @@ -0,0 +1,65 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/include/exchange-database/complete_legitimization_process_start.h + * @brief conditionally complete external KYC process initiation + */ +#ifndef EXCHANGE_DATABASE_COMPLETE_LEGITIMIZATION_PROCESS_START_H +#define EXCHANGE_DATABASE_COMPLETE_LEGITIMIZATION_PROCESS_START_H + +#include "taler/taler_error_codes.h" +#include "taler/taler_util.h" +#include "exchangedb_lib.h" + + +/** + * Store the result of provider initiation if the caller still owns the + * process generation. Matching @a expected_start_time prevents a late + * callback from overwriting a newer initiation in the same row. + * + * Primary test table: `legitimization_processes` (see + * test_legitimization_processes.c). + * + * @param pg database context + * @param process_row process row to update + * @param expected_start_time generation the caller reserved + * @param provider_name expected provider configuration name + * @param h_payto expected account + * @param provider_account_id account identifier returned by provider, or NULL + * @param provider_legitimization_id process identifier returned by provider, or NULL + * @param redirect_url provider redirect URL, or NULL on failure + * @param process_expiration time until the unfinished process may be reused + * @param ec initiation result + * @param error_message_hint optional human-readable error details + * @param finished true if initiation failed and the process is finished + * @return one result if updated, no results if superseded, negative on error + */ +enum GNUNET_DB_QueryStatus +TALER_EXCHANGEDB_complete_legitimization_process_start ( + struct TALER_EXCHANGEDB_PostgresContext *pg, + uint64_t process_row, + struct GNUNET_TIME_Absolute expected_start_time, + const char *provider_name, + const struct TALER_NormalizedPaytoHashP *h_payto, + const char *provider_account_id, + const char *provider_legitimization_id, + const char *redirect_url, + struct GNUNET_TIME_Absolute process_expiration, + enum TALER_ErrorCode ec, + const char *error_message_hint, + bool finished); + +#endif diff --git a/src/include/exchange-database/do_reserve_legitimization_process.h b/src/include/exchange-database/do_reserve_legitimization_process.h @@ -0,0 +1,72 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/include/exchange-database/do_reserve_legitimization_process.h + * @brief atomically reserve or reuse an external KYC process + */ +#ifndef EXCHANGE_DATABASE_DO_RESERVE_LEGITIMIZATION_PROCESS_H +#define EXCHANGE_DATABASE_DO_RESERVE_LEGITIMIZATION_PROCESS_H + +#include "taler/taler_util.h" +#include "exchangedb_lib.h" + +enum TALER_EXCHANGEDB_KycProcessReservationState +{ + /** A reusable redirect URL is available. */ + TALER_EXCHANGEDB_KPRS_READY = 0, + + /** The caller owns the reservation and must initiate the provider. */ + TALER_EXCHANGEDB_KPRS_OWNER = 1, + + /** Another caller is currently initiating the provider. */ + TALER_EXCHANGEDB_KPRS_BUSY = 2 +}; + + +/** + * Atomically reuse, wait for, or reserve an external KYC process. + * + * Primary test table: `legitimization_processes` (see + * test_legitimization_processes.c). + * + * @param pg database context + * @param h_payto account to validate + * @param measure_index selected measure index + * @param legitimization_measure_serial_id selected measure row + * @param provider_name provider configuration name + * @param now current time + * @param lease_expiration expiration of a newly reserved initiation lease + * @param[out] process_row process row that was selected or reserved + * @param[out] start_time generation of @a process_row + * @param[out] redirect_url reusable redirect for READY, otherwise NULL + * @param[out] state result of the reservation attempt + * @return database query status + */ +enum GNUNET_DB_QueryStatus +TALER_EXCHANGEDB_do_reserve_legitimization_process ( + struct TALER_EXCHANGEDB_PostgresContext *pg, + const struct TALER_NormalizedPaytoHashP *h_payto, + uint32_t measure_index, + uint64_t legitimization_measure_serial_id, + const char *provider_name, + struct GNUNET_TIME_Absolute now, + struct GNUNET_TIME_Absolute lease_expiration, + uint64_t *process_row, + struct GNUNET_TIME_Absolute *start_time, + char **redirect_url, + enum TALER_EXCHANGEDB_KycProcessReservationState *state); + +#endif diff --git a/src/include/taler/taler_kyclogic_lib.h b/src/include/taler/taler_kyclogic_lib.h @@ -603,6 +603,18 @@ TALER_KYCLOGIC_provider_to_logic ( /** + * Return the fallback lifetime for a provider process. This is used only + * when the provider does not report an authoritative expiration. + * + * @param provider provider to inspect + * @return finite fallback lifetime + */ +struct GNUNET_TIME_Relative +TALER_KYCLOGIC_provider_get_process_timeout ( + const struct TALER_KYCLOGIC_KycProvider *provider); + + +/** * Find default measure @a measure_name. * * @param measure_name name of measure to find diff --git a/src/include/taler/taler_kyclogic_plugin.h b/src/include/taler/taler_kyclogic_plugin.h @@ -416,6 +416,20 @@ struct TALER_KYCLOGIC_Plugin /** + * Obtain the finite provider-reported expiration for an initiation result. + * Called from the initiation callback, before the plugin releases @a ih. + * A zero timestamp means that the provider did not report an expiration. + * Optional; NULL is equivalent to returning zero. + * + * @param ih active initiation handle + * @return provider-reported process expiration or zero + */ + struct GNUNET_TIME_Timestamp + (*initiate_get_expiration)( + const struct TALER_KYCLOGIC_InitiateHandle *ih); + + + /** * Cancel KYC check initiation. * * @param[in] ih handle of operation to cancel diff --git a/src/kyclogic/kyclogic-kycaid.conf b/src/kyclogic/kyclogic-kycaid.conf @@ -6,6 +6,10 @@ LOGIC = kycaid +# Fallback lifetime for a provider process when the provider does not report +# its own expiration. Optional, defaults to one day. +KYC_PROCESS_TIMEOUT = 1 day + # How long is the KYC check valid? KYC_KYCAID_VALIDITY = forever diff --git a/src/kyclogic/kyclogic-oauth2.conf b/src/kyclogic/kyclogic-oauth2.conf @@ -9,6 +9,10 @@ LOGIC = oauth2 USER_TYPE = INDIVIDUAL PROVIDED_CHECKS = EXAMPLE_DO_NOT_USE +# Fallback lifetime for a provider process when the provider does not report +# its own expiration. Optional, defaults to one day. +KYC_PROCESS_TIMEOUT = 1 day + # Converter that converts OAuth2.0 data about the user # into GNU Taler standardized attribute data. # diff --git a/src/kyclogic/kyclogic-persona.conf b/src/kyclogic/kyclogic-persona.conf @@ -17,6 +17,10 @@ LOGIC = persona +# Fallback lifetime for a provider process when the provider does not report +# its own expiration. Optional, defaults to one day. +KYC_PROCESS_TIMEOUT = 1 day + # Generic converter that does nothing. KYC_PERSONA_CONVERTER_HELPER = cat @@ -42,4 +46,4 @@ KYC_PERSONA_POST_URL = https://taler.net/ # Salt to give to requests for idempotency. # Optional. -# KYC_PERSONA_SALT = salt -\ No newline at end of file +# KYC_PERSONA_SALT = salt diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c @@ -54,6 +54,12 @@ struct TALER_KYCLOGIC_KycProvider */ struct TALER_KYCLOGIC_ProviderDetails *pd; + /** + * Maximum time to reuse a process when the provider does not report its + * authoritative expiration. + */ + struct GNUNET_TIME_Relative process_timeout; + }; @@ -2168,6 +2174,41 @@ add_provider (const struct GNUNET_CONFIGURATION_Handle *cfg, = GNUNET_strdup (&section[strlen ("kyc-provider-")]); kp->logic = lp; kp->pd = pd; + kp->process_timeout = GNUNET_TIME_UNIT_DAYS; + if (GNUNET_YES == + GNUNET_CONFIGURATION_have_value (cfg, + section, + "KYC_PROCESS_TIMEOUT")) + { + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (cfg, + section, + "KYC_PROCESS_TIMEOUT", + &kp->process_timeout)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "KYC_PROCESS_TIMEOUT", + "finite positive duration required"); + GNUNET_free (kp->provider_name); + GNUNET_free (kp); + lp->unload_configuration (pd); + return GNUNET_SYSERR; + } + } + if ( (0 == kp->process_timeout.rel_value_us) || + (GNUNET_TIME_UNIT_FOREVER_REL.rel_value_us == + kp->process_timeout.rel_value_us) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "KYC_PROCESS_TIMEOUT", + "finite positive duration required"); + GNUNET_free (kp->provider_name); + GNUNET_free (kp); + lp->unload_configuration (pd); + return GNUNET_SYSERR; + } GNUNET_array_append (kyc_providers, num_kyc_providers, kp); @@ -2176,6 +2217,14 @@ add_provider (const struct GNUNET_CONFIGURATION_Handle *cfg, } +struct GNUNET_TIME_Relative +TALER_KYCLOGIC_provider_get_process_timeout ( + const struct TALER_KYCLOGIC_KycProvider *provider) +{ + return provider->process_timeout; +} + + /** * Tokenize @a input along @a token * and build an array of the tokens. diff --git a/src/kyclogic/plugin_kyclogic_oauth2.c b/src/kyclogic/plugin_kyclogic_oauth2.c @@ -190,6 +190,11 @@ struct TALER_KYCLOGIC_InitiateHandle json_t *initial_address; /** + * Expiration reported by a dynamic /setup endpoint. + */ + struct GNUNET_TIME_Timestamp process_expiration; + + /** * Context for #TEH_curl_easy_post(). Keeps the data that must * persist for Curl to make the upload. */ @@ -689,9 +694,14 @@ handle_curl_setup_finished (void *cls, case MHD_HTTP_OK: { const char *nonce; + bool no_expiration; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("nonce", &nonce), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("expires", + &ih->process_expiration), + &no_expiration), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; @@ -718,6 +728,23 @@ handle_curl_setup_finished (void *cls, oauth2_initiate_cancel (ih); return; } + if ( (! no_expiration) && + GNUNET_TIME_timestamp_cmp ( + ih->process_expiration, + <=, + GNUNET_TIME_timestamp_get ()) ) + { + GNUNET_break_op (0); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, + NULL, + NULL, + NULL, + "KYC gateway returned an expired setup process"); + GNUNET_JSON_parse_free (spec); + oauth2_initiate_cancel (ih); + return; + } { char *nonce_encoded; @@ -750,6 +777,14 @@ handle_curl_setup_finished (void *cls, } +static struct GNUNET_TIME_Timestamp +oauth2_initiate_get_expiration ( + const struct TALER_KYCLOGIC_InitiateHandle *ih) +{ + return ih->process_expiration; +} + + /** * Logic to asynchronously return the response for how to begin the OAuth2.0 * checking process to the client. May first request a dynamic URL via @@ -1942,6 +1977,8 @@ libtaler_plugin_kyclogic_oauth2_init (void *cls) = &oauth2_unload_configuration; plugin->initiate = &oauth2_initiate; + plugin->initiate_get_expiration + = &oauth2_initiate_get_expiration; plugin->initiate_cancel = &oauth2_initiate_cancel; plugin->proof