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