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