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 :
134 :
135 : /**
136 : * Function called when we're done processing the
137 : * HTTP POST /aml/$OFFICER_PUB/decision request.
138 : *
139 : * @param cls the `struct TALER_EXCHANGE_PostAmlDecisionHandle *`
140 : * @param response_code HTTP response code, 0 on error
141 : * @param response response body, NULL if not in JSON
142 : */
143 : static void
144 3 : handle_post_aml_decision_finished (void *cls,
145 : long response_code,
146 : const void *response)
147 : {
148 3 : struct TALER_EXCHANGE_PostAmlDecisionHandle *padh = cls;
149 3 : const json_t *json = response;
150 3 : struct TALER_EXCHANGE_PostAmlDecisionResponse pr = {
151 3 : .hr.http_status = (unsigned int) response_code,
152 : .hr.reply = json
153 : };
154 :
155 3 : padh->job = NULL;
156 3 : switch (response_code)
157 : {
158 0 : case 0:
159 0 : pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
160 0 : pr.hr.hint = "server offline?";
161 0 : break;
162 2 : case MHD_HTTP_NO_CONTENT:
163 2 : break;
164 1 : case MHD_HTTP_FORBIDDEN:
165 1 : pr.hr.ec = TALER_JSON_get_error_code (json);
166 1 : pr.hr.hint = TALER_JSON_get_error_hint (json);
167 1 : break;
168 0 : case MHD_HTTP_CONFLICT:
169 0 : pr.hr.ec = TALER_JSON_get_error_code (json);
170 0 : pr.hr.hint = TALER_JSON_get_error_hint (json);
171 0 : break;
172 0 : default:
173 0 : GNUNET_break_op (0);
174 0 : pr.hr.ec = TALER_JSON_get_error_code (json);
175 0 : pr.hr.hint = TALER_JSON_get_error_hint (json);
176 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
177 : "Unexpected response code %u/%d for POST AML decision\n",
178 : (unsigned int) response_code,
179 : (int) pr.hr.ec);
180 0 : break;
181 : }
182 3 : if (NULL != padh->cb)
183 : {
184 3 : padh->cb (padh->cb_cls,
185 : &pr);
186 3 : padh->cb = NULL;
187 : }
188 3 : TALER_EXCHANGE_post_aml_decision_cancel (padh);
189 3 : }
190 :
191 :
192 : /**
193 : * Build the new_rules JSON object from rules and measures arrays.
194 : *
195 : * @param successor_measure optional successor measure name
196 : * @param expiration_time when the new rules expire
197 : * @param num_rules length of @a rules
198 : * @param rules the rules array
199 : * @param num_measures length of @a measures
200 : * @param measures the measures array
201 : * @return new JSON object (caller owns reference), NULL on error
202 : */
203 : static json_t *
204 3 : build_new_rules (
205 : const char *successor_measure,
206 : struct GNUNET_TIME_Timestamp expiration_time,
207 : unsigned int num_rules,
208 : const struct TALER_EXCHANGE_AccountRule rules[static num_rules],
209 : unsigned int num_measures,
210 : const struct TALER_EXCHANGE_MeasureInformation measures[static num_measures])
211 3 : {
212 : json_t *jrules;
213 : json_t *jmeasures;
214 :
215 3 : jrules = json_array ();
216 3 : GNUNET_assert (NULL != jrules);
217 6 : for (unsigned int i = 0; i < num_rules; i++)
218 : {
219 3 : const struct TALER_EXCHANGE_AccountRule *al = &rules[i];
220 : json_t *ameasures;
221 : json_t *rule;
222 :
223 3 : ameasures = json_array ();
224 3 : GNUNET_assert (NULL != ameasures);
225 4 : for (unsigned int j = 0; j < al->num_measures; j++)
226 1 : GNUNET_assert (0 ==
227 : json_array_append_new (ameasures,
228 : json_string (al->measures[j])));
229 3 : rule = GNUNET_JSON_PACK (
230 : TALER_JSON_pack_kycte ("operation_type",
231 : al->operation_type),
232 : TALER_JSON_pack_amount ("threshold",
233 : &al->threshold),
234 : GNUNET_JSON_pack_time_rel ("timeframe",
235 : al->timeframe),
236 : GNUNET_JSON_pack_array_steal ("measures",
237 : ameasures),
238 : GNUNET_JSON_pack_bool ("exposed",
239 : al->exposed),
240 : GNUNET_JSON_pack_bool ("is_and_combinator",
241 : al->is_and_combinator),
242 : GNUNET_JSON_pack_uint64 ("display_priority",
243 : al->display_priority)
244 : );
245 3 : GNUNET_break (0 ==
246 : json_array_append_new (jrules,
247 : rule));
248 : }
249 :
250 3 : jmeasures = json_object ();
251 3 : GNUNET_assert (NULL != jmeasures);
252 4 : for (unsigned int i = 0; i < num_measures; i++)
253 : {
254 1 : const struct TALER_EXCHANGE_MeasureInformation *mi = &measures[i];
255 : json_t *measure;
256 :
257 1 : measure = GNUNET_JSON_PACK (
258 : GNUNET_JSON_pack_string ("check_name",
259 : mi->check_name),
260 : GNUNET_JSON_pack_allow_null (
261 : GNUNET_JSON_pack_string ("prog_name",
262 : mi->prog_name)),
263 : GNUNET_JSON_pack_allow_null (
264 : GNUNET_JSON_pack_object_incref ("context",
265 : (json_t *) mi->context))
266 : );
267 1 : GNUNET_break (0 ==
268 : json_object_set_new (jmeasures,
269 : mi->measure_name,
270 : measure));
271 : }
272 :
273 3 : return GNUNET_JSON_PACK (
274 : GNUNET_JSON_pack_timestamp ("expiration_time",
275 : expiration_time),
276 : GNUNET_JSON_pack_allow_null (
277 : GNUNET_JSON_pack_string ("successor_measure",
278 : successor_measure)),
279 : GNUNET_JSON_pack_array_steal ("rules",
280 : jrules),
281 : GNUNET_JSON_pack_object_steal ("custom_measures",
282 : jmeasures)
283 : );
284 : }
285 :
286 :
287 : struct TALER_EXCHANGE_PostAmlDecisionHandle *
288 3 : TALER_EXCHANGE_post_aml_decision_create (
289 : struct GNUNET_CURL_Context *ctx,
290 : const char *url,
291 : const struct TALER_NormalizedPaytoHashP *h_payto,
292 : struct GNUNET_TIME_Timestamp decision_time,
293 : const char *successor_measure,
294 : struct GNUNET_TIME_Timestamp expiration_time,
295 : unsigned int num_rules,
296 : const struct TALER_EXCHANGE_AccountRule rules[static num_rules],
297 : unsigned int num_measures,
298 : const struct TALER_EXCHANGE_MeasureInformation measures[static num_measures],
299 : bool keep_investigating,
300 : const char *justification,
301 : const struct TALER_AmlOfficerPrivateKeyP *officer_priv)
302 3 : {
303 : struct TALER_EXCHANGE_PostAmlDecisionHandle *padh;
304 : json_t *new_rules;
305 :
306 3 : new_rules = build_new_rules (successor_measure,
307 : expiration_time,
308 : num_rules,
309 : rules,
310 : num_measures,
311 : measures);
312 3 : if (NULL == new_rules)
313 : {
314 0 : GNUNET_break (0);
315 0 : return NULL;
316 : }
317 :
318 3 : padh = GNUNET_new (struct TALER_EXCHANGE_PostAmlDecisionHandle);
319 3 : padh->ctx = ctx;
320 3 : padh->base_url = GNUNET_strdup (url);
321 3 : padh->h_payto = *h_payto;
322 3 : padh->decision_time = decision_time;
323 3 : padh->justification = GNUNET_strdup (justification);
324 3 : padh->keep_investigating = keep_investigating;
325 3 : padh->new_rules = new_rules;
326 3 : padh->officer_priv = *officer_priv;
327 3 : GNUNET_CRYPTO_eddsa_key_get_public (&officer_priv->eddsa_priv,
328 : &padh->officer_pub.eddsa_pub);
329 3 : return padh;
330 : }
331 :
332 :
333 : enum GNUNET_GenericReturnValue
334 4 : TALER_EXCHANGE_post_aml_decision_set_options_ (
335 : struct TALER_EXCHANGE_PostAmlDecisionHandle *padh,
336 : unsigned int num_options,
337 : const struct TALER_EXCHANGE_PostAmlDecisionOptionValue options[])
338 : {
339 8 : for (unsigned int i = 0; i < num_options; i++)
340 : {
341 8 : const struct TALER_EXCHANGE_PostAmlDecisionOptionValue *opt = &options[i];
342 :
343 8 : switch (opt->option)
344 : {
345 4 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_END:
346 4 : return GNUNET_OK;
347 0 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_PAYTO_URI:
348 0 : GNUNET_free (padh->payto_uri_str);
349 0 : padh->payto_uri_str =
350 0 : (NULL != opt->details.payto_uri.full_payto)
351 0 : ? GNUNET_strdup (opt->details.payto_uri.full_payto)
352 0 : : NULL;
353 0 : break;
354 1 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_NEW_MEASURES:
355 1 : GNUNET_free (padh->new_measures);
356 1 : padh->new_measures =
357 1 : (NULL != opt->details.new_measures)
358 1 : ? GNUNET_strdup (opt->details.new_measures)
359 1 : : NULL;
360 1 : break;
361 3 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_PROPERTIES:
362 3 : if (NULL != padh->properties)
363 0 : json_decref (padh->properties);
364 3 : padh->properties =
365 3 : (NULL != opt->details.properties)
366 3 : ? json_incref ((json_t *) opt->details.properties)
367 3 : : NULL;
368 3 : break;
369 0 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_EVENTS:
370 : {
371 0 : if (NULL != padh->jevents)
372 0 : json_decref (padh->jevents);
373 0 : if (0 == opt->details.events.num_events)
374 : {
375 0 : padh->jevents = NULL;
376 : }
377 : else
378 : {
379 0 : padh->jevents = json_array ();
380 0 : GNUNET_assert (NULL != padh->jevents);
381 0 : for (unsigned int j = 0; j < opt->details.events.num_events; j++)
382 0 : GNUNET_assert (0 ==
383 : json_array_append_new (
384 : padh->jevents,
385 : json_string (opt->details.events.events[j])));
386 : }
387 : }
388 0 : break;
389 0 : default:
390 0 : GNUNET_break (0);
391 0 : return GNUNET_SYSERR;
392 : }
393 : }
394 0 : return GNUNET_OK;
395 : }
396 :
397 :
398 : enum TALER_ErrorCode
399 3 : TALER_EXCHANGE_post_aml_decision_start (
400 : struct TALER_EXCHANGE_PostAmlDecisionHandle *padh,
401 : TALER_EXCHANGE_PostAmlDecisionCallback cb,
402 : TALER_EXCHANGE_POST_AML_DECISION_RESULT_CLOSURE *cb_cls)
403 : {
404 : CURL *eh;
405 : struct TALER_AmlOfficerSignatureP officer_sig;
406 : json_t *body;
407 : char *path;
408 : char opus[sizeof (padh->officer_pub) * 2];
409 : char *end;
410 3 : struct TALER_FullPayto payto_uri_val = {
411 3 : .full_payto = padh->payto_uri_str
412 : };
413 :
414 3 : padh->cb = cb;
415 3 : padh->cb_cls = cb_cls;
416 3 : TALER_officer_aml_decision_sign (padh->justification,
417 : padh->decision_time,
418 3 : &padh->h_payto,
419 3 : padh->new_rules,
420 3 : padh->properties,
421 3 : padh->new_measures,
422 3 : padh->keep_investigating,
423 3 : &padh->officer_priv,
424 : &officer_sig);
425 :
426 3 : end = GNUNET_STRINGS_data_to_string (
427 3 : &padh->officer_pub,
428 : sizeof (padh->officer_pub),
429 : opus,
430 : sizeof (opus));
431 3 : *end = '\0';
432 3 : GNUNET_asprintf (&path,
433 : "aml/%s/decision",
434 : opus);
435 3 : padh->url = TALER_url_join (padh->base_url,
436 : path,
437 : NULL);
438 3 : GNUNET_free (path);
439 3 : if (NULL == padh->url)
440 : {
441 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
442 : "Could not construct request URL.\n");
443 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
444 : }
445 :
446 3 : body = GNUNET_JSON_PACK (
447 : GNUNET_JSON_pack_string ("justification",
448 : padh->justification),
449 : GNUNET_JSON_pack_data_auto ("h_payto",
450 : &padh->h_payto),
451 : GNUNET_JSON_pack_allow_null (
452 : TALER_JSON_pack_full_payto ("payto_uri",
453 : payto_uri_val)),
454 : GNUNET_JSON_pack_object_incref ("new_rules",
455 : padh->new_rules),
456 : GNUNET_JSON_pack_allow_null (
457 : GNUNET_JSON_pack_object_incref ("properties",
458 : padh->properties)),
459 : GNUNET_JSON_pack_allow_null (
460 : GNUNET_JSON_pack_string ("new_measures",
461 : padh->new_measures)),
462 : GNUNET_JSON_pack_bool ("keep_investigating",
463 : padh->keep_investigating),
464 : GNUNET_JSON_pack_data_auto ("officer_sig",
465 : &officer_sig),
466 : GNUNET_JSON_pack_timestamp ("decision_time",
467 : padh->decision_time),
468 : GNUNET_JSON_pack_allow_null (
469 : GNUNET_JSON_pack_array_incref ("events",
470 : padh->jevents))
471 : );
472 :
473 3 : eh = TALER_EXCHANGE_curl_easy_get_ (padh->url);
474 6 : if ( (NULL == eh) ||
475 : (GNUNET_OK !=
476 3 : TALER_curl_easy_post (&padh->post_ctx,
477 : eh,
478 : body)) )
479 : {
480 0 : GNUNET_break (0);
481 0 : if (NULL != eh)
482 0 : curl_easy_cleanup (eh);
483 0 : json_decref (body);
484 0 : GNUNET_free (padh->url);
485 0 : padh->url = NULL;
486 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
487 : }
488 3 : json_decref (body);
489 3 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
490 : "Requesting URL '%s'\n",
491 : padh->url);
492 6 : padh->job = GNUNET_CURL_job_add2 (padh->ctx,
493 : eh,
494 3 : padh->post_ctx.headers,
495 : &handle_post_aml_decision_finished,
496 : padh);
497 3 : if (NULL == padh->job)
498 : {
499 0 : TALER_curl_easy_post_finished (&padh->post_ctx);
500 0 : GNUNET_free (padh->url);
501 0 : padh->url = NULL;
502 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
503 : }
504 3 : return TALER_EC_NONE;
505 : }
506 :
507 :
508 : void
509 3 : TALER_EXCHANGE_post_aml_decision_cancel (
510 : struct TALER_EXCHANGE_PostAmlDecisionHandle *padh)
511 : {
512 3 : if (NULL != padh->job)
513 : {
514 0 : GNUNET_CURL_job_cancel (padh->job);
515 0 : padh->job = NULL;
516 : }
517 3 : TALER_curl_easy_post_finished (&padh->post_ctx);
518 3 : json_decref (padh->new_rules);
519 3 : if (NULL != padh->properties)
520 3 : json_decref (padh->properties);
521 3 : if (NULL != padh->jevents)
522 0 : json_decref (padh->jevents);
523 3 : GNUNET_free (padh->url);
524 3 : GNUNET_free (padh->base_url);
525 3 : GNUNET_free (padh->justification);
526 3 : GNUNET_free (padh->payto_uri_str);
527 3 : GNUNET_free (padh->new_measures);
528 3 : GNUNET_free (padh);
529 3 : }
530 :
531 :
532 : /* end of exchange_api_post-aml-OFFICER_PUB-decision.c */
|