merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

taler-merchant-httpd_post-private-accounts.c (14796B)


      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 taler-merchant-httpd_post-private-accounts.c
     22  * @brief implementing POST /private/accounts request handling
     23  * @author Christian Grothoff
     24  */
     25 #include "taler/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 
     34 /**
     35  * Maximum number of retries we do on serialization failures.
     36  */
     37 #define MAX_RETRIES 5
     38 
     39 /**
     40  * Closure for account_cb().
     41  */
     42 struct PostAccountContext
     43 {
     44   /**
     45    * Payto URI of the account to add (from the request).
     46    */
     47   struct TALER_FullPayto uri;
     48 
     49   /**
     50    * Hash of the wire details (@e uri and @e salt).
     51    * Set if @e have_same_account is true.
     52    */
     53   struct TALER_MerchantWireHashP h_wire;
     54 
     55   /**
     56    * Salt value used for hashing @e uri.
     57    * Set if @e have_same_account is true.
     58    */
     59   struct TALER_WireSaltP salt;
     60 
     61   /**
     62    * Credit facade URL from the request.
     63    */
     64   const char *credit_facade_url;
     65 
     66   /**
     67    * Facade credentials from the request.
     68    */
     69   const json_t *credit_facade_credentials;
     70 
     71   /**
     72    * Wire subject metadata from the request.
     73    */
     74   const char *extra_wire_subject_metadata;
     75 
     76   /**
     77    * True if we have ANY account already and thus require MFA.
     78    */
     79   bool have_any_account;
     80 
     81   /**
     82    * True if we have exact match already and thus require MFA.
     83    */
     84   bool have_same_account;
     85 
     86   /**
     87    * True if we have an account with the same normalized payto
     88    * already and thus the client can only do PATCH but not POST.
     89    */
     90   bool have_conflicting_account;
     91 };
     92 
     93 
     94 /**
     95  * Callback invoked with information about a bank account.
     96  *
     97  * @param cls closure with a `struct PostAccountContext`
     98  * @param merchant_priv private key of the merchant instance
     99  * @param ad details about the account
    100  */
    101 static void
    102 account_cb (
    103   void *cls,
    104   const struct TALER_MerchantPrivateKeyP *merchant_priv,
    105   const struct TALER_MERCHANTDB_AccountDetails *ad)
    106 {
    107   struct PostAccountContext *pac = cls;
    108 
    109   if (! ad->active)
    110     return;
    111   pac->have_any_account = true;
    112   if ( (0 == TALER_full_payto_cmp (pac->uri,
    113                                    ad->payto_uri) ) &&
    114        ( (pac->credit_facade_credentials ==
    115           ad->credit_facade_credentials) ||
    116          ( (NULL != pac->credit_facade_credentials) &&
    117            (NULL != ad->credit_facade_credentials) &&
    118            (1 == json_equal (pac->credit_facade_credentials,
    119                              ad->credit_facade_credentials)) ) ) &&
    120        ( (pac->extra_wire_subject_metadata ==
    121           ad->extra_wire_subject_metadata) ||
    122          ( (NULL != pac->extra_wire_subject_metadata) &&
    123            (NULL != ad->extra_wire_subject_metadata) &&
    124            (0 == strcmp (pac->extra_wire_subject_metadata,
    125                          ad->extra_wire_subject_metadata)) ) ) &&
    126        ( (pac->credit_facade_url == ad->credit_facade_url) ||
    127          ( (NULL != pac->credit_facade_url) &&
    128            (NULL != ad->credit_facade_url) &&
    129            (0 == strcmp (pac->credit_facade_url,
    130                          ad->credit_facade_url)) ) ) )
    131   {
    132     pac->have_same_account = true;
    133     pac->salt = ad->salt;
    134     pac->h_wire = ad->h_wire;
    135     return;
    136   }
    137 
    138   if (0 == TALER_full_payto_normalize_and_cmp (pac->uri,
    139                                                ad->payto_uri) )
    140   {
    141     pac->have_conflicting_account = true;
    142     return;
    143   }
    144 }
    145 
    146 
    147 MHD_RESULT
    148 TMH_private_post_account (const struct TMH_RequestHandler *rh,
    149                           struct MHD_Connection *connection,
    150                           struct TMH_HandlerContext *hc)
    151 {
    152   struct TMH_MerchantInstance *mi = hc->instance;
    153   struct PostAccountContext pac = { 0 };
    154   struct GNUNET_JSON_Specification ispec[] = {
    155     TALER_JSON_spec_full_payto_uri ("payto_uri",
    156                                     &pac.uri),
    157     GNUNET_JSON_spec_mark_optional (
    158       TALER_JSON_spec_web_url ("credit_facade_url",
    159                                &pac.credit_facade_url),
    160       NULL),
    161     GNUNET_JSON_spec_mark_optional (
    162       GNUNET_JSON_spec_string ("extra_wire_subject_metadata",
    163                                &pac.extra_wire_subject_metadata),
    164       NULL),
    165     GNUNET_JSON_spec_mark_optional (
    166       GNUNET_JSON_spec_object_const ("credit_facade_credentials",
    167                                      &pac.credit_facade_credentials),
    168       NULL),
    169     GNUNET_JSON_spec_end ()
    170   };
    171 
    172   {
    173     enum GNUNET_GenericReturnValue res;
    174 
    175     res = TALER_MHD_parse_json_data (connection,
    176                                      hc->request_body,
    177                                      ispec);
    178     if (GNUNET_OK != res)
    179       return (GNUNET_NO == res)
    180              ? MHD_YES
    181              : MHD_NO;
    182   }
    183 
    184   {
    185     char *err;
    186 
    187     if (NULL !=
    188         (err = TALER_payto_validate (pac.uri)))
    189     {
    190       MHD_RESULT mret;
    191 
    192       GNUNET_break_op (0);
    193       mret = TALER_MHD_reply_with_error (connection,
    194                                          MHD_HTTP_BAD_REQUEST,
    195                                          TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
    196                                          err);
    197       GNUNET_free (err);
    198       return mret;
    199     }
    200   }
    201   if (! TALER_is_valid_subject_metadata_string (
    202         pac.extra_wire_subject_metadata))
    203   {
    204     GNUNET_break_op (0);
    205     return TALER_MHD_reply_with_error (
    206       connection,
    207       MHD_HTTP_BAD_REQUEST,
    208       TALER_EC_GENERIC_PARAMETER_MALFORMED,
    209       "extra_wire_subject_metadata");
    210   }
    211 
    212   {
    213     char *apt = GNUNET_strdup (TMH_allowed_payment_targets);
    214     char *method = TALER_payto_get_method (pac.uri.full_payto);
    215     bool ok;
    216 
    217     ok = false;
    218     for (const char *tok = strtok (apt,
    219                                    " ");
    220          NULL != tok;
    221          tok = strtok (NULL,
    222                        " "))
    223     {
    224       if (0 == strcmp ("*",
    225                        tok))
    226         ok = true;
    227       if (0 == strcmp (method,
    228                        tok))
    229         ok = true;
    230       if (ok)
    231         break;
    232     }
    233     GNUNET_free (method);
    234     GNUNET_free (apt);
    235     if (! ok)
    236     {
    237       GNUNET_break_op (0);
    238       return TALER_MHD_reply_with_error (connection,
    239                                          MHD_HTTP_BAD_REQUEST,
    240                                          TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
    241                                          "The payment target type is forbidden by policy");
    242     }
    243   }
    244 
    245   if ( (NULL != TMH_payment_target_regex) &&
    246        (0 !=
    247         regexec (&TMH_payment_target_re,
    248                  pac.uri.full_payto,
    249                  0,
    250                  NULL,
    251                  0)) )
    252   {
    253     GNUNET_break_op (0);
    254     return TALER_MHD_reply_with_error (connection,
    255                                        MHD_HTTP_BAD_REQUEST,
    256                                        TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
    257                                        "The specific account is forbidden by policy");
    258   }
    259 
    260   if ( (NULL == pac.credit_facade_url) !=
    261        (NULL == pac.credit_facade_credentials) )
    262   {
    263     GNUNET_break_op (0);
    264     return TALER_MHD_reply_with_error (connection,
    265                                        MHD_HTTP_BAD_REQUEST,
    266                                        TALER_EC_GENERIC_PARAMETER_MISSING,
    267                                        (NULL == pac.credit_facade_url)
    268                                        ? "credit_facade_url"
    269                                        : "credit_facade_credentials");
    270   }
    271   if ( (NULL != pac.credit_facade_url) ||
    272        (NULL != pac.credit_facade_credentials) )
    273   {
    274     struct TALER_MERCHANT_BANK_AuthenticationData auth;
    275 
    276     if (GNUNET_OK !=
    277         TALER_MERCHANT_BANK_auth_parse_json (pac.credit_facade_credentials,
    278                                              pac.credit_facade_url,
    279                                              &auth))
    280     {
    281       GNUNET_break_op (0);
    282       return TALER_MHD_reply_with_error (connection,
    283                                          MHD_HTTP_BAD_REQUEST,
    284                                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
    285                                          "credit_facade_url or credit_facade_credentials");
    286     }
    287     TALER_MERCHANT_BANK_auth_free (&auth);
    288   }
    289 
    290   TMH_db->preflight (TMH_db->cls);
    291   for (unsigned int retries = 0;
    292        retries < MAX_RETRIES;
    293        retries++)
    294   {
    295     enum GNUNET_DB_QueryStatus qs;
    296     struct TMH_WireMethod *wm;
    297 
    298     TMH_db->rollback (TMH_db->cls);
    299     if (GNUNET_OK !=
    300         TMH_db->start (TMH_db->cls,
    301                        "post-account"))
    302     {
    303       GNUNET_break (0);
    304       break;
    305     }
    306     qs = TMH_db->select_accounts (TMH_db->cls,
    307                                   mi->settings.id,
    308                                   &account_cb,
    309                                   &pac);
    310     switch (qs)
    311     {
    312     case GNUNET_DB_STATUS_HARD_ERROR:
    313       GNUNET_break (0);
    314       TMH_db->rollback (TMH_db->cls);
    315       return TALER_MHD_reply_with_error (connection,
    316                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    317                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
    318                                          "select_accounts");
    319     case GNUNET_DB_STATUS_SOFT_ERROR:
    320       continue;
    321     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    322       break;
    323     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    324       break;
    325     }
    326 
    327     if (pac.have_same_account)
    328     {
    329       /* Idempotent request */
    330       TMH_db->rollback (TMH_db->cls);
    331       return TALER_MHD_REPLY_JSON_PACK (
    332         connection,
    333         MHD_HTTP_OK,
    334         GNUNET_JSON_pack_data_auto (
    335           "salt",
    336           &pac.salt),
    337         GNUNET_JSON_pack_data_auto (
    338           "h_wire",
    339           &pac.h_wire));
    340 
    341     }
    342 
    343     if (pac.have_conflicting_account)
    344     {
    345       /* Conflict, refuse request */
    346       TMH_db->rollback (TMH_db->cls);
    347       GNUNET_break_op (0);
    348       return TALER_MHD_reply_with_error (connection,
    349                                          MHD_HTTP_CONFLICT,
    350                                          TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
    351                                          pac.uri.full_payto);
    352     }
    353 
    354     if (pac.have_any_account)
    355     {
    356       /* MFA needed */
    357       enum GNUNET_GenericReturnValue ret;
    358 
    359       TMH_db->rollback (TMH_db->cls);
    360       ret = TMH_mfa_check_simple (hc,
    361                                   TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
    362                                   mi);
    363       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    364                   "Account creation MFA check returned %d\n",
    365                   (int) ret);
    366       if (GNUNET_OK != ret)
    367       {
    368         return (GNUNET_NO == ret)
    369         ? MHD_YES
    370         : MHD_NO;
    371       }
    372     }
    373 
    374     /* convert provided payto URI into internal data structure with salts */
    375     wm = TMH_setup_wire_account (pac.uri,
    376                                  pac.credit_facade_url,
    377                                  pac.credit_facade_credentials);
    378     if (NULL != pac.extra_wire_subject_metadata)
    379       wm->extra_wire_subject_metadata
    380         = GNUNET_strdup (pac.extra_wire_subject_metadata);
    381     GNUNET_assert (NULL != wm);
    382     {
    383       struct TALER_MERCHANTDB_AccountDetails ad = {
    384         .payto_uri = wm->payto_uri,
    385         .salt = wm->wire_salt,
    386         .instance_id = mi->settings.id,
    387         .h_wire = wm->h_wire,
    388         .credit_facade_url = wm->credit_facade_url,
    389         .credit_facade_credentials = wm->credit_facade_credentials,
    390         .extra_wire_subject_metadata = wm->extra_wire_subject_metadata,
    391         .active = wm->active
    392       };
    393       struct GNUNET_DB_EventHeaderP es = {
    394         .size = htons (sizeof (es)),
    395         .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED)
    396       };
    397 
    398       qs = TMH_db->insert_account (TMH_db->cls,
    399                                    &ad);
    400       switch (qs)
    401       {
    402       case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    403         break;
    404       case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    405         GNUNET_break (0);
    406         TMH_wire_method_free (wm);
    407         return TALER_MHD_reply_with_error (
    408           connection,
    409           MHD_HTTP_INTERNAL_SERVER_ERROR,
    410           TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
    411           "insert_account");
    412       case GNUNET_DB_STATUS_SOFT_ERROR:
    413         continue;
    414       case GNUNET_DB_STATUS_HARD_ERROR:
    415         GNUNET_break (0);
    416         TMH_wire_method_free (wm);
    417         return TALER_MHD_reply_with_error (
    418           connection,
    419           MHD_HTTP_INTERNAL_SERVER_ERROR,
    420           TALER_EC_GENERIC_DB_STORE_FAILED,
    421           "insert_account");
    422       }
    423 
    424       TMH_db->event_notify (TMH_db->cls,
    425                             &es,
    426                             NULL,
    427                             0);
    428       qs = TMH_db->commit (TMH_db->cls);
    429       switch (qs)
    430       {
    431       case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    432         break;
    433       case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    434         break;
    435       case GNUNET_DB_STATUS_SOFT_ERROR:
    436         TMH_wire_method_free (wm);
    437         continue;
    438       case GNUNET_DB_STATUS_HARD_ERROR:
    439         TMH_wire_method_free (wm);
    440         GNUNET_break (0);
    441         return TALER_MHD_reply_with_error (
    442           connection,
    443           MHD_HTTP_INTERNAL_SERVER_ERROR,
    444           TALER_EC_GENERIC_DB_COMMIT_FAILED,
    445           "post-account");
    446       }
    447       /* Finally, also update our running process */
    448       GNUNET_CONTAINER_DLL_insert (mi->wm_head,
    449                                    mi->wm_tail,
    450                                    wm);
    451       /* Note: we may not need to do this, as we notified
    452          about the account change above. But also hardly hurts. */
    453       TMH_reload_instances (mi->settings.id);
    454     }
    455     return TALER_MHD_REPLY_JSON_PACK (
    456       connection,
    457       MHD_HTTP_OK,
    458       GNUNET_JSON_pack_data_auto ("salt",
    459                                   &wm->wire_salt),
    460       GNUNET_JSON_pack_data_auto ("h_wire",
    461                                   &wm->h_wire));
    462   } /* end retries */
    463   TMH_db->rollback (TMH_db->cls);
    464   return TALER_MHD_reply_with_error (connection,
    465                                      MHD_HTTP_INTERNAL_SERVER_ERROR,
    466                                      TALER_EC_GENERIC_DB_SOFT_FAILURE,
    467                                      "post-accounts");
    468 }
    469 
    470 
    471 /* end of taler-merchant-httpd_post-private-accounts.c */