exchange_api_post-aml-OFFICER_PUB-decision.c (17453B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2023, 2024, 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 13 You should have received a copy of the GNU General Public License along with 14 TALER; see the file COPYING. If not, see 15 <http://www.gnu.org/licenses/> 16 */ 17 /** 18 * @file lib/exchange_api_post-aml-OFFICER_PUB-decision.c 19 * @brief functions to add an AML decision by an AML officer 20 * @author Christian Grothoff 21 */ 22 #include "taler/taler_json_lib.h" 23 #include <microhttpd.h> 24 #include <gnunet/gnunet_curl_lib.h> 25 #include "taler/exchange/post-aml-OFFICER_PUB-decision.h" 26 #include "exchange_api_curl_defaults.h" 27 #include "taler/taler_signatures.h" 28 #include "taler/taler_curl_lib.h" 29 30 31 /** 32 * @brief A POST /aml/$OFFICER_PUB/decision Handle 33 */ 34 struct TALER_EXCHANGE_PostAmlDecisionHandle 35 { 36 37 /** 38 * The base URL of the exchange. 39 */ 40 char *base_url; 41 42 /** 43 * The full URL for this request. 44 */ 45 char *url; 46 47 /** 48 * Minor context that holds body and headers. 49 */ 50 struct TALER_CURL_PostContext post_ctx; 51 52 /** 53 * Handle for the request. 54 */ 55 struct GNUNET_CURL_Job *job; 56 57 /** 58 * Function to call with the result. 59 */ 60 TALER_EXCHANGE_PostAmlDecisionCallback cb; 61 62 /** 63 * Closure for @e cb. 64 */ 65 TALER_EXCHANGE_POST_AML_DECISION_RESULT_CLOSURE *cb_cls; 66 67 /** 68 * Reference to the execution context. 69 */ 70 struct GNUNET_CURL_Context *ctx; 71 72 /** 73 * Public key of the AML officer. 74 */ 75 struct TALER_AmlOfficerPublicKeyP officer_pub; 76 77 /** 78 * Private key of the AML officer. 79 */ 80 struct TALER_AmlOfficerPrivateKeyP officer_priv; 81 82 /** 83 * Hash of the payto URI of the account the decision is about. 84 */ 85 struct TALER_NormalizedPaytoHashP h_payto; 86 87 /** 88 * When was the decision made. 89 */ 90 struct GNUNET_TIME_Timestamp decision_time; 91 92 /** 93 * Human-readable justification. 94 */ 95 char *justification; 96 97 /** 98 * True to keep the investigation open. 99 */ 100 bool keep_investigating; 101 102 /** 103 * Pre-built new_rules JSON object. 104 */ 105 json_t *new_rules; 106 107 /** 108 * Optional: full payto URI string, may be NULL. 109 */ 110 char *payto_uri_str; 111 112 /** 113 * Optional: space-separated list of measures to trigger immediately 114 * (from options, may be NULL). 115 */ 116 char *new_measures; 117 118 /** 119 * Optional: JSON object with account properties 120 * (from options, may be NULL). 121 */ 122 json_t *properties; 123 124 /** 125 * Optional: JSON array of events to trigger 126 * (from options; may be NULL). 127 */ 128 json_t *jevents; 129 130 /** 131 * Optional: JSON object with KYC attributes 132 * (from options; may be NULL). 133 */ 134 json_t *attributes; 135 136 /** 137 * Optional: expiration time for KYC attributes. 138 * Only meaningful if @e attributes is non-NULL. 139 */ 140 struct GNUNET_TIME_Timestamp attributes_expiration; 141 142 }; 143 144 145 /** 146 * Function called when we're done processing the 147 * HTTP POST /aml/$OFFICER_PUB/decision request. 148 * 149 * @param cls the `struct TALER_EXCHANGE_PostAmlDecisionHandle *` 150 * @param response_code HTTP response code, 0 on error 151 * @param response response body, NULL if not in JSON 152 */ 153 static void 154 handle_post_aml_decision_finished (void *cls, 155 long response_code, 156 const void *response) 157 { 158 struct TALER_EXCHANGE_PostAmlDecisionHandle *padh = cls; 159 const json_t *json = response; 160 struct TALER_EXCHANGE_PostAmlDecisionResponse pr = { 161 .hr.http_status = (unsigned int) response_code, 162 .hr.reply = json 163 }; 164 165 padh->job = NULL; 166 switch (response_code) 167 { 168 case 0: 169 pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 170 pr.hr.hint = "server offline?"; 171 break; 172 case MHD_HTTP_NO_CONTENT: 173 break; 174 case MHD_HTTP_FORBIDDEN: 175 pr.hr.ec = TALER_JSON_get_error_code (json); 176 pr.hr.hint = TALER_JSON_get_error_hint (json); 177 break; 178 case MHD_HTTP_CONFLICT: 179 pr.hr.ec = TALER_JSON_get_error_code (json); 180 pr.hr.hint = TALER_JSON_get_error_hint (json); 181 break; 182 default: 183 GNUNET_break_op (0); 184 pr.hr.ec = TALER_JSON_get_error_code (json); 185 pr.hr.hint = TALER_JSON_get_error_hint (json); 186 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 187 "Unexpected response code %u/%d for POST AML decision\n", 188 (unsigned int) response_code, 189 (int) pr.hr.ec); 190 break; 191 } 192 if (NULL != padh->cb) 193 { 194 padh->cb (padh->cb_cls, 195 &pr); 196 padh->cb = NULL; 197 } 198 TALER_EXCHANGE_post_aml_decision_cancel (padh); 199 } 200 201 202 /** 203 * Build the new_rules JSON object from rules and measures arrays. 204 * 205 * @param successor_measure optional successor measure name 206 * @param expiration_time when the new rules expire 207 * @param num_rules length of @a rules 208 * @param rules the rules array 209 * @param num_measures length of @a measures 210 * @param measures the measures array 211 * @return new JSON object (caller owns reference), NULL on error 212 */ 213 static json_t * 214 build_new_rules ( 215 const char *successor_measure, 216 struct GNUNET_TIME_Timestamp expiration_time, 217 unsigned int num_rules, 218 const struct TALER_EXCHANGE_AccountRule rules[static num_rules], 219 unsigned int num_measures, 220 const struct TALER_EXCHANGE_MeasureInformation measures[static num_measures]) 221 { 222 json_t *jrules; 223 json_t *jmeasures; 224 225 jrules = json_array (); 226 GNUNET_assert (NULL != jrules); 227 for (unsigned int i = 0; i < num_rules; i++) 228 { 229 const struct TALER_EXCHANGE_AccountRule *al = &rules[i]; 230 json_t *ameasures; 231 json_t *rule; 232 233 ameasures = json_array (); 234 GNUNET_assert (NULL != ameasures); 235 for (unsigned int j = 0; j < al->num_measures; j++) 236 GNUNET_assert (0 == 237 json_array_append_new (ameasures, 238 json_string (al->measures[j]))); 239 rule = GNUNET_JSON_PACK ( 240 TALER_JSON_pack_kycte ("operation_type", 241 al->operation_type), 242 TALER_JSON_pack_amount ("threshold", 243 &al->threshold), 244 GNUNET_JSON_pack_time_rel ("timeframe", 245 al->timeframe), 246 GNUNET_JSON_pack_array_steal ("measures", 247 ameasures), 248 GNUNET_JSON_pack_allow_null ( 249 GNUNET_JSON_pack_string ("rule_name", 250 al->rule_name)), 251 GNUNET_JSON_pack_bool ("exposed", 252 al->exposed), 253 GNUNET_JSON_pack_bool ("is_and_combinator", 254 al->is_and_combinator), 255 GNUNET_JSON_pack_uint64 ("display_priority", 256 al->display_priority) 257 ); 258 GNUNET_break (0 == 259 json_array_append_new (jrules, 260 rule)); 261 } 262 263 jmeasures = json_object (); 264 GNUNET_assert (NULL != jmeasures); 265 for (unsigned int i = 0; i < num_measures; i++) 266 { 267 const struct TALER_EXCHANGE_MeasureInformation *mi = &measures[i]; 268 json_t *measure; 269 270 measure = GNUNET_JSON_PACK ( 271 GNUNET_JSON_pack_string ("check_name", 272 mi->check_name), 273 GNUNET_JSON_pack_allow_null ( 274 GNUNET_JSON_pack_string ("prog_name", 275 mi->prog_name)), 276 /* We pack "NONE" for unknown, NULL would also be OK in that case. */ 277 TALER_JSON_pack_kycte ("operation_type", 278 mi->operation_type), 279 GNUNET_JSON_pack_allow_null ( 280 GNUNET_JSON_pack_object_incref ("context", 281 (json_t *) mi->context)) 282 ); 283 GNUNET_break (0 == 284 json_object_set_new (jmeasures, 285 mi->measure_name, 286 measure)); 287 } 288 289 return GNUNET_JSON_PACK ( 290 GNUNET_JSON_pack_timestamp ("expiration_time", 291 expiration_time), 292 GNUNET_JSON_pack_allow_null ( 293 GNUNET_JSON_pack_string ("successor_measure", 294 successor_measure)), 295 GNUNET_JSON_pack_array_steal ("rules", 296 jrules), 297 GNUNET_JSON_pack_object_steal ("custom_measures", 298 jmeasures) 299 ); 300 } 301 302 303 struct TALER_EXCHANGE_PostAmlDecisionHandle * 304 TALER_EXCHANGE_post_aml_decision_create ( 305 struct GNUNET_CURL_Context *ctx, 306 const char *url, 307 const struct TALER_NormalizedPaytoHashP *h_payto, 308 struct GNUNET_TIME_Timestamp decision_time, 309 const char *successor_measure, 310 struct GNUNET_TIME_Timestamp expiration_time, 311 unsigned int num_rules, 312 const struct TALER_EXCHANGE_AccountRule rules[static num_rules], 313 unsigned int num_measures, 314 const struct TALER_EXCHANGE_MeasureInformation measures[static num_measures], 315 bool keep_investigating, 316 const char *justification, 317 const struct TALER_AmlOfficerPrivateKeyP *officer_priv) 318 { 319 struct TALER_EXCHANGE_PostAmlDecisionHandle *padh; 320 json_t *new_rules; 321 322 new_rules = build_new_rules (successor_measure, 323 expiration_time, 324 num_rules, 325 rules, 326 num_measures, 327 measures); 328 if (NULL == new_rules) 329 { 330 GNUNET_break (0); 331 return NULL; 332 } 333 334 padh = GNUNET_new (struct TALER_EXCHANGE_PostAmlDecisionHandle); 335 padh->ctx = ctx; 336 padh->base_url = GNUNET_strdup (url); 337 padh->h_payto = *h_payto; 338 padh->decision_time = decision_time; 339 padh->justification = GNUNET_strdup (justification); 340 padh->keep_investigating = keep_investigating; 341 padh->new_rules = new_rules; 342 padh->officer_priv = *officer_priv; 343 GNUNET_CRYPTO_eddsa_key_get_public (&officer_priv->eddsa_priv, 344 &padh->officer_pub.eddsa_pub); 345 return padh; 346 } 347 348 349 enum GNUNET_GenericReturnValue 350 TALER_EXCHANGE_post_aml_decision_set_options_ ( 351 struct TALER_EXCHANGE_PostAmlDecisionHandle *padh, 352 unsigned int num_options, 353 const struct TALER_EXCHANGE_PostAmlDecisionOptionValue options[]) 354 { 355 for (unsigned int i = 0; i < num_options; i++) 356 { 357 const struct TALER_EXCHANGE_PostAmlDecisionOptionValue *opt = &options[i]; 358 359 switch (opt->option) 360 { 361 case TALER_EXCHANGE_POST_AML_DECISION_OPTION_END: 362 return GNUNET_OK; 363 case TALER_EXCHANGE_POST_AML_DECISION_OPTION_PAYTO_URI: 364 GNUNET_free (padh->payto_uri_str); 365 padh->payto_uri_str = 366 (NULL != opt->details.payto_uri.full_payto) 367 ? GNUNET_strdup (opt->details.payto_uri.full_payto) 368 : NULL; 369 break; 370 case TALER_EXCHANGE_POST_AML_DECISION_OPTION_NEW_MEASURES: 371 GNUNET_free (padh->new_measures); 372 padh->new_measures = 373 (NULL != opt->details.new_measures) 374 ? GNUNET_strdup (opt->details.new_measures) 375 : NULL; 376 break; 377 case TALER_EXCHANGE_POST_AML_DECISION_OPTION_PROPERTIES: 378 if (NULL != padh->properties) 379 json_decref (padh->properties); 380 padh->properties = 381 (NULL != opt->details.properties) 382 ? json_incref ((json_t *) opt->details.properties) 383 : NULL; 384 break; 385 case TALER_EXCHANGE_POST_AML_DECISION_OPTION_EVENTS: 386 { 387 if (NULL != padh->jevents) 388 json_decref (padh->jevents); 389 if (0 == opt->details.events.num_events) 390 { 391 padh->jevents = NULL; 392 } 393 else 394 { 395 padh->jevents = json_array (); 396 GNUNET_assert (NULL != padh->jevents); 397 for (unsigned int j = 0; j < opt->details.events.num_events; j++) 398 GNUNET_assert (0 == 399 json_array_append_new ( 400 padh->jevents, 401 json_string (opt->details.events.events[j]))); 402 } 403 } 404 break; 405 case TALER_EXCHANGE_POST_AML_DECISION_OPTION_ATTRIBUTES: 406 if (NULL != padh->attributes) 407 json_decref (padh->attributes); 408 padh->attributes = 409 (NULL != opt->details.attributes) 410 ? json_incref ((json_t *) opt->details.attributes) 411 : NULL; 412 break; 413 case TALER_EXCHANGE_POST_AML_DECISION_OPTION_ATTRIBUTES_EXPIRATION: 414 padh->attributes_expiration = opt->details.attributes_expiration; 415 break; 416 default: 417 GNUNET_break (0); 418 return GNUNET_SYSERR; 419 } 420 } 421 return GNUNET_OK; 422 } 423 424 425 enum TALER_ErrorCode 426 TALER_EXCHANGE_post_aml_decision_start ( 427 struct TALER_EXCHANGE_PostAmlDecisionHandle *padh, 428 TALER_EXCHANGE_PostAmlDecisionCallback cb, 429 TALER_EXCHANGE_POST_AML_DECISION_RESULT_CLOSURE *cb_cls) 430 { 431 CURL *eh; 432 struct TALER_AmlOfficerSignatureP officer_sig; 433 json_t *body; 434 char *path; 435 char opus[sizeof (padh->officer_pub) * 2]; 436 char *end; 437 struct TALER_FullPayto payto_uri_val = { 438 .full_payto = padh->payto_uri_str 439 }; 440 441 padh->cb = cb; 442 padh->cb_cls = cb_cls; 443 TALER_officer_aml_decision_sign (padh->justification, 444 padh->decision_time, 445 &padh->h_payto, 446 padh->new_rules, 447 padh->properties, 448 padh->new_measures, 449 padh->keep_investigating, 450 &padh->officer_priv, 451 &officer_sig); 452 453 end = GNUNET_STRINGS_data_to_string ( 454 &padh->officer_pub, 455 sizeof (padh->officer_pub), 456 opus, 457 sizeof (opus)); 458 *end = '\0'; 459 GNUNET_asprintf (&path, 460 "aml/%s/decision", 461 opus); 462 padh->url = TALER_url_join (padh->base_url, 463 path, 464 NULL); 465 GNUNET_free (path); 466 if (NULL == padh->url) 467 { 468 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 469 "Could not construct request URL.\n"); 470 return TALER_EC_GENERIC_CONFIGURATION_INVALID; 471 } 472 473 body = GNUNET_JSON_PACK ( 474 GNUNET_JSON_pack_string ("justification", 475 padh->justification), 476 GNUNET_JSON_pack_data_auto ("h_payto", 477 &padh->h_payto), 478 GNUNET_JSON_pack_allow_null ( 479 TALER_JSON_pack_full_payto ("payto_uri", 480 payto_uri_val)), 481 GNUNET_JSON_pack_object_incref ("new_rules", 482 padh->new_rules), 483 GNUNET_JSON_pack_allow_null ( 484 GNUNET_JSON_pack_object_incref ("properties", 485 padh->properties)), 486 GNUNET_JSON_pack_allow_null ( 487 GNUNET_JSON_pack_string ("new_measures", 488 padh->new_measures)), 489 GNUNET_JSON_pack_bool ("keep_investigating", 490 padh->keep_investigating), 491 GNUNET_JSON_pack_data_auto ("officer_sig", 492 &officer_sig), 493 GNUNET_JSON_pack_timestamp ("decision_time", 494 padh->decision_time), 495 GNUNET_JSON_pack_allow_null ( 496 GNUNET_JSON_pack_array_incref ("events", 497 padh->jevents)), 498 GNUNET_JSON_pack_allow_null ( 499 GNUNET_JSON_pack_object_incref ("attributes", 500 padh->attributes)) 501 ); 502 if (NULL != padh->attributes) 503 { 504 GNUNET_assert ( 505 0 == 506 json_object_set_new ( 507 body, 508 "attributes_expiration", 509 GNUNET_JSON_from_timestamp (padh->attributes_expiration))); 510 } 511 512 eh = TALER_EXCHANGE_curl_easy_get_ (padh->url); 513 if ( (NULL == eh) || 514 (GNUNET_OK != 515 TALER_curl_easy_post (&padh->post_ctx, 516 eh, 517 body)) ) 518 { 519 GNUNET_break (0); 520 if (NULL != eh) 521 curl_easy_cleanup (eh); 522 json_decref (body); 523 GNUNET_free (padh->url); 524 padh->url = NULL; 525 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 526 } 527 json_decref (body); 528 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 529 "Requesting URL '%s'\n", 530 padh->url); 531 padh->job = GNUNET_CURL_job_add2 (padh->ctx, 532 eh, 533 padh->post_ctx.headers, 534 &handle_post_aml_decision_finished, 535 padh); 536 if (NULL == padh->job) 537 { 538 TALER_curl_easy_post_finished (&padh->post_ctx); 539 GNUNET_free (padh->url); 540 padh->url = NULL; 541 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 542 } 543 return TALER_EC_NONE; 544 } 545 546 547 void 548 TALER_EXCHANGE_post_aml_decision_cancel ( 549 struct TALER_EXCHANGE_PostAmlDecisionHandle *padh) 550 { 551 if (NULL != padh->job) 552 { 553 GNUNET_CURL_job_cancel (padh->job); 554 padh->job = NULL; 555 } 556 TALER_curl_easy_post_finished (&padh->post_ctx); 557 json_decref (padh->new_rules); 558 if (NULL != padh->properties) 559 json_decref (padh->properties); 560 if (NULL != padh->jevents) 561 json_decref (padh->jevents); 562 if (NULL != padh->attributes) 563 json_decref (padh->attributes); 564 GNUNET_free (padh->url); 565 GNUNET_free (padh->base_url); 566 GNUNET_free (padh->justification); 567 GNUNET_free (padh->payto_uri_str); 568 GNUNET_free (padh->new_measures); 569 GNUNET_free (padh); 570 } 571 572 573 /* end of exchange_api_post-aml-OFFICER_PUB-decision.c */