Line data Source code
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/do_insert_account.h"
34 : #include "merchant-database/iterate_accounts_by_instance.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 7 : account_cb (
106 : void *cls,
107 : const struct TALER_MerchantPrivateKeyP *merchant_priv,
108 : const struct TALER_MERCHANTDB_AccountDetails *ad)
109 : {
110 7 : struct PostAccountContext *pac = cls;
111 :
112 7 : if (! ad->active)
113 0 : return;
114 7 : pac->have_any_account = true;
115 7 : if ( (0 == TALER_full_payto_cmp (pac->uri,
116 1 : ad->payto_uri) ) &&
117 1 : ( (pac->credit_facade_credentials ==
118 1 : ad->credit_facade_credentials) ||
119 0 : ( (NULL != pac->credit_facade_credentials) &&
120 0 : (NULL != ad->credit_facade_credentials) &&
121 0 : (1 == json_equal (pac->credit_facade_credentials,
122 0 : ad->credit_facade_credentials)) ) ) &&
123 1 : ( (pac->extra_wire_subject_metadata ==
124 1 : ad->extra_wire_subject_metadata) ||
125 0 : ( (NULL != pac->extra_wire_subject_metadata) &&
126 0 : (NULL != ad->extra_wire_subject_metadata) &&
127 0 : (0 == strcmp (pac->extra_wire_subject_metadata,
128 0 : ad->extra_wire_subject_metadata)) ) ) &&
129 1 : ( (pac->credit_facade_url == ad->credit_facade_url) ||
130 0 : ( (NULL != pac->credit_facade_url) &&
131 0 : (NULL != ad->credit_facade_url) &&
132 0 : (0 == strcmp (pac->credit_facade_url,
133 0 : ad->credit_facade_url)) ) ) )
134 : {
135 1 : pac->have_same_account = true;
136 1 : pac->salt = ad->salt;
137 1 : pac->h_wire = ad->h_wire;
138 1 : return;
139 : }
140 :
141 6 : if (0 == TALER_full_payto_normalize_and_cmp (pac->uri,
142 : ad->payto_uri) )
143 : {
144 1 : pac->have_conflicting_account = true;
145 1 : return;
146 : }
147 : }
148 :
149 :
150 : enum MHD_Result
151 30 : TMH_private_post_account (const struct TMH_RequestHandler *rh,
152 : struct MHD_Connection *connection,
153 : struct TMH_HandlerContext *hc)
154 : {
155 30 : struct TMH_MerchantInstance *mi = hc->instance;
156 30 : struct PostAccountContext pac = { 0 };
157 : struct GNUNET_JSON_Specification ispec[] = {
158 30 : TALER_JSON_spec_full_payto_uri ("payto_uri",
159 : &pac.uri),
160 30 : GNUNET_JSON_spec_mark_optional (
161 : TALER_JSON_spec_web_url ("credit_facade_url",
162 : &pac.credit_facade_url),
163 : NULL),
164 30 : GNUNET_JSON_spec_mark_optional (
165 : GNUNET_JSON_spec_string ("extra_wire_subject_metadata",
166 : &pac.extra_wire_subject_metadata),
167 : NULL),
168 30 : GNUNET_JSON_spec_mark_optional (
169 : GNUNET_JSON_spec_object_const ("credit_facade_credentials",
170 : &pac.credit_facade_credentials),
171 : NULL),
172 30 : GNUNET_JSON_spec_end ()
173 : };
174 :
175 : {
176 : enum GNUNET_GenericReturnValue res;
177 :
178 30 : res = TALER_MHD_parse_json_data (connection,
179 30 : hc->request_body,
180 : ispec);
181 30 : if (GNUNET_OK != res)
182 : return (GNUNET_NO == res)
183 : ? MHD_YES
184 0 : : MHD_NO;
185 : }
186 :
187 : {
188 : char *err;
189 :
190 30 : if (NULL !=
191 30 : (err = TALER_payto_validate (pac.uri)))
192 : {
193 : enum MHD_Result mret;
194 :
195 0 : GNUNET_break_op (0);
196 0 : mret = TALER_MHD_reply_with_error (
197 : connection,
198 : MHD_HTTP_BAD_REQUEST,
199 : TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
200 : err);
201 0 : GNUNET_free (err);
202 0 : return mret;
203 : }
204 : }
205 30 : if (! TALER_is_valid_subject_metadata_string (
206 : pac.extra_wire_subject_metadata))
207 : {
208 0 : GNUNET_break_op (0);
209 0 : 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 30 : char *apt = GNUNET_strdup (TMH_allowed_payment_targets);
218 30 : char *method = TALER_payto_get_method (pac.uri.full_payto);
219 : bool ok;
220 :
221 30 : ok = false;
222 30 : for (const char *tok = strtok (apt,
223 : " ");
224 30 : NULL != tok;
225 0 : tok = strtok (NULL,
226 : " "))
227 : {
228 30 : if (0 == strcmp ("*",
229 : tok))
230 30 : ok = true;
231 30 : if (0 == strcmp (method,
232 : tok))
233 0 : ok = true;
234 30 : if (ok)
235 30 : break;
236 : }
237 30 : GNUNET_free (method);
238 30 : GNUNET_free (apt);
239 30 : if (! ok)
240 : {
241 0 : GNUNET_break_op (0);
242 0 : return TALER_MHD_reply_with_error (
243 : connection,
244 : MHD_HTTP_BAD_REQUEST,
245 : TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
246 : "The payment target type is forbidden by policy");
247 : }
248 : }
249 :
250 30 : if ( (NULL != TMH_payment_target_regex) &&
251 : (0 !=
252 0 : regexec (&TMH_payment_target_re,
253 0 : pac.uri.full_payto,
254 : 0,
255 : NULL,
256 : 0)) )
257 : {
258 0 : GNUNET_break_op (0);
259 0 : return TALER_MHD_reply_with_error (
260 : connection,
261 : MHD_HTTP_BAD_REQUEST,
262 : TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
263 : "The specific account is forbidden by policy");
264 : }
265 :
266 30 : if ( (NULL == pac.credit_facade_url) !=
267 30 : (NULL == pac.credit_facade_credentials) )
268 : {
269 0 : GNUNET_break_op (0);
270 0 : return TALER_MHD_reply_with_error (
271 : connection,
272 : MHD_HTTP_BAD_REQUEST,
273 : TALER_EC_GENERIC_PARAMETER_MISSING,
274 0 : (NULL == pac.credit_facade_url)
275 : ? "credit_facade_url"
276 : : "credit_facade_credentials");
277 : }
278 30 : if ( (NULL != pac.credit_facade_url) ||
279 28 : (NULL != pac.credit_facade_credentials) )
280 : {
281 : struct TALER_MERCHANT_BANK_AuthenticationData auth;
282 :
283 2 : if (GNUNET_OK !=
284 2 : TALER_MERCHANT_BANK_auth_parse_json (pac.credit_facade_credentials,
285 : pac.credit_facade_url,
286 : &auth))
287 : {
288 0 : GNUNET_break_op (0);
289 0 : return TALER_MHD_reply_with_error (
290 : connection,
291 : MHD_HTTP_BAD_REQUEST,
292 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
293 : "credit_facade_url or credit_facade_credentials");
294 : }
295 2 : TALER_MERCHANT_BANK_auth_free (&auth);
296 : }
297 :
298 : {
299 : enum GNUNET_DB_QueryStatus qs;
300 :
301 30 : TALER_MERCHANTDB_preflight (TMH_db);
302 30 : qs = TALER_MERCHANTDB_iterate_accounts_by_instance (
303 : TMH_db,
304 30 : mi->settings.id,
305 : &account_cb,
306 : &pac);
307 30 : switch (qs)
308 : {
309 0 : case GNUNET_DB_STATUS_HARD_ERROR:
310 0 : GNUNET_break (0);
311 0 : return TALER_MHD_reply_with_error (
312 : connection,
313 : MHD_HTTP_INTERNAL_SERVER_ERROR,
314 : TALER_EC_GENERIC_DB_FETCH_FAILED,
315 : "iterate_accounts");
316 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
317 0 : GNUNET_break (0);
318 0 : return TALER_MHD_reply_with_error (
319 : connection,
320 : MHD_HTTP_INTERNAL_SERVER_ERROR,
321 : TALER_EC_GENERIC_DB_FETCH_FAILED,
322 : "iterate_accounts");
323 23 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
324 23 : break;
325 7 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
326 7 : break;
327 : }
328 :
329 30 : if (pac.have_same_account)
330 : {
331 : /* Idempotent request */
332 1 : return TALER_MHD_REPLY_JSON_PACK (
333 : connection,
334 : MHD_HTTP_OK,
335 : GNUNET_JSON_pack_data_auto (
336 : "salt",
337 : &pac.salt),
338 : GNUNET_JSON_pack_data_auto (
339 : "h_wire",
340 : &pac.h_wire));
341 : }
342 :
343 : #if 1
344 29 : if (pac.have_conflicting_account)
345 : {
346 : /* Conflict, refuse request */
347 1 : GNUNET_break_op (0);
348 1 : return TALER_MHD_reply_with_error (
349 : connection,
350 : MHD_HTTP_CONFLICT,
351 : TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
352 1 : pac.uri.full_payto);
353 : }
354 : #endif
355 :
356 28 : if (pac.have_any_account)
357 : {
358 : /* MFA needed */
359 : enum GNUNET_GenericReturnValue ret;
360 :
361 5 : ret = TMH_mfa_check_simple (
362 : hc,
363 : TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
364 : mi);
365 5 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
366 : "Account creation MFA check returned %d\n",
367 : (int) ret);
368 5 : if (GNUNET_OK != ret)
369 : {
370 : return (GNUNET_NO == ret)
371 : ? MHD_YES
372 1 : : MHD_NO;
373 : }
374 : }
375 : }
376 :
377 : /* All pre-checks clear, now try to activate/setup the new account */
378 : {
379 : struct TMH_WireMethod *wm;
380 :
381 : /* convert provided payto URI into internal data structure with salts */
382 27 : wm = TMH_setup_wire_account (pac.uri,
383 : pac.credit_facade_url,
384 : pac.credit_facade_credentials);
385 27 : GNUNET_assert (NULL != wm);
386 27 : if (NULL != pac.extra_wire_subject_metadata)
387 : wm->extra_wire_subject_metadata
388 0 : = GNUNET_strdup (pac.extra_wire_subject_metadata);
389 : {
390 27 : struct TALER_MERCHANTDB_AccountDetails ad = {
391 : .payto_uri = wm->payto_uri,
392 : .salt = wm->wire_salt,
393 27 : .instance_id = mi->settings.id,
394 : .h_wire = wm->h_wire,
395 27 : .credit_facade_url = wm->credit_facade_url,
396 27 : .credit_facade_credentials = wm->credit_facade_credentials,
397 27 : .extra_wire_subject_metadata = wm->extra_wire_subject_metadata,
398 : .active = true
399 : };
400 : enum GNUNET_DB_QueryStatus qs;
401 : struct TALER_MerchantWireHashP h_wire;
402 : struct TALER_WireSaltP salt;
403 : bool not_found;
404 : bool conflict;
405 :
406 27 : TALER_MERCHANTDB_preflight (TMH_db);
407 27 : qs = TALER_MERCHANTDB_do_insert_account (TMH_db,
408 : &ad,
409 : &h_wire,
410 : &salt,
411 : ¬_found,
412 : &conflict);
413 27 : switch (qs)
414 : {
415 27 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
416 27 : break;
417 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
418 0 : GNUNET_break (0);
419 0 : TMH_wire_method_free (wm);
420 0 : return TALER_MHD_reply_with_error (
421 : connection,
422 : MHD_HTTP_INTERNAL_SERVER_ERROR,
423 : TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
424 : "do_insert_account");
425 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
426 0 : GNUNET_break (0);
427 0 : TMH_wire_method_free (wm);
428 0 : return TALER_MHD_reply_with_error (
429 : connection,
430 : MHD_HTTP_INTERNAL_SERVER_ERROR,
431 : TALER_EC_GENERIC_DB_STORE_FAILED,
432 : "do_insert_account");
433 0 : case GNUNET_DB_STATUS_HARD_ERROR:
434 0 : GNUNET_break (0);
435 0 : TMH_wire_method_free (wm);
436 0 : return TALER_MHD_reply_with_error (
437 : connection,
438 : MHD_HTTP_INTERNAL_SERVER_ERROR,
439 : TALER_EC_GENERIC_DB_STORE_FAILED,
440 : "do_insert_account");
441 : }
442 27 : if (not_found)
443 : {
444 : /* must have been concurrently deleted, rare! */
445 0 : TMH_wire_method_free (wm);
446 0 : return TALER_MHD_reply_with_error (
447 : connection,
448 : MHD_HTTP_NOT_FOUND,
449 : TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
450 0 : mi->settings.id);
451 : }
452 27 : if (conflict)
453 : {
454 : /* Conflicting POST must have been done between our pre-check
455 : and the actual transaction, rare! */
456 0 : TMH_wire_method_free (wm);
457 0 : GNUNET_break_op (0);
458 0 : return TALER_MHD_reply_with_error (
459 : connection,
460 : MHD_HTTP_CONFLICT,
461 : TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
462 0 : pac.uri.full_payto);
463 : }
464 :
465 : /* Update salt/h_wire in case we re-activated an
466 : existing account and found a different salt
467 : value already in the DB */
468 27 : wm->wire_salt = salt;
469 27 : wm->h_wire = h_wire;
470 :
471 : /* Finally, also update our running process */
472 27 : GNUNET_CONTAINER_DLL_insert (mi->wm_head,
473 : mi->wm_tail,
474 : wm);
475 : /* Note: we may not need to do this, as we notified
476 : about the account change above. But also hardly hurts. */
477 27 : TMH_reload_instances (mi->settings.id);
478 : }
479 27 : return TALER_MHD_REPLY_JSON_PACK (
480 : connection,
481 : MHD_HTTP_OK,
482 : GNUNET_JSON_pack_data_auto ("salt",
483 : &wm->wire_salt),
484 : GNUNET_JSON_pack_data_auto ("h_wire",
485 : &wm->h_wire));
486 : }
487 : }
488 :
489 :
490 : /* end of taler-merchant-httpd_post-private-accounts.c */
|