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 */