taler-merchant-httpd_post-private-accounts.c (14492B)
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_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 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 ( 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 if ( (NULL != TMH_payment_target_regex) && 251 (0 != 252 regexec (&TMH_payment_target_re, 253 pac.uri.full_payto, 254 0, 255 NULL, 256 0)) ) 257 { 258 GNUNET_break_op (0); 259 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 if ( (NULL == pac.credit_facade_url) != 267 (NULL == pac.credit_facade_credentials) ) 268 { 269 GNUNET_break_op (0); 270 return TALER_MHD_reply_with_error ( 271 connection, 272 MHD_HTTP_BAD_REQUEST, 273 TALER_EC_GENERIC_PARAMETER_MISSING, 274 (NULL == pac.credit_facade_url) 275 ? "credit_facade_url" 276 : "credit_facade_credentials"); 277 } 278 if ( (NULL != pac.credit_facade_url) || 279 (NULL != pac.credit_facade_credentials) ) 280 { 281 struct TALER_MERCHANT_BANK_AuthenticationData auth; 282 283 if (GNUNET_OK != 284 TALER_MERCHANT_BANK_auth_parse_json (pac.credit_facade_credentials, 285 pac.credit_facade_url, 286 &auth)) 287 { 288 GNUNET_break_op (0); 289 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 TALER_MERCHANT_BANK_auth_free (&auth); 296 } 297 298 { 299 enum GNUNET_DB_QueryStatus qs; 300 301 TALER_MERCHANTDB_preflight (TMH_db); 302 qs = TALER_MERCHANTDB_select_accounts_by_instance ( 303 TMH_db, 304 mi->settings.id, 305 &account_cb, 306 &pac); 307 switch (qs) 308 { 309 case GNUNET_DB_STATUS_HARD_ERROR: 310 GNUNET_break (0); 311 return TALER_MHD_reply_with_error ( 312 connection, 313 MHD_HTTP_INTERNAL_SERVER_ERROR, 314 TALER_EC_GENERIC_DB_FETCH_FAILED, 315 "select_accounts"); 316 case GNUNET_DB_STATUS_SOFT_ERROR: 317 GNUNET_break (0); 318 return TALER_MHD_reply_with_error ( 319 connection, 320 MHD_HTTP_INTERNAL_SERVER_ERROR, 321 TALER_EC_GENERIC_DB_FETCH_FAILED, 322 "select_accounts"); 323 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 324 break; 325 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 326 break; 327 } 328 329 if (pac.have_same_account) 330 { 331 /* Idempotent request */ 332 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 if (pac.have_conflicting_account) 345 { 346 /* Conflict, refuse request */ 347 GNUNET_break_op (0); 348 return TALER_MHD_reply_with_error ( 349 connection, 350 MHD_HTTP_CONFLICT, 351 TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS, 352 pac.uri.full_payto); 353 } 354 #endif 355 356 if (pac.have_any_account) 357 { 358 /* MFA needed */ 359 enum GNUNET_GenericReturnValue ret; 360 361 ret = TMH_mfa_check_simple ( 362 hc, 363 TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, 364 mi); 365 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 366 "Account creation MFA check returned %d\n", 367 (int) ret); 368 if (GNUNET_OK != ret) 369 { 370 return (GNUNET_NO == ret) 371 ? MHD_YES 372 : 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 wm = TMH_setup_wire_account (pac.uri, 383 pac.credit_facade_url, 384 pac.credit_facade_credentials); 385 GNUNET_assert (NULL != wm); 386 if (NULL != pac.extra_wire_subject_metadata) 387 wm->extra_wire_subject_metadata 388 = GNUNET_strdup (pac.extra_wire_subject_metadata); 389 { 390 struct TALER_MERCHANTDB_AccountDetails ad = { 391 .payto_uri = wm->payto_uri, 392 .salt = wm->wire_salt, 393 .instance_id = mi->settings.id, 394 .h_wire = wm->h_wire, 395 .credit_facade_url = wm->credit_facade_url, 396 .credit_facade_credentials = wm->credit_facade_credentials, 397 .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 TALER_MERCHANTDB_preflight (TMH_db); 407 qs = TALER_MERCHANTDB_activate_account (TMH_db, 408 &ad, 409 &h_wire, 410 &salt, 411 ¬_found, 412 &conflict); 413 switch (qs) 414 { 415 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 416 break; 417 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 418 GNUNET_break (0); 419 TMH_wire_method_free (wm); 420 return TALER_MHD_reply_with_error ( 421 connection, 422 MHD_HTTP_INTERNAL_SERVER_ERROR, 423 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 424 "activate_account"); 425 case GNUNET_DB_STATUS_SOFT_ERROR: 426 GNUNET_break (0); 427 TMH_wire_method_free (wm); 428 return TALER_MHD_reply_with_error ( 429 connection, 430 MHD_HTTP_INTERNAL_SERVER_ERROR, 431 TALER_EC_GENERIC_DB_STORE_FAILED, 432 "activate_account"); 433 case GNUNET_DB_STATUS_HARD_ERROR: 434 GNUNET_break (0); 435 TMH_wire_method_free (wm); 436 return TALER_MHD_reply_with_error ( 437 connection, 438 MHD_HTTP_INTERNAL_SERVER_ERROR, 439 TALER_EC_GENERIC_DB_STORE_FAILED, 440 "activate_account"); 441 } 442 if (not_found) 443 { 444 /* must have been concurrently deleted, rare! */ 445 TMH_wire_method_free (wm); 446 return TALER_MHD_reply_with_error ( 447 connection, 448 MHD_HTTP_NOT_FOUND, 449 TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, 450 mi->settings.id); 451 } 452 if (conflict) 453 { 454 /* Conflicting POST must have been done between our pre-check 455 and the actual transaction, rare! */ 456 TMH_wire_method_free (wm); 457 GNUNET_break_op (0); 458 return TALER_MHD_reply_with_error ( 459 connection, 460 MHD_HTTP_CONFLICT, 461 TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS, 462 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 wm->wire_salt = salt; 469 wm->h_wire = h_wire; 470 471 /* Finally, also update our running process */ 472 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 TMH_reload_instances (mi->settings.id); 478 } 479 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 */