merchant

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

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


      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/activate_account.h"
     34 #include "merchant-database/select_accounts.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 account_cb (
    106   void *cls,
    107   const struct TALER_MerchantPrivateKeyP *merchant_priv,
    108   const struct TALER_MERCHANTDB_AccountDetails *ad)
    109 {
    110   struct PostAccountContext *pac = cls;
    111 
    112   if (! ad->active)
    113     return;
    114   pac->have_any_account = true;
    115   if ( (0 == TALER_full_payto_cmp (pac->uri,
    116                                    ad->payto_uri) ) &&
    117        ( (pac->credit_facade_credentials ==
    118           ad->credit_facade_credentials) ||
    119          ( (NULL != pac->credit_facade_credentials) &&
    120            (NULL != ad->credit_facade_credentials) &&
    121            (1 == json_equal (pac->credit_facade_credentials,
    122                              ad->credit_facade_credentials)) ) ) &&
    123        ( (pac->extra_wire_subject_metadata ==
    124           ad->extra_wire_subject_metadata) ||
    125          ( (NULL != pac->extra_wire_subject_metadata) &&
    126            (NULL != ad->extra_wire_subject_metadata) &&
    127            (0 == strcmp (pac->extra_wire_subject_metadata,
    128                          ad->extra_wire_subject_metadata)) ) ) &&
    129        ( (pac->credit_facade_url == ad->credit_facade_url) ||
    130          ( (NULL != pac->credit_facade_url) &&
    131            (NULL != ad->credit_facade_url) &&
    132            (0 == strcmp (pac->credit_facade_url,
    133                          ad->credit_facade_url)) ) ) )
    134   {
    135     pac->have_same_account = true;
    136     pac->salt = ad->salt;
    137     pac->h_wire = ad->h_wire;
    138     return;
    139   }
    140 
    141   if (0 == TALER_full_payto_normalize_and_cmp (pac->uri,
    142                                                ad->payto_uri) )
    143   {
    144     pac->have_conflicting_account = true;
    145     return;
    146   }
    147 }
    148 
    149 
    150 enum MHD_Result
    151 TMH_private_post_account (const struct TMH_RequestHandler *rh,
    152                           struct MHD_Connection *connection,
    153                           struct TMH_HandlerContext *hc)
    154 {
    155   struct TMH_MerchantInstance *mi = hc->instance;
    156   struct PostAccountContext pac = { 0 };
    157   struct GNUNET_JSON_Specification ispec[] = {
    158     TALER_JSON_spec_full_payto_uri ("payto_uri",
    159                                     &pac.uri),
    160     GNUNET_JSON_spec_mark_optional (
    161       TALER_JSON_spec_web_url ("credit_facade_url",
    162                                &pac.credit_facade_url),
    163       NULL),
    164     GNUNET_JSON_spec_mark_optional (
    165       GNUNET_JSON_spec_string ("extra_wire_subject_metadata",
    166                                &pac.extra_wire_subject_metadata),
    167       NULL),
    168     GNUNET_JSON_spec_mark_optional (
    169       GNUNET_JSON_spec_object_const ("credit_facade_credentials",
    170                                      &pac.credit_facade_credentials),
    171       NULL),
    172     GNUNET_JSON_spec_end ()
    173   };
    174 
    175   {
    176     enum GNUNET_GenericReturnValue res;
    177 
    178     res = TALER_MHD_parse_json_data (connection,
    179                                      hc->request_body,
    180                                      ispec);
    181     if (GNUNET_OK != res)
    182       return (GNUNET_NO == res)
    183              ? MHD_YES
    184              : MHD_NO;
    185   }
    186 
    187   {
    188     char *err;
    189 
    190     if (NULL !=
    191         (err = TALER_payto_validate (pac.uri)))
    192     {
    193       enum MHD_Result mret;
    194 
    195       GNUNET_break_op (0);
    196       mret = TALER_MHD_reply_with_error (
    197         connection,
    198         MHD_HTTP_BAD_REQUEST,
    199         TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
    200         err);
    201       GNUNET_free (err);
    202       return mret;
    203     }
    204   }
    205   if (! TALER_is_valid_subject_metadata_string (
    206         pac.extra_wire_subject_metadata))
    207   {
    208     GNUNET_break_op (0);
    209     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     char *apt = GNUNET_strdup (TMH_allowed_payment_targets);
    218     char *method = TALER_payto_get_method (pac.uri.full_payto);
    219     bool ok;
    220 
    221     ok = false;
    222     for (const char *tok = strtok (apt,
    223                                    " ");
    224          NULL != tok;
    225          tok = strtok (NULL,
    226                        " "))
    227     {
    228       if (0 == strcmp ("*",
    229                        tok))
    230         ok = true;
    231       if (0 == strcmp (method,
    232                        tok))
    233         ok = true;
    234       if (ok)
    235         break;
    236     }
    237     GNUNET_free (method);
    238     GNUNET_free (apt);
    239     if (! ok)
    240     {
    241       GNUNET_break_op (0);
    242       return TALER_MHD_reply_with_error (connection,
    243                                          MHD_HTTP_BAD_REQUEST,
    244                                          TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
    245                                          "The payment target type is forbidden by policy");
    246     }
    247   }
    248 
    249   if ( (NULL != TMH_payment_target_regex) &&
    250        (0 !=
    251         regexec (&TMH_payment_target_re,
    252                  pac.uri.full_payto,
    253                  0,
    254                  NULL,
    255                  0)) )
    256   {
    257     GNUNET_break_op (0);
    258     return TALER_MHD_reply_with_error (connection,
    259                                        MHD_HTTP_BAD_REQUEST,
    260                                        TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
    261                                        "The specific account is forbidden by policy");
    262   }
    263 
    264   if ( (NULL == pac.credit_facade_url) !=
    265        (NULL == pac.credit_facade_credentials) )
    266   {
    267     GNUNET_break_op (0);
    268     return TALER_MHD_reply_with_error (connection,
    269                                        MHD_HTTP_BAD_REQUEST,
    270                                        TALER_EC_GENERIC_PARAMETER_MISSING,
    271                                        (NULL == pac.credit_facade_url)
    272                                        ? "credit_facade_url"
    273                                        : "credit_facade_credentials");
    274   }
    275   if ( (NULL != pac.credit_facade_url) ||
    276        (NULL != pac.credit_facade_credentials) )
    277   {
    278     struct TALER_MERCHANT_BANK_AuthenticationData auth;
    279 
    280     if (GNUNET_OK !=
    281         TALER_MERCHANT_BANK_auth_parse_json (pac.credit_facade_credentials,
    282                                              pac.credit_facade_url,
    283                                              &auth))
    284     {
    285       GNUNET_break_op (0);
    286       return TALER_MHD_reply_with_error (connection,
    287                                          MHD_HTTP_BAD_REQUEST,
    288                                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
    289                                          "credit_facade_url or credit_facade_credentials");
    290     }
    291     TALER_MERCHANT_BANK_auth_free (&auth);
    292   }
    293 
    294   {
    295     enum GNUNET_DB_QueryStatus qs;
    296 
    297     TALER_MERCHANTDB_preflight (TMH_db);
    298     qs = TALER_MERCHANTDB_select_accounts (TMH_db,
    299                                            mi->settings.id,
    300                                            &account_cb,
    301                                            &pac);
    302     switch (qs)
    303     {
    304     case GNUNET_DB_STATUS_HARD_ERROR:
    305       GNUNET_break (0);
    306       return TALER_MHD_reply_with_error (connection,
    307                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    308                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
    309                                          "select_accounts");
    310     case GNUNET_DB_STATUS_SOFT_ERROR:
    311       GNUNET_break (0);
    312       return TALER_MHD_reply_with_error (connection,
    313                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    314                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
    315                                          "select_accounts");
    316     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    317       break;
    318     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    319       break;
    320     }
    321 
    322     if (pac.have_same_account)
    323     {
    324       /* Idempotent request */
    325       return TALER_MHD_REPLY_JSON_PACK (
    326         connection,
    327         MHD_HTTP_OK,
    328         GNUNET_JSON_pack_data_auto (
    329           "salt",
    330           &pac.salt),
    331         GNUNET_JSON_pack_data_auto (
    332           "h_wire",
    333           &pac.h_wire));
    334     }
    335 
    336     if (pac.have_conflicting_account)
    337     {
    338       /* Conflict, refuse request */
    339       GNUNET_break_op (0);
    340       return TALER_MHD_reply_with_error (connection,
    341                                          MHD_HTTP_CONFLICT,
    342                                          TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
    343                                          pac.uri.full_payto);
    344     }
    345 
    346     if (pac.have_any_account)
    347     {
    348       /* MFA needed */
    349       enum GNUNET_GenericReturnValue ret;
    350 
    351       ret = TMH_mfa_check_simple (hc,
    352                                   TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
    353                                   mi);
    354       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    355                   "Account creation MFA check returned %d\n",
    356                   (int) ret);
    357       if (GNUNET_OK != ret)
    358       {
    359         return (GNUNET_NO == ret)
    360         ? MHD_YES
    361         : MHD_NO;
    362       }
    363     }
    364   }
    365 
    366   /* All pre-checks clear, now try to activate/setup the new account */
    367   {
    368     struct TMH_WireMethod *wm;
    369 
    370     /* convert provided payto URI into internal data structure with salts */
    371     wm = TMH_setup_wire_account (pac.uri,
    372                                  pac.credit_facade_url,
    373                                  pac.credit_facade_credentials);
    374     GNUNET_assert (NULL != wm);
    375     if (NULL != pac.extra_wire_subject_metadata)
    376       wm->extra_wire_subject_metadata
    377         = GNUNET_strdup (pac.extra_wire_subject_metadata);
    378     {
    379       struct TALER_MERCHANTDB_AccountDetails ad = {
    380         .payto_uri = wm->payto_uri,
    381         .salt = wm->wire_salt,
    382         .instance_id = mi->settings.id,
    383         .h_wire = wm->h_wire,
    384         .credit_facade_url = wm->credit_facade_url,
    385         .credit_facade_credentials = wm->credit_facade_credentials,
    386         .extra_wire_subject_metadata = wm->extra_wire_subject_metadata,
    387         .active = true
    388       };
    389       enum GNUNET_DB_QueryStatus qs;
    390       struct TALER_MerchantWireHashP h_wire;
    391       struct TALER_WireSaltP salt;
    392       bool not_found;
    393       bool conflict;
    394 
    395       TALER_MERCHANTDB_preflight (TMH_db);
    396       qs = TALER_MERCHANTDB_activate_account (TMH_db,
    397                                               &ad,
    398                                               &h_wire,
    399                                               &salt,
    400                                               &not_found,
    401                                               &conflict);
    402       switch (qs)
    403       {
    404       case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    405         break;
    406       case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    407         GNUNET_break (0);
    408         TMH_wire_method_free (wm);
    409         return TALER_MHD_reply_with_error (
    410           connection,
    411           MHD_HTTP_INTERNAL_SERVER_ERROR,
    412           TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
    413           "activate_account");
    414       case GNUNET_DB_STATUS_SOFT_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           "activate_account");
    422       case GNUNET_DB_STATUS_HARD_ERROR:
    423         GNUNET_break (0);
    424         TMH_wire_method_free (wm);
    425         return TALER_MHD_reply_with_error (
    426           connection,
    427           MHD_HTTP_INTERNAL_SERVER_ERROR,
    428           TALER_EC_GENERIC_DB_STORE_FAILED,
    429           "activate_account");
    430       }
    431       if (not_found)
    432       {
    433         /* must have been concurrently deleted, rare! */
    434         TMH_wire_method_free (wm);
    435         return TALER_MHD_reply_with_error (
    436           connection,
    437           MHD_HTTP_NOT_FOUND,
    438           TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
    439           mi->settings.id);
    440       }
    441       if (conflict)
    442       {
    443         /* Conflicting POST must have been done between our pre-check
    444            and the actual transaction, rare! */
    445         TMH_wire_method_free (wm);
    446         GNUNET_break_op (0);
    447         return TALER_MHD_reply_with_error (
    448           connection,
    449           MHD_HTTP_CONFLICT,
    450           TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
    451           pac.uri.full_payto);
    452       }
    453 
    454       /* Update salt/h_wire in case we re-activated an
    455          existing account and found a different salt
    456          value already in the DB */
    457       wm->wire_salt = salt;
    458       wm->h_wire = h_wire;
    459 
    460       /* Finally, also update our running process */
    461       GNUNET_CONTAINER_DLL_insert (mi->wm_head,
    462                                    mi->wm_tail,
    463                                    wm);
    464       /* Note: we may not need to do this, as we notified
    465          about the account change above. But also hardly hurts. */
    466       TMH_reload_instances (mi->settings.id);
    467     }
    468     return TALER_MHD_REPLY_JSON_PACK (
    469       connection,
    470       MHD_HTTP_OK,
    471       GNUNET_JSON_pack_data_auto ("salt",
    472                                   &wm->wire_salt),
    473       GNUNET_JSON_pack_data_auto ("h_wire",
    474                                   &wm->h_wire));
    475   }
    476 }
    477 
    478 
    479 /* end of taler-merchant-httpd_post-private-accounts.c */