commit ad46f95ca5709750ad6959fd0a2ecd72e3f4ed37
parent 7d22e626f9e67a4f6ca0102f45d0b0c01a907c60
Author: Christian Grothoff <christian@grothoff.org>
Date: Mon, 23 Feb 2026 12:05:48 +0100
fix #11036
Diffstat:
2 files changed, 279 insertions(+), 145 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_private-post-account.c b/src/backend/taler-merchant-httpd_private-post-account.c
@@ -31,6 +31,116 @@
#include "taler-merchant-httpd_mfa.h"
#include <regex.h>
+/**
+ * Maximum number of retries we do on serialization failures.
+ */
+#define MAX_RETRIES 5
+
+/**
+ * Closure for account_cb().
+ */
+struct PostAccountContext
+{
+ /**
+ * Payto URI of the account to add (from the request).
+ */
+ struct TALER_FullPayto uri;
+
+ /**
+ * Hash of the wire details (@e uri and @e salt).
+ * Set if @e have_same_account is true.
+ */
+ struct TALER_MerchantWireHashP h_wire;
+
+ /**
+ * Salt value used for hashing @e uri.
+ * Set if @e have_same_account is true.
+ */
+ struct TALER_WireSaltP salt;
+
+ /**
+ * Credit facade URL from the request.
+ */
+ const char *credit_facade_url;
+
+ /**
+ * Facade credentials from the request.
+ */
+ const json_t *credit_facade_credentials;
+
+ /**
+ * Wire subject metadata from the request.
+ */
+ const char *extra_wire_subject_metadata;
+
+ /**
+ * True if we have ANY account already and thus require MFA.
+ */
+ bool have_any_account;
+
+ /**
+ * True if we have exact match already and thus require MFA.
+ */
+ bool have_same_account;
+
+ /**
+ * True if we have an account with the same normalized payto
+ * already and thus the client can only do PATCH but not POST.
+ */
+ bool have_conflicting_account;
+};
+
+
+/**
+ * Callback invoked with information about a bank account.
+ *
+ * @param cls closure with a `struct PostAccountContext`
+ * @param merchant_priv private key of the merchant instance
+ * @param ad details about the account
+ */
+static void
+account_cb (
+ void *cls,
+ const struct TALER_MerchantPrivateKeyP *merchant_priv,
+ const struct TALER_MERCHANTDB_AccountDetails *ad)
+{
+ struct PostAccountContext *pac = cls;
+
+ pac->have_any_account = true;
+ if ( (0 == TALER_full_payto_cmp (pac->uri,
+ ad->payto_uri) ) &&
+ ( (pac->credit_facade_credentials ==
+ ad->credit_facade_credentials) ||
+ ( (NULL != pac->credit_facade_credentials) &&
+ (NULL != ad->credit_facade_credentials) &&
+ (1 == json_equal (pac->credit_facade_credentials,
+ ad->credit_facade_credentials)) ) ) &&
+ ( (pac->extra_wire_subject_metadata ==
+ ad->extra_wire_subject_metadata) ||
+ ( (NULL != pac->extra_wire_subject_metadata) &&
+ (NULL != ad->extra_wire_subject_metadata) &&
+ (0 == strcmp (pac->extra_wire_subject_metadata,
+ ad->extra_wire_subject_metadata)) ) ) &&
+ ( (pac->credit_facade_url == ad->credit_facade_url) ||
+ ( (NULL != pac->credit_facade_url) &&
+ (NULL != ad->credit_facade_url) &&
+ (0 == strcmp (pac->credit_facade_url,
+ ad->credit_facade_url)) ) ) )
+ {
+ pac->have_same_account = true;
+ pac->salt = ad->salt;
+ pac->h_wire = ad->h_wire;
+ return;
+ }
+
+ if (0 == TALER_full_payto_normalize_and_cmp (pac->uri,
+ ad->payto_uri) )
+ {
+ pac->have_conflicting_account = true;
+ return;
+ }
+}
+
MHD_RESULT
TMH_private_post_account (const struct TMH_RequestHandler *rh,
@@ -38,28 +148,24 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
struct TMH_HandlerContext *hc)
{
struct TMH_MerchantInstance *mi = hc->instance;
- const char *credit_facade_url = NULL;
- const char *extra_wire_subject_metadata = NULL;
- const json_t *credit_facade_credentials = NULL;
- struct TALER_FullPayto uri;
+ struct PostAccountContext pac = { 0 };
struct GNUNET_JSON_Specification ispec[] = {
TALER_JSON_spec_full_payto_uri ("payto_uri",
- &uri),
+ &pac.uri),
GNUNET_JSON_spec_mark_optional (
TALER_JSON_spec_web_url ("credit_facade_url",
- &credit_facade_url),
+ &pac.credit_facade_url),
NULL),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_string ("extra_wire_subject_metadata",
- &extra_wire_subject_metadata),
+ &pac.extra_wire_subject_metadata),
NULL),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_object_const ("credit_facade_credentials",
- &credit_facade_credentials),
+ &pac.credit_facade_credentials),
NULL),
GNUNET_JSON_spec_end ()
};
- struct TMH_WireMethod *wm;
{
enum GNUNET_GenericReturnValue res;
@@ -77,7 +183,7 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
char *err;
if (NULL !=
- (err = TALER_payto_validate (uri)))
+ (err = TALER_payto_validate (pac.uri)))
{
MHD_RESULT mret;
@@ -91,7 +197,7 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
}
}
if (! TALER_is_valid_subject_metadata_string (
- extra_wire_subject_metadata))
+ pac.extra_wire_subject_metadata))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (
@@ -103,7 +209,7 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
{
char *apt = GNUNET_strdup (TMH_allowed_payment_targets);
- char *method = TALER_payto_get_method (uri.full_payto);
+ char *method = TALER_payto_get_method (pac.uri.full_payto);
bool ok;
ok = false;
@@ -137,7 +243,7 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
if ( (NULL != TMH_payment_target_regex) &&
(0 !=
regexec (&TMH_payment_target_re,
- uri.full_payto,
+ pac.uri.full_payto,
0,
NULL,
0)) )
@@ -149,25 +255,25 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
"The specific account is forbidden by policy");
}
- if ( (NULL == credit_facade_url) !=
- (NULL == credit_facade_credentials) )
+ if ( (NULL == pac.credit_facade_url) !=
+ (NULL == pac.credit_facade_credentials) )
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MISSING,
- (NULL == credit_facade_url)
+ (NULL == pac.credit_facade_url)
? "credit_facade_url"
: "credit_facade_credentials");
}
- if ( (NULL != credit_facade_url) ||
- (NULL != credit_facade_credentials) )
+ if ( (NULL != pac.credit_facade_url) ||
+ (NULL != pac.credit_facade_credentials) )
{
struct TALER_MERCHANT_BANK_AuthenticationData auth;
if (GNUNET_OK !=
- TALER_MERCHANT_BANK_auth_parse_json (credit_facade_credentials,
- credit_facade_url,
+ TALER_MERCHANT_BANK_auth_parse_json (pac.credit_facade_credentials,
+ pac.credit_facade_url,
&auth))
{
GNUNET_break_op (0);
@@ -179,163 +285,183 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
TALER_MERCHANT_BANK_auth_free (&auth);
}
- /* FIXME: might want to do all of the DB interactions below in one transaction... */
+ TMH_db->preflight (TMH_db->cls);
+ for (unsigned int retries = 0;
+ retries < MAX_RETRIES;
+ retries++)
{
enum GNUNET_DB_QueryStatus qs;
- enum GNUNET_GenericReturnValue ret;
+ struct TMH_WireMethod *wm;
+ TMH_db->rollback (TMH_db->cls);
+ if (GNUNET_OK !=
+ TMH_db->start (TMH_db->cls,
+ "post-account"))
+ {
+ GNUNET_break (0);
+ break;
+ }
qs = TMH_db->select_accounts (TMH_db->cls,
mi->settings.id,
- NULL,
- NULL);
+ &account_cb,
+ &pac);
switch (qs)
{
- case GNUNET_DB_STATUS_SOFT_ERROR:
case GNUNET_DB_STATUS_HARD_ERROR:
GNUNET_break (0);
+ TMH_db->rollback (TMH_db->cls);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select_accounts");
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ continue;
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- /* skip MFA */
break;
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
+
+ if (pac.have_same_account)
+ {
+ /* Idempotent request */
+ TMH_db->rollback (TMH_db->cls);
+ return TALER_MHD_REPLY_JSON_PACK (
+ connection,
+ MHD_HTTP_OK,
+ GNUNET_JSON_pack_data_auto (
+ "salt",
+ &pac.salt),
+ GNUNET_JSON_pack_data_auto (
+ "h_wire",
+ &pac.h_wire));
+
+ }
+
+ if (pac.have_conflicting_account)
+ {
+ /* Conflict, refuse request */
+ TMH_db->rollback (TMH_db->cls);
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
+ pac.uri.full_payto);
+ }
+
+ if (pac.have_any_account)
+ {
+ /* MFA needed */
+ enum GNUNET_GenericReturnValue ret;
+
ret = TMH_mfa_check_simple (hc,
TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
mi);
-
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Account creation MFA check returned %d\n",
(int) ret);
if (GNUNET_OK != ret)
{
+ TMH_db->rollback (TMH_db->cls);
return (GNUNET_NO == ret)
? MHD_YES
: MHD_NO;
}
}
- }
-
- /* convert provided payto URI into internal data structure with salts */
- wm = TMH_setup_wire_account (uri,
- credit_facade_url,
- credit_facade_credentials);
- GNUNET_assert (NULL != wm);
- {
- struct TALER_MERCHANTDB_AccountDetails ad = {
- .payto_uri = wm->payto_uri,
- .salt = wm->wire_salt,
- .instance_id = mi->settings.id,
- .h_wire = wm->h_wire,
- .credit_facade_url = wm->credit_facade_url,
- .credit_facade_credentials = wm->credit_facade_credentials,
- .extra_wire_subject_metadata = (char *) extra_wire_subject_metadata,
- .active = wm->active
- };
- enum GNUNET_DB_QueryStatus qs;
- qs = TMH_db->insert_account (TMH_db->cls,
- &ad);
- switch (qs)
+ /* convert provided payto URI into internal data structure with salts */
+ wm = TMH_setup_wire_account (pac.uri,
+ pac.credit_facade_url,
+ pac.credit_facade_credentials);
+ GNUNET_assert (NULL != wm);
{
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- break;
- case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- /* conflict: account exists */
+ struct TALER_MERCHANTDB_AccountDetails ad = {
+ .payto_uri = wm->payto_uri,
+ .salt = wm->wire_salt,
+ .instance_id = mi->settings.id,
+ .h_wire = wm->h_wire,
+ .credit_facade_url = wm->credit_facade_url,
+ .credit_facade_credentials = wm->credit_facade_credentials,
+ .extra_wire_subject_metadata = (char *) pac.extra_wire_subject_metadata,
+ .active = wm->active
+ };
+ struct GNUNET_DB_EventHeaderP es = {
+ .size = htons (sizeof (es)),
+ .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED)
+ };
+
+ qs = TMH_db->insert_account (TMH_db->cls,
+ &ad);
+ switch (qs)
{
- struct TALER_MERCHANTDB_AccountDetails adx;
-
- qs = TMH_db->select_account_by_uri (TMH_db->cls,
- mi->settings.id,
- ad.payto_uri,
- &adx);
- switch (qs)
- {
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- if ( (0 == TALER_full_payto_cmp (adx.payto_uri,
- ad.payto_uri) ) &&
- ( (adx.credit_facade_credentials ==
- ad.credit_facade_credentials) ||
- ( (NULL != adx.credit_facade_credentials) &&
- (NULL != ad.credit_facade_credentials) &&
- (1 == json_equal (adx.credit_facade_credentials,
- ad.credit_facade_credentials)) ) ) &&
- ( (adx.credit_facade_url == ad.credit_facade_url) ||
- ( (NULL != adx.credit_facade_url) &&
- (NULL != ad.credit_facade_url) &&
- (0 == strcmp (adx.credit_facade_url,
- ad.credit_facade_url)) ) ) )
- {
- TMH_wire_method_free (wm);
- GNUNET_free (adx.payto_uri.full_payto);
- GNUNET_free (adx.credit_facade_url);
- json_decref (adx.credit_facade_credentials);
- return TALER_MHD_REPLY_JSON_PACK (
- connection,
- MHD_HTTP_OK,
- GNUNET_JSON_pack_data_auto (
- "salt",
- &adx.salt),
- GNUNET_JSON_pack_data_auto (
- "h_wire",
- &adx.h_wire));
- }
- GNUNET_free (adx.payto_uri.full_payto);
- GNUNET_free (adx.credit_facade_url);
- json_decref (adx.credit_facade_credentials);
- break;
- case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- case GNUNET_DB_STATUS_SOFT_ERROR:
- case GNUNET_DB_STATUS_HARD_ERROR:
- GNUNET_break (0);
- TMH_wire_method_free (wm);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "select_account");
- }
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break (0);
+ TMH_wire_method_free (wm);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
+ "insert_account");
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ continue;
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ TMH_wire_method_free (wm);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "insert_account");
}
- TMH_wire_method_free (wm);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_CONFLICT,
- TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
- uri.full_payto);
- case GNUNET_DB_STATUS_SOFT_ERROR:
- case GNUNET_DB_STATUS_HARD_ERROR:
- GNUNET_break (0);
- TMH_wire_method_free (wm);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_STORE_FAILED,
- "insert_account");
- }
- }
- {
- struct GNUNET_DB_EventHeaderP es = {
- .size = htons (sizeof (es)),
- .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED)
- };
-
- TMH_db->event_notify (TMH_db->cls,
- &es,
- NULL,
- 0);
- }
- /* Finally, also update our running process */
- GNUNET_CONTAINER_DLL_insert (mi->wm_head,
- mi->wm_tail,
- wm);
- /* Note: we may not need to do this, as we notified
- about the account change above. But also hardly hurts. */
- TMH_reload_instances (mi->settings.id);
- return TALER_MHD_REPLY_JSON_PACK (connection,
- MHD_HTTP_OK,
- GNUNET_JSON_pack_data_auto ("salt",
- &wm->wire_salt),
- GNUNET_JSON_pack_data_auto ("h_wire",
- &wm->h_wire));
+ TMH_db->event_notify (TMH_db->cls,
+ &es,
+ NULL,
+ 0);
+ qs = TMH_db->commit (TMH_db->cls);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ break;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ TMH_wire_method_free (wm);
+ continue;
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ TMH_wire_method_free (wm);
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_COMMIT_FAILED,
+ "post-account");
+ }
+ /* Finally, also update our running process */
+ GNUNET_CONTAINER_DLL_insert (mi->wm_head,
+ mi->wm_tail,
+ wm);
+ /* Note: we may not need to do this, as we notified
+ about the account change above. But also hardly hurts. */
+ TMH_reload_instances (mi->settings.id);
+ }
+ return TALER_MHD_REPLY_JSON_PACK (
+ connection,
+ MHD_HTTP_OK,
+ GNUNET_JSON_pack_data_auto ("salt",
+ &wm->
+ wire_salt
+ ),
+ GNUNET_JSON_pack_data_auto ("h_wire",
+ &wm->h_wire));
+ } /* end retries */
+ TMH_db->rollback (TMH_db->cls);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ "post-accounts");
}
diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c
@@ -4183,6 +4183,10 @@ test_insert_transfer (const struct InstanceData *instance,
const struct TransferData *transfer,
enum GNUNET_DB_QueryStatus expected_result)
{
+ bool no_instance;
+ bool no_account;
+ bool conflict;
+
TEST_COND_RET_ON_FAIL (expected_result ==
plugin->insert_transfer (plugin->cls,
instance->instance.id,
@@ -4190,7 +4194,10 @@ test_insert_transfer (const struct InstanceData *instance,
&transfer->wtid,
&transfer->data.total_amount,
account->payto_uri,
- transfer->confirmed),
+ transfer->confirmed,
+ &no_instance,
+ &no_account,
+ &conflict),
"Insert transfer failed\n");
return 0;
}
@@ -4447,7 +4454,8 @@ run_test_transfers (struct TestTransfers_Closure *cls)
TEST_RET_ON_FAIL (test_insert_transfer (&cls->instance,
&cls->account,
&cls->transfers[0],
- GNUNET_DB_STATUS_SUCCESS_NO_RESULTS));
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT))
+ ;
TEST_RET_ON_FAIL (test_insert_deposit_to_transfer (&cls->instance,
&cls->signkey,
&cls->order,