taler-merchant-httpd_patch-management-instances-INSTANCE.c (19888B)
1 /* 2 This file is part of TALER 3 (C) 2020-2025 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_patch-management-instances-INSTANCE.c 22 * @brief implementing PATCH /instances/$ID request handling 23 * @author Christian Grothoff 24 */ 25 #include "platform.h" 26 #include "taler-merchant-httpd_patch-management-instances-INSTANCE.h" 27 #include "taler-merchant-httpd_helper.h" 28 #include <taler/taler_json_lib.h> 29 #include <taler/taler_dbevents.h> 30 #include "taler-merchant-httpd_mfa.h" 31 #include "merchant-database/update_instance.h" 32 #include "merchant-database/start.h" 33 34 35 /** 36 * How often do we retry the simple INSERT database transaction? 37 */ 38 #define MAX_RETRIES 3 39 40 41 /** 42 * Free memory used by @a wm 43 * 44 * @param wm wire method to free 45 */ 46 static void 47 free_wm (struct TMH_WireMethod *wm) 48 { 49 GNUNET_free (wm->payto_uri.full_payto); 50 GNUNET_free (wm->wire_method); 51 GNUNET_free (wm); 52 } 53 54 55 /** 56 * PATCH configuration of an existing instance, given its configuration. 57 * 58 * @param mi instance to patch 59 * @param mfa_check true if a MFA check is required 60 * @param connection the MHD connection to handle 61 * @param[in,out] hc context with further information about the request 62 * @return MHD result code 63 */ 64 static enum MHD_Result 65 patch_instances_ID (struct TMH_MerchantInstance *mi, 66 bool mfa_check, 67 struct MHD_Connection *connection, 68 struct TMH_HandlerContext *hc) 69 { 70 struct TALER_MERCHANTDB_InstanceSettings is; 71 const char *name; 72 struct TMH_WireMethod *wm_head = NULL; 73 struct TMH_WireMethod *wm_tail = NULL; 74 const char *iphone = NULL; 75 bool no_transfer_delay; 76 bool no_pay_delay; 77 bool no_wire_transfer_rounding_interval; 78 bool no_refund_delay; 79 struct GNUNET_JSON_Specification spec[] = { 80 GNUNET_JSON_spec_string ("name", 81 &name), 82 GNUNET_JSON_spec_mark_optional ( 83 TALER_JSON_spec_web_url ("website", 84 (const char **) &is.website), 85 NULL), 86 GNUNET_JSON_spec_mark_optional ( 87 GNUNET_JSON_spec_string ("email", 88 (const char **) &is.email), 89 NULL), 90 GNUNET_JSON_spec_mark_optional ( 91 GNUNET_JSON_spec_string ("phone_number", 92 &iphone), 93 NULL), 94 GNUNET_JSON_spec_mark_optional ( 95 GNUNET_JSON_spec_string ("logo", 96 (const char **) &is.logo), 97 NULL), 98 GNUNET_JSON_spec_json ("address", 99 &is.address), 100 GNUNET_JSON_spec_json ("jurisdiction", 101 &is.jurisdiction), 102 GNUNET_JSON_spec_bool ("use_stefan", 103 &is.use_stefan), 104 GNUNET_JSON_spec_mark_optional ( 105 GNUNET_JSON_spec_relative_time ("default_pay_delay", 106 &is.default_pay_delay), 107 &no_pay_delay), 108 GNUNET_JSON_spec_mark_optional ( 109 GNUNET_JSON_spec_relative_time ("default_refund_delay", 110 &is.default_refund_delay), 111 &no_refund_delay), 112 GNUNET_JSON_spec_mark_optional ( 113 GNUNET_JSON_spec_relative_time ("default_wire_transfer_delay", 114 &is.default_wire_transfer_delay), 115 &no_transfer_delay), 116 GNUNET_JSON_spec_mark_optional ( 117 GNUNET_JSON_spec_time_rounder_interval ( 118 "default_wire_transfer_rounding_interval", 119 &is.default_wire_transfer_rounding_interval), 120 &no_wire_transfer_rounding_interval), 121 GNUNET_JSON_spec_end () 122 }; 123 enum GNUNET_DB_QueryStatus qs; 124 125 GNUNET_assert (NULL != mi); 126 memset (&is, 127 0, 128 sizeof (is)); 129 { 130 enum GNUNET_GenericReturnValue res; 131 132 res = TALER_MHD_parse_json_data (connection, 133 hc->request_body, 134 spec); 135 if (GNUNET_OK != res) 136 return (GNUNET_NO == res) 137 ? MHD_YES 138 : MHD_NO; 139 } 140 if (! TMH_location_object_valid (is.address)) 141 { 142 GNUNET_break_op (0); 143 GNUNET_JSON_parse_free (spec); 144 return TALER_MHD_reply_with_error (connection, 145 MHD_HTTP_BAD_REQUEST, 146 TALER_EC_GENERIC_PARAMETER_MALFORMED, 147 "address"); 148 } 149 if ( (NULL != is.logo) && 150 (! TALER_MERCHANT_image_data_url_valid (is.logo)) ) 151 { 152 GNUNET_break_op (0); 153 GNUNET_JSON_parse_free (spec); 154 return TALER_MHD_reply_with_error (connection, 155 MHD_HTTP_BAD_REQUEST, 156 TALER_EC_GENERIC_PARAMETER_MALFORMED, 157 "logo"); 158 } 159 160 if (! TMH_location_object_valid (is.jurisdiction)) 161 { 162 GNUNET_break_op (0); 163 GNUNET_JSON_parse_free (spec); 164 return TALER_MHD_reply_with_error (connection, 165 MHD_HTTP_BAD_REQUEST, 166 TALER_EC_GENERIC_PARAMETER_MALFORMED, 167 "jurisdiction"); 168 } 169 170 if (no_transfer_delay) 171 is.default_wire_transfer_delay = mi->settings.default_wire_transfer_delay; 172 if (no_pay_delay) 173 is.default_pay_delay = mi->settings.default_pay_delay; 174 if (no_wire_transfer_rounding_interval) 175 is.default_wire_transfer_rounding_interval = 176 mi->settings.default_wire_transfer_rounding_interval; 177 if (no_refund_delay) 178 is.default_refund_delay = mi->settings.default_refund_delay; 179 if (GNUNET_TIME_relative_is_forever (is.default_pay_delay)) 180 { 181 GNUNET_break_op (0); 182 GNUNET_JSON_parse_free (spec); 183 return TALER_MHD_reply_with_error (connection, 184 MHD_HTTP_BAD_REQUEST, 185 TALER_EC_GENERIC_PARAMETER_MALFORMED, 186 "default_pay_delay"); 187 } 188 if (GNUNET_TIME_relative_is_forever (is.default_refund_delay)) 189 { 190 GNUNET_break_op (0); 191 GNUNET_JSON_parse_free (spec); 192 return TALER_MHD_reply_with_error (connection, 193 MHD_HTTP_BAD_REQUEST, 194 TALER_EC_GENERIC_PARAMETER_MALFORMED, 195 "default_refund_delay"); 196 } 197 if (GNUNET_TIME_relative_is_forever (is.default_wire_transfer_delay)) 198 { 199 GNUNET_break_op (0); 200 GNUNET_JSON_parse_free (spec); 201 return TALER_MHD_reply_with_error (connection, 202 MHD_HTTP_BAD_REQUEST, 203 TALER_EC_GENERIC_PARAMETER_MALFORMED, 204 "default_wire_transfer_delay"); 205 } 206 if (NULL != iphone) 207 { 208 is.phone = TALER_MERCHANT_phone_validate_normalize (iphone, 209 false); 210 if (NULL == is.phone) 211 { 212 GNUNET_break_op (0); 213 GNUNET_JSON_parse_free (spec); 214 return TALER_MHD_reply_with_error (connection, 215 MHD_HTTP_BAD_REQUEST, 216 TALER_EC_GENERIC_PARAMETER_MALFORMED, 217 "phone_number"); 218 } 219 if ( (NULL != TMH_phone_regex) && 220 (0 != 221 regexec (&TMH_phone_rx, 222 is.phone, 223 0, 224 NULL, 225 0)) ) 226 { 227 GNUNET_break_op (0); 228 GNUNET_JSON_parse_free (spec); 229 return TALER_MHD_reply_with_error (connection, 230 MHD_HTTP_BAD_REQUEST, 231 TALER_EC_GENERIC_PARAMETER_MALFORMED, 232 "phone_number"); 233 } 234 } 235 if ( (NULL != is.email) && 236 (! TALER_MERCHANT_email_valid (is.email)) ) 237 { 238 GNUNET_break_op (0); 239 GNUNET_JSON_parse_free (spec); 240 GNUNET_free (is.phone); 241 return TALER_MHD_reply_with_error (connection, 242 MHD_HTTP_BAD_REQUEST, 243 TALER_EC_GENERIC_PARAMETER_MALFORMED, 244 "email"); 245 } 246 if ( (NULL != is.phone) && 247 (NULL != mi->settings.phone) && 248 (0 == strcmp (mi->settings.phone, 249 is.phone)) ) 250 is.phone_validated = mi->settings.phone_validated; 251 if ( (NULL != is.email) && 252 (NULL != mi->settings.email) && 253 (0 == strcmp (mi->settings.email, 254 is.email)) ) 255 is.email_validated = mi->settings.email_validated; 256 if (mfa_check) 257 { 258 enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; 259 enum TEH_TanChannelSet mtc = TEH_mandatory_tan_channels; 260 261 if ( (0 != (mtc & TMH_TCS_SMS)) && 262 (NULL != mi->settings.phone) && 263 (NULL == is.phone) ) 264 { 265 GNUNET_break_op (0); 266 GNUNET_JSON_parse_free (spec); 267 GNUNET_free (is.phone); 268 return TALER_MHD_reply_with_error ( 269 connection, 270 MHD_HTTP_BAD_REQUEST, 271 TALER_EC_GENERIC_PARAMETER_MISSING, 272 "phone_number"); 273 } 274 if ( (0 != (mtc & TMH_TCS_EMAIL)) && 275 (NULL != mi->settings.email) && 276 (NULL == is.email) ) 277 { 278 GNUNET_break_op (0); 279 GNUNET_JSON_parse_free (spec); 280 GNUNET_free (is.phone); 281 return TALER_MHD_reply_with_error ( 282 connection, 283 MHD_HTTP_BAD_REQUEST, 284 TALER_EC_GENERIC_PARAMETER_MISSING, 285 "email"); 286 } 287 if ( (is.phone_validated || 288 (NULL == is.phone) ) && 289 (0 != (mtc & TMH_TCS_SMS)) ) 290 mtc -= TMH_TCS_SMS; 291 if ( (is.email_validated || 292 (NULL == is.email) ) && 293 (0 != (mtc & TMH_TCS_EMAIL)) ) 294 mtc -= TMH_TCS_EMAIL; 295 switch (mtc) 296 { 297 case TMH_TCS_NONE: 298 ret = GNUNET_OK; 299 break; 300 case TMH_TCS_SMS: 301 GNUNET_assert (NULL != is.phone); 302 is.phone_validated = true; 303 /* validate new phone number, if possible require old e-mail 304 address for authorization */ 305 ret = TMH_mfa_challenges_do (hc, 306 mi->settings.id, 307 TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, 308 true, 309 TALER_MERCHANT_MFA_CHANNEL_SMS, 310 is.phone, 311 0 == (TALER_MERCHANT_MFA_CHANNEL_EMAIL 312 & TEH_mandatory_tan_channels) 313 ? TALER_MERCHANT_MFA_CHANNEL_NONE 314 : TALER_MERCHANT_MFA_CHANNEL_EMAIL, 315 mi->settings.email, 316 TALER_MERCHANT_MFA_CHANNEL_NONE); 317 break; 318 case TMH_TCS_EMAIL: 319 GNUNET_assert (NULL != is.email); 320 is.email_validated = true; 321 /* validate new e-mail address, if possible require old phone 322 address for authorization */ 323 ret = TMH_mfa_challenges_do (hc, 324 mi->settings.id, 325 TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, 326 true, 327 TALER_MERCHANT_MFA_CHANNEL_EMAIL, 328 is.email, 329 0 == (TALER_MERCHANT_MFA_CHANNEL_SMS 330 & TEH_mandatory_tan_channels) 331 ? TALER_MERCHANT_MFA_CHANNEL_NONE 332 : TALER_MERCHANT_MFA_CHANNEL_SMS, 333 mi->settings.phone, 334 TALER_MERCHANT_MFA_CHANNEL_NONE); 335 break; 336 case TMH_TCS_EMAIL_AND_SMS: 337 GNUNET_assert (NULL != is.phone); 338 GNUNET_assert (NULL != is.email); 339 is.phone_validated = true; 340 is.email_validated = true; 341 /* To change both, we require both old and both new 342 addresses to consent */ 343 ret = TMH_mfa_challenges_do (hc, 344 mi->settings.id, 345 TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, 346 true, 347 TALER_MERCHANT_MFA_CHANNEL_EMAIL, 348 mi->settings.email, 349 TALER_MERCHANT_MFA_CHANNEL_EMAIL, 350 is.email, 351 TALER_MERCHANT_MFA_CHANNEL_SMS, 352 mi->settings.phone, 353 TALER_MERCHANT_MFA_CHANNEL_SMS, 354 is.phone, 355 TALER_MERCHANT_MFA_CHANNEL_NONE); 356 break; 357 } 358 if (GNUNET_OK != ret) 359 { 360 GNUNET_JSON_parse_free (spec); 361 GNUNET_free (is.phone); 362 return (GNUNET_NO == ret) 363 ? MHD_YES 364 : MHD_NO; 365 } 366 } 367 368 for (unsigned int retry = 0; retry<MAX_RETRIES; retry++) 369 { 370 /* Cleanup after earlier loops */ 371 { 372 struct TMH_WireMethod *wm; 373 374 while (NULL != (wm = wm_head)) 375 { 376 GNUNET_CONTAINER_DLL_remove (wm_head, 377 wm_tail, 378 wm); 379 free_wm (wm); 380 } 381 } 382 if (GNUNET_OK != 383 TALER_MERCHANTDB_start (TMH_db, 384 "PATCH /instances")) 385 { 386 GNUNET_JSON_parse_free (spec); 387 GNUNET_free (is.phone); 388 return TALER_MHD_reply_with_error (connection, 389 MHD_HTTP_INTERNAL_SERVER_ERROR, 390 TALER_EC_GENERIC_DB_START_FAILED, 391 NULL); 392 } 393 /* Check for equality of settings */ 394 if (! ( (0 == strcmp (mi->settings.name, 395 name)) && 396 ((mi->settings.email == is.email) || 397 (NULL != is.email && NULL != mi->settings.email && 398 0 == strcmp (mi->settings.email, 399 is.email))) && 400 ((mi->settings.phone == is.phone) || 401 (NULL != is.phone && NULL != mi->settings.phone && 402 0 == strcmp (mi->settings.phone, 403 is.phone))) && 404 ((mi->settings.website == is.website) || 405 (NULL != is.website && NULL != mi->settings.website && 406 0 == strcmp (mi->settings.website, 407 is.website))) && 408 ((mi->settings.logo == is.logo) || 409 (NULL != is.logo && NULL != mi->settings.logo && 410 0 == strcmp (mi->settings.logo, 411 is.logo))) && 412 (1 == json_equal (mi->settings.address, 413 is.address)) && 414 (1 == json_equal (mi->settings.jurisdiction, 415 is.jurisdiction)) && 416 (mi->settings.use_stefan == is.use_stefan) && 417 (GNUNET_TIME_relative_cmp (mi->settings.default_wire_transfer_delay, 418 ==, 419 is.default_wire_transfer_delay)) && 420 (GNUNET_TIME_relative_cmp (mi->settings.default_refund_delay, 421 ==, 422 is.default_refund_delay)) && 423 (GNUNET_TIME_relative_cmp (mi->settings.default_pay_delay, 424 ==, 425 is.default_pay_delay)) && 426 (mi->settings.default_wire_transfer_rounding_interval == 427 is.default_wire_transfer_rounding_interval) ) ) 428 { 429 is.id = mi->settings.id; 430 is.name = GNUNET_strdup (name); 431 qs = TALER_MERCHANTDB_update_instance (TMH_db, 432 &is); 433 GNUNET_free (is.name); 434 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) 435 { 436 TALER_MERCHANTDB_rollback (TMH_db); 437 goto retry; 438 } 439 } 440 qs = TALER_MERCHANTDB_commit (TMH_db); 441 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) 442 qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; 443 retry: 444 if (GNUNET_DB_STATUS_SOFT_ERROR == qs) 445 continue; 446 break; 447 } /* for(... MAX_RETRIES) */ 448 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) 449 { 450 /* Hard error or retries exhausted: the transaction did not 451 commit, so do NOT mutate our in-memory settings and report 452 the failure to the client. */ 453 GNUNET_break (0); 454 GNUNET_JSON_parse_free (spec); 455 GNUNET_free (is.phone); 456 return TALER_MHD_reply_with_error (connection, 457 MHD_HTTP_INTERNAL_SERVER_ERROR, 458 TALER_EC_GENERIC_DB_COMMIT_FAILED, 459 NULL); 460 } 461 /* Update our 'settings' */ 462 GNUNET_free (mi->settings.name); 463 GNUNET_free (mi->settings.email); 464 GNUNET_free (mi->settings.phone); 465 GNUNET_free (mi->settings.website); 466 GNUNET_free (mi->settings.logo); 467 json_decref (mi->settings.address); 468 json_decref (mi->settings.jurisdiction); 469 is.id = mi->settings.id; 470 mi->settings = is; 471 mi->settings.address = json_incref (mi->settings.address); 472 mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction); 473 mi->settings.name = GNUNET_strdup (name); 474 if (NULL != is.email) 475 mi->settings.email = GNUNET_strdup (is.email); 476 mi->settings.phone = is.phone; 477 is.phone = NULL; 478 if (NULL != is.website) 479 mi->settings.website = GNUNET_strdup (is.website); 480 if (NULL != is.logo) 481 mi->settings.logo = GNUNET_strdup (is.logo); 482 483 GNUNET_JSON_parse_free (spec); 484 TMH_reload_instances (mi->settings.id); 485 return TALER_MHD_reply_static (connection, 486 MHD_HTTP_NO_CONTENT, 487 NULL, 488 NULL, 489 0); 490 } 491 492 493 enum MHD_Result 494 TMH_private_patch_instances_ID (const struct TMH_RequestHandler *rh, 495 struct MHD_Connection *connection, 496 struct TMH_HandlerContext *hc) 497 { 498 struct TMH_MerchantInstance *mi = hc->instance; 499 500 return patch_instances_ID (mi, 501 true, 502 connection, 503 hc); 504 } 505 506 507 enum MHD_Result 508 TMH_private_patch_instances_default_ID (const struct TMH_RequestHandler *rh, 509 struct MHD_Connection *connection, 510 struct TMH_HandlerContext *hc) 511 { 512 struct TMH_MerchantInstance *mi; 513 514 mi = TMH_lookup_instance (hc->infix); 515 if (NULL == mi) 516 { 517 return TALER_MHD_reply_with_error (connection, 518 MHD_HTTP_NOT_FOUND, 519 TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, 520 hc->infix); 521 } 522 if (mi->deleted) 523 { 524 return TALER_MHD_reply_with_error (connection, 525 MHD_HTTP_CONFLICT, 526 TALER_EC_MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED, 527 hc->infix); 528 } 529 return patch_instances_ID (mi, 530 false, 531 connection, 532 hc); 533 } 534 535 536 /* end of taler-merchant-httpd_patch-management-instances-INSTANCE.c */