LCOV - code coverage report
Current view: top level - backend - taler-merchant-httpd_post-private-accounts.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 60.9 % 151 92
Test Date: 2026-09-04 23:42:01 Functions: 100.0 % 2 2

            Line data    Source code
       1              : /*
       2              :   This file is part of TALER
       3              :   (C) 2020-2024 Taler Systems SA
       4              : 
       5              :   TALER is free software; you can redistribute it and/or modify
       6              :   it under the terms of the GNU Affero General Public License as
       7              :   published by the Free Software Foundation; either version 3,
       8              :   or (at your option) any later version.
       9              : 
      10              :   TALER is distributed in the hope that it will be useful, but
      11              :   WITHOUT ANY WARRANTY; without even the implied warranty of
      12              :   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      13              :   GNU General Public License for more details.
      14              : 
      15              :   You should have received a copy of the GNU General Public
      16              :   License along with TALER; see the file COPYING.  If not,
      17              :   see <http://www.gnu.org/licenses/>
      18              : */
      19              : 
      20              : /**
      21              :  * @file src/backend/taler-merchant-httpd_post-private-accounts.c
      22              :  * @brief implementing POST /private/accounts request handling
      23              :  * @author Christian Grothoff
      24              :  */
      25              : #include "platform.h"
      26              : #include "taler-merchant-httpd_post-private-accounts.h"
      27              : #include "taler-merchant-httpd_helper.h"
      28              : #include "taler/taler_merchant_bank_lib.h"
      29              : #include <taler/taler_dbevents.h>
      30              : #include <taler/taler_json_lib.h>
      31              : #include "taler-merchant-httpd_mfa.h"
      32              : #include <regex.h>
      33              : #include "merchant-database/do_insert_account.h"
      34              : #include "merchant-database/iterate_accounts_by_instance.h"
      35              : #include "merchant-database/preflight.h"
      36              : 
      37              : /**
      38              :  * Maximum number of retries we do on serialization failures.
      39              :  */
      40              : #define MAX_RETRIES 5
      41              : 
      42              : /**
      43              :  * Closure for account_cb().
      44              :  */
      45              : struct PostAccountContext
      46              : {
      47              :   /**
      48              :    * Payto URI of the account to add (from the request).
      49              :    */
      50              :   struct TALER_FullPayto uri;
      51              : 
      52              :   /**
      53              :    * Hash of the wire details (@e uri and @e salt).
      54              :    * Set if @e have_same_account is true.
      55              :    */
      56              :   struct TALER_MerchantWireHashP h_wire;
      57              : 
      58              :   /**
      59              :    * Salt value used for hashing @e uri.
      60              :    * Set if @e have_same_account is true.
      61              :    */
      62              :   struct TALER_WireSaltP salt;
      63              : 
      64              :   /**
      65              :    * Credit facade URL from the request.
      66              :    */
      67              :   const char *credit_facade_url;
      68              : 
      69              :   /**
      70              :    * Facade credentials from the request.
      71              :    */
      72              :   const json_t *credit_facade_credentials;
      73              : 
      74              :   /**
      75              :    * Wire subject metadata from the request.
      76              :    */
      77              :   const char *extra_wire_subject_metadata;
      78              : 
      79              :   /**
      80              :    * True if we have ANY account already and thus require MFA.
      81              :    */
      82              :   bool have_any_account;
      83              : 
      84              :   /**
      85              :    * True if we have exact match already and thus require MFA.
      86              :    */
      87              :   bool have_same_account;
      88              : 
      89              :   /**
      90              :    * True if we have an account with the same normalized payto
      91              :    * already and thus the client can only do PATCH but not POST.
      92              :    */
      93              :   bool have_conflicting_account;
      94              : };
      95              : 
      96              : 
      97              : /**
      98              :  * Callback invoked with information about a bank account.
      99              :  *
     100              :  * @param cls closure with a `struct PostAccountContext`
     101              :  * @param merchant_priv private key of the merchant instance
     102              :  * @param ad details about the account
     103              :  */
     104              : static void
     105            7 : account_cb (
     106              :   void *cls,
     107              :   const struct TALER_MerchantPrivateKeyP *merchant_priv,
     108              :   const struct TALER_MERCHANTDB_AccountDetails *ad)
     109              : {
     110            7 :   struct PostAccountContext *pac = cls;
     111              : 
     112            7 :   if (! ad->active)
     113            0 :     return;
     114            7 :   pac->have_any_account = true;
     115            7 :   if ( (0 == TALER_full_payto_cmp (pac->uri,
     116            1 :                                    ad->payto_uri) ) &&
     117            1 :        ( (pac->credit_facade_credentials ==
     118            1 :           ad->credit_facade_credentials) ||
     119            0 :          ( (NULL != pac->credit_facade_credentials) &&
     120            0 :            (NULL != ad->credit_facade_credentials) &&
     121            0 :            (1 == json_equal (pac->credit_facade_credentials,
     122            0 :                              ad->credit_facade_credentials)) ) ) &&
     123            1 :        ( (pac->extra_wire_subject_metadata ==
     124            1 :           ad->extra_wire_subject_metadata) ||
     125            0 :          ( (NULL != pac->extra_wire_subject_metadata) &&
     126            0 :            (NULL != ad->extra_wire_subject_metadata) &&
     127            0 :            (0 == strcmp (pac->extra_wire_subject_metadata,
     128            0 :                          ad->extra_wire_subject_metadata)) ) ) &&
     129            1 :        ( (pac->credit_facade_url == ad->credit_facade_url) ||
     130            0 :          ( (NULL != pac->credit_facade_url) &&
     131            0 :            (NULL != ad->credit_facade_url) &&
     132            0 :            (0 == strcmp (pac->credit_facade_url,
     133            0 :                          ad->credit_facade_url)) ) ) )
     134              :   {
     135            1 :     pac->have_same_account = true;
     136            1 :     pac->salt = ad->salt;
     137            1 :     pac->h_wire = ad->h_wire;
     138            1 :     return;
     139              :   }
     140              : 
     141            6 :   if (0 == TALER_full_payto_normalize_and_cmp (pac->uri,
     142              :                                                ad->payto_uri) )
     143              :   {
     144            1 :     pac->have_conflicting_account = true;
     145            1 :     return;
     146              :   }
     147              : }
     148              : 
     149              : 
     150              : enum MHD_Result
     151           30 : TMH_private_post_account (const struct TMH_RequestHandler *rh,
     152              :                           struct MHD_Connection *connection,
     153              :                           struct TMH_HandlerContext *hc)
     154              : {
     155           30 :   struct TMH_MerchantInstance *mi = hc->instance;
     156           30 :   struct PostAccountContext pac = { 0 };
     157              :   struct GNUNET_JSON_Specification ispec[] = {
     158           30 :     TALER_JSON_spec_full_payto_uri ("payto_uri",
     159              :                                     &pac.uri),
     160           30 :     GNUNET_JSON_spec_mark_optional (
     161              :       TALER_JSON_spec_web_url ("credit_facade_url",
     162              :                                &pac.credit_facade_url),
     163              :       NULL),
     164           30 :     GNUNET_JSON_spec_mark_optional (
     165              :       GNUNET_JSON_spec_string ("extra_wire_subject_metadata",
     166              :                                &pac.extra_wire_subject_metadata),
     167              :       NULL),
     168           30 :     GNUNET_JSON_spec_mark_optional (
     169              :       GNUNET_JSON_spec_object_const ("credit_facade_credentials",
     170              :                                      &pac.credit_facade_credentials),
     171              :       NULL),
     172           30 :     GNUNET_JSON_spec_end ()
     173              :   };
     174              : 
     175              :   {
     176              :     enum GNUNET_GenericReturnValue res;
     177              : 
     178           30 :     res = TALER_MHD_parse_json_data (connection,
     179           30 :                                      hc->request_body,
     180              :                                      ispec);
     181           30 :     if (GNUNET_OK != res)
     182              :       return (GNUNET_NO == res)
     183              :              ? MHD_YES
     184            0 :              : MHD_NO;
     185              :   }
     186              : 
     187              :   {
     188              :     char *err;
     189              : 
     190           30 :     if (NULL !=
     191           30 :         (err = TALER_payto_validate (pac.uri)))
     192              :     {
     193              :       enum MHD_Result mret;
     194              : 
     195            0 :       GNUNET_break_op (0);
     196            0 :       mret = TALER_MHD_reply_with_error (
     197              :         connection,
     198              :         MHD_HTTP_BAD_REQUEST,
     199              :         TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
     200              :         err);
     201            0 :       GNUNET_free (err);
     202            0 :       return mret;
     203              :     }
     204              :   }
     205           30 :   if (! TALER_is_valid_subject_metadata_string (
     206              :         pac.extra_wire_subject_metadata))
     207              :   {
     208            0 :     GNUNET_break_op (0);
     209            0 :     return TALER_MHD_reply_with_error (
     210              :       connection,
     211              :       MHD_HTTP_BAD_REQUEST,
     212              :       TALER_EC_GENERIC_PARAMETER_MALFORMED,
     213              :       "extra_wire_subject_metadata");
     214              :   }
     215              : 
     216              :   {
     217           30 :     char *apt = GNUNET_strdup (TMH_allowed_payment_targets);
     218           30 :     char *method = TALER_payto_get_method (pac.uri.full_payto);
     219              :     bool ok;
     220              : 
     221           30 :     ok = false;
     222           30 :     for (const char *tok = strtok (apt,
     223              :                                    " ");
     224           30 :          NULL != tok;
     225            0 :          tok = strtok (NULL,
     226              :                        " "))
     227              :     {
     228           30 :       if (0 == strcmp ("*",
     229              :                        tok))
     230           30 :         ok = true;
     231           30 :       if (0 == strcmp (method,
     232              :                        tok))
     233            0 :         ok = true;
     234           30 :       if (ok)
     235           30 :         break;
     236              :     }
     237           30 :     GNUNET_free (method);
     238           30 :     GNUNET_free (apt);
     239           30 :     if (! ok)
     240              :     {
     241            0 :       GNUNET_break_op (0);
     242            0 :       return TALER_MHD_reply_with_error (
     243              :         connection,
     244              :         MHD_HTTP_BAD_REQUEST,
     245              :         TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
     246              :         "The payment target type is forbidden by policy");
     247              :     }
     248              :   }
     249              : 
     250           30 :   if ( (NULL != TMH_payment_target_regex) &&
     251              :        (0 !=
     252            0 :         regexec (&TMH_payment_target_re,
     253            0 :                  pac.uri.full_payto,
     254              :                  0,
     255              :                  NULL,
     256              :                  0)) )
     257              :   {
     258            0 :     GNUNET_break_op (0);
     259            0 :     return TALER_MHD_reply_with_error (
     260              :       connection,
     261              :       MHD_HTTP_BAD_REQUEST,
     262              :       TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
     263              :       "The specific account is forbidden by policy");
     264              :   }
     265              : 
     266           30 :   if ( (NULL == pac.credit_facade_url) !=
     267           30 :        (NULL == pac.credit_facade_credentials) )
     268              :   {
     269            0 :     GNUNET_break_op (0);
     270            0 :     return TALER_MHD_reply_with_error (
     271              :       connection,
     272              :       MHD_HTTP_BAD_REQUEST,
     273              :       TALER_EC_GENERIC_PARAMETER_MISSING,
     274            0 :       (NULL == pac.credit_facade_url)
     275              :                                        ? "credit_facade_url"
     276              :                                        : "credit_facade_credentials");
     277              :   }
     278           30 :   if ( (NULL != pac.credit_facade_url) ||
     279           28 :        (NULL != pac.credit_facade_credentials) )
     280              :   {
     281              :     struct TALER_MERCHANT_BANK_AuthenticationData auth;
     282              : 
     283            2 :     if (GNUNET_OK !=
     284            2 :         TALER_MERCHANT_BANK_auth_parse_json (pac.credit_facade_credentials,
     285              :                                              pac.credit_facade_url,
     286              :                                              &auth))
     287              :     {
     288            0 :       GNUNET_break_op (0);
     289            0 :       return TALER_MHD_reply_with_error (
     290              :         connection,
     291              :         MHD_HTTP_BAD_REQUEST,
     292              :         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     293              :         "credit_facade_url or credit_facade_credentials");
     294              :     }
     295            2 :     TALER_MERCHANT_BANK_auth_free (&auth);
     296              :   }
     297              : 
     298              :   {
     299              :     enum GNUNET_DB_QueryStatus qs;
     300              : 
     301           30 :     TALER_MERCHANTDB_preflight (TMH_db);
     302           30 :     qs = TALER_MERCHANTDB_iterate_accounts_by_instance (
     303              :       TMH_db,
     304           30 :       mi->settings.id,
     305              :       &account_cb,
     306              :       &pac);
     307           30 :     switch (qs)
     308              :     {
     309            0 :     case GNUNET_DB_STATUS_HARD_ERROR:
     310            0 :       GNUNET_break (0);
     311            0 :       return TALER_MHD_reply_with_error (
     312              :         connection,
     313              :         MHD_HTTP_INTERNAL_SERVER_ERROR,
     314              :         TALER_EC_GENERIC_DB_FETCH_FAILED,
     315              :         "iterate_accounts");
     316            0 :     case GNUNET_DB_STATUS_SOFT_ERROR:
     317            0 :       GNUNET_break (0);
     318            0 :       return TALER_MHD_reply_with_error (
     319              :         connection,
     320              :         MHD_HTTP_INTERNAL_SERVER_ERROR,
     321              :         TALER_EC_GENERIC_DB_FETCH_FAILED,
     322              :         "iterate_accounts");
     323           23 :     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     324           23 :       break;
     325            7 :     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     326            7 :       break;
     327              :     }
     328              : 
     329           30 :     if (pac.have_same_account)
     330              :     {
     331              :       /* Idempotent request */
     332            1 :       return TALER_MHD_REPLY_JSON_PACK (
     333              :         connection,
     334              :         MHD_HTTP_OK,
     335              :         GNUNET_JSON_pack_data_auto (
     336              :           "salt",
     337              :           &pac.salt),
     338              :         GNUNET_JSON_pack_data_auto (
     339              :           "h_wire",
     340              :           &pac.h_wire));
     341              :     }
     342              : 
     343              : #if 1
     344           29 :     if (pac.have_conflicting_account)
     345              :     {
     346              :       /* Conflict, refuse request */
     347            1 :       GNUNET_break_op (0);
     348            1 :       return TALER_MHD_reply_with_error (
     349              :         connection,
     350              :         MHD_HTTP_CONFLICT,
     351              :         TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
     352            1 :         pac.uri.full_payto);
     353              :     }
     354              : #endif
     355              : 
     356           28 :     if (pac.have_any_account)
     357              :     {
     358              :       /* MFA needed */
     359              :       enum GNUNET_GenericReturnValue ret;
     360              : 
     361            5 :       ret = TMH_mfa_check_simple (
     362              :         hc,
     363              :         TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
     364              :         mi);
     365            5 :       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     366              :                   "Account creation MFA check returned %d\n",
     367              :                   (int) ret);
     368            5 :       if (GNUNET_OK != ret)
     369              :       {
     370              :         return (GNUNET_NO == ret)
     371              :         ? MHD_YES
     372            1 :         : MHD_NO;
     373              :       }
     374              :     }
     375              :   }
     376              : 
     377              :   /* All pre-checks clear, now try to activate/setup the new account */
     378              :   {
     379              :     struct TMH_WireMethod *wm;
     380              : 
     381              :     /* convert provided payto URI into internal data structure with salts */
     382           27 :     wm = TMH_setup_wire_account (pac.uri,
     383              :                                  pac.credit_facade_url,
     384              :                                  pac.credit_facade_credentials);
     385           27 :     GNUNET_assert (NULL != wm);
     386           27 :     if (NULL != pac.extra_wire_subject_metadata)
     387              :       wm->extra_wire_subject_metadata
     388            0 :         = GNUNET_strdup (pac.extra_wire_subject_metadata);
     389              :     {
     390           27 :       struct TALER_MERCHANTDB_AccountDetails ad = {
     391              :         .payto_uri = wm->payto_uri,
     392              :         .salt = wm->wire_salt,
     393           27 :         .instance_id = mi->settings.id,
     394              :         .h_wire = wm->h_wire,
     395           27 :         .credit_facade_url = wm->credit_facade_url,
     396           27 :         .credit_facade_credentials = wm->credit_facade_credentials,
     397           27 :         .extra_wire_subject_metadata = wm->extra_wire_subject_metadata,
     398              :         .active = true
     399              :       };
     400              :       enum GNUNET_DB_QueryStatus qs;
     401              :       struct TALER_MerchantWireHashP h_wire;
     402              :       struct TALER_WireSaltP salt;
     403              :       bool not_found;
     404              :       bool conflict;
     405              : 
     406           27 :       TALER_MERCHANTDB_preflight (TMH_db);
     407           27 :       qs = TALER_MERCHANTDB_do_insert_account (TMH_db,
     408              :                                                &ad,
     409              :                                                &h_wire,
     410              :                                                &salt,
     411              :                                                &not_found,
     412              :                                                &conflict);
     413           27 :       switch (qs)
     414              :       {
     415           27 :       case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     416           27 :         break;
     417            0 :       case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     418            0 :         GNUNET_break (0);
     419            0 :         TMH_wire_method_free (wm);
     420            0 :         return TALER_MHD_reply_with_error (
     421              :           connection,
     422              :           MHD_HTTP_INTERNAL_SERVER_ERROR,
     423              :           TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
     424              :           "do_insert_account");
     425            0 :       case GNUNET_DB_STATUS_SOFT_ERROR:
     426            0 :         GNUNET_break (0);
     427            0 :         TMH_wire_method_free (wm);
     428            0 :         return TALER_MHD_reply_with_error (
     429              :           connection,
     430              :           MHD_HTTP_INTERNAL_SERVER_ERROR,
     431              :           TALER_EC_GENERIC_DB_STORE_FAILED,
     432              :           "do_insert_account");
     433            0 :       case GNUNET_DB_STATUS_HARD_ERROR:
     434            0 :         GNUNET_break (0);
     435            0 :         TMH_wire_method_free (wm);
     436            0 :         return TALER_MHD_reply_with_error (
     437              :           connection,
     438              :           MHD_HTTP_INTERNAL_SERVER_ERROR,
     439              :           TALER_EC_GENERIC_DB_STORE_FAILED,
     440              :           "do_insert_account");
     441              :       }
     442           27 :       if (not_found)
     443              :       {
     444              :         /* must have been concurrently deleted, rare! */
     445            0 :         TMH_wire_method_free (wm);
     446            0 :         return TALER_MHD_reply_with_error (
     447              :           connection,
     448              :           MHD_HTTP_NOT_FOUND,
     449              :           TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
     450            0 :           mi->settings.id);
     451              :       }
     452           27 :       if (conflict)
     453              :       {
     454              :         /* Conflicting POST must have been done between our pre-check
     455              :            and the actual transaction, rare! */
     456            0 :         TMH_wire_method_free (wm);
     457            0 :         GNUNET_break_op (0);
     458            0 :         return TALER_MHD_reply_with_error (
     459              :           connection,
     460              :           MHD_HTTP_CONFLICT,
     461              :           TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
     462            0 :           pac.uri.full_payto);
     463              :       }
     464              : 
     465              :       /* Update salt/h_wire in case we re-activated an
     466              :          existing account and found a different salt
     467              :          value already in the DB */
     468           27 :       wm->wire_salt = salt;
     469           27 :       wm->h_wire = h_wire;
     470              : 
     471              :       /* Finally, also update our running process */
     472           27 :       GNUNET_CONTAINER_DLL_insert (mi->wm_head,
     473              :                                    mi->wm_tail,
     474              :                                    wm);
     475              :       /* Note: we may not need to do this, as we notified
     476              :          about the account change above. But also hardly hurts. */
     477           27 :       TMH_reload_instances (mi->settings.id);
     478              :     }
     479           27 :     return TALER_MHD_REPLY_JSON_PACK (
     480              :       connection,
     481              :       MHD_HTTP_OK,
     482              :       GNUNET_JSON_pack_data_auto ("salt",
     483              :                                   &wm->wire_salt),
     484              :       GNUNET_JSON_pack_data_auto ("h_wire",
     485              :                                   &wm->h_wire));
     486              :   }
     487              : }
     488              : 
     489              : 
     490              : /* end of taler-merchant-httpd_post-private-accounts.c */
        

Generated by: LCOV version 2.0-1