From 3194ccabc1fa0ed52d59167668a7f546dbdbf377 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Thu, 18 Aug 2022 15:39:28 +0200 Subject: untested draft of webhook logic for persona --- contrib/gana | 2 +- src/include/taler_kyclogic_lib.h | 31 ++ src/include/taler_kyclogic_plugin.h | 8 + src/kyclogic/Makefile.am | 4 +- src/kyclogic/kyclogic-persona.conf | 33 ++ src/kyclogic/kyclogic_api.c | 37 +++ src/kyclogic/plugin_kyclogic_kycaid.c | 15 + src/kyclogic/plugin_kyclogic_persona.c | 588 ++++++++++++++++++++++----------- 8 files changed, 521 insertions(+), 197 deletions(-) create mode 100644 src/kyclogic/kyclogic-persona.conf diff --git a/contrib/gana b/contrib/gana index ce901edba..2e264e285 160000 --- a/contrib/gana +++ b/contrib/gana @@ -1 +1 @@ -Subproject commit ce901edbaf496244f50f45b221d0c2c929c47637 +Subproject commit 2e264e2856ee1f490d894a64d36bd4eac71802eb diff --git a/src/include/taler_kyclogic_lib.h b/src/include/taler_kyclogic_lib.h index fedb26a99..df547c3db 100644 --- a/src/include/taler_kyclogic_lib.h +++ b/src/include/taler_kyclogic_lib.h @@ -237,6 +237,37 @@ TALER_KYCLOGIC_kyc_iterate_thresholds ( void *it_cls); +/** + * Function called with the provider details and + * associated plugin closures for matching logics. + * + * @param cls closure + * @param pd provider details of a matching logic + * @param plugin_cls closure of the plugin + * @return #GNUNET_OK to continue to iterate + */ +typedef enum GNUNET_GenericReturnValue +(*TALER_KYCLOGIC_DetailsCallback)( + void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + void *plugin_cls); + + +/** + * Call @a cb for all logics with name @a logic_name, + * providing the plugin closure and the @a pd configurations. + * + * @param logic_name name of the logic to match + * @param cb function to call on matching results + * @param cb_cls closure for @a cb + */ +void +TALER_KYCLOGIC_kyc_get_details ( + const char *logic_name, + TALER_KYCLOGIC_DetailsCallback cb, + void *cb_cls); + + /** * Obtain the provider logic for a given @a provider_section_name. * diff --git a/src/include/taler_kyclogic_plugin.h b/src/include/taler_kyclogic_plugin.h index 7c0ebbc43..a4c166abc 100644 --- a/src/include/taler_kyclogic_plugin.h +++ b/src/include/taler_kyclogic_plugin.h @@ -77,6 +77,8 @@ enum TALER_KYCLOGIC_KycStatus * The provider is still checking. */ TALER_KYCLOGIC_STATUS_PROVIDER_PENDING + + = TALER_KYCLOGIC_STATUS_PROVIDER | TALER_KYCLOGIC_STATUS_PENDING, @@ -240,6 +242,12 @@ struct TALER_KYCLOGIC_Plugin */ char *library_name; + /** + * Name of the logic, for webhook matching. Set by the + * plugin loader. + */ + char *name; + /** * Load the configuration of the KYC provider. * diff --git a/src/kyclogic/Makefile.am b/src/kyclogic/Makefile.am index 7b442bcf1..280eda338 100644 --- a/src/kyclogic/Makefile.am +++ b/src/kyclogic/Makefile.am @@ -11,7 +11,8 @@ pkgcfgdir = $(prefix)/share/taler/config.d/ pkgcfg_DATA = \ kyclogic.conf \ kyclogic-kycaid.conf \ - kyclogic-oauth2.conf + kyclogic-oauth2.conf \ + kyclogic-persona.conf EXTRA_DIST = \ kyclogic.conf \ @@ -97,6 +98,7 @@ libtaler_plugin_kyclogic_persona_la_LIBADD = \ $(LTLIBINTL) libtaler_plugin_kyclogic_persona_la_LDFLAGS = \ $(TALER_PLUGIN_LDFLAGS) \ + libtalerkyclogic.la \ $(top_builddir)/src/json/libtalerjson.la \ $(top_builddir)/src/curl/libtalercurl.la \ $(top_builddir)/src/util/libtalerutil.la \ diff --git a/src/kyclogic/kyclogic-persona.conf b/src/kyclogic/kyclogic-persona.conf new file mode 100644 index 000000000..de90238c7 --- /dev/null +++ b/src/kyclogic/kyclogic-persona.conf @@ -0,0 +1,33 @@ +# This file is in the public domain. + +# FIXME: add to taler.conf man page! + +# Example persona provider configuration. + +[kyclogic-persona] + +# Optional authorization token for the webhook +#WEBHOOK_AUTH_TOKEN = wbhsec_698b5a19-c790-47f6-b396-deb572ec82f9 + + +[kyc-provider-example-persona] + +COST = 42 +LOGIC = persona +USER_TYPE = INDIVIDUAL +PROVIDED_CHECKS = EXAMPLE_DO_NOT_USE + +# How long is the KYC check valid? +PERSONA_VALIDITY = forever + +# Which subdomain is used for our API? +PERSONA_SUBDOMAIN = taler + +# Authentication token to use. +PERSONA_AUTH_TOKEN = persona_sandbox_42 + +# Form to use. +PERSONA_TEMPLATE_ID = itempl_Uj6Xxxxx + +# Where do we redirect to after KYC finished successfully. +KYC_POST_URL = https://taler.net/ diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c index 18707a18f..303869804 100644 --- a/src/kyclogic/kyclogic_api.c +++ b/src/kyclogic/kyclogic_api.c @@ -294,6 +294,7 @@ load_logic (const struct GNUNET_CONFIGURATION_Handle *cfg, return NULL; } plugin->library_name = lib_name; + plugin->name = GNUNET_strdup (name); GNUNET_array_append (kyc_logics, num_kyc_logics, plugin); @@ -737,6 +738,7 @@ TALER_KYCLOGIC_kyc_done (void) struct TALER_KYCLOGIC_Plugin *lp = kyc_logics[i]; char *lib_name = lp->library_name; + GNUNET_free (lp->name); GNUNET_assert (NULL == GNUNET_PLUGIN_unload (lib_name, lp)); GNUNET_free (lib_name); @@ -1092,6 +1094,29 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, } +void +TALER_KYCLOGIC_kyc_get_details ( + const char *logic_name, + TALER_KYCLOGIC_DetailsCallback cb, + void *cb_cls) +{ + for (unsigned int i = 0; ilogic->name, + logic_name)) + continue; + if (GNUNET_OK != + cb (cb_cls, + kp->pd, + kp->logic->cls)) + return; + } +} + + enum GNUNET_GenericReturnValue TALER_KYCLOGIC_kyc_get_logic (const char *provider_section_name, struct TALER_KYCLOGIC_Plugin **plugin, @@ -1109,6 +1134,18 @@ TALER_KYCLOGIC_kyc_get_logic (const char *provider_section_name, *pd = kp->pd; return GNUNET_OK; } + for (unsigned int i = 0; iname, + provider_section_name)) + continue; + *plugin = logic; + *pd = NULL; + return GNUNET_OK; + } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Provider `%s' unknown\n", provider_section_name); diff --git a/src/kyclogic/plugin_kyclogic_kycaid.c b/src/kyclogic/plugin_kyclogic_kycaid.c index 8a5714e72..05bcb4f62 100644 --- a/src/kyclogic/plugin_kyclogic_kycaid.c +++ b/src/kyclogic/plugin_kyclogic_kycaid.c @@ -1081,6 +1081,21 @@ kycaid_webhook (void *cls, wh->pd = pd; wh->connection = connection; + if (NULL == pd) + { + GNUNET_break_op (0); + json_dumpf (body, + stderr, + JSON_INDENT (2)); + wh->resp = TALER_MHD_make_error ( + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, + "kycaid"); + wh->response_code = MHD_HTTP_NOT_FOUND; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + if (GNUNET_OK != GNUNET_JSON_parse (body, spec, diff --git a/src/kyclogic/plugin_kyclogic_persona.c b/src/kyclogic/plugin_kyclogic_persona.c index c8040f828..2ce8e6a1f 100644 --- a/src/kyclogic/plugin_kyclogic_persona.c +++ b/src/kyclogic/plugin_kyclogic_persona.c @@ -23,6 +23,7 @@ #include "taler_mhd_lib.h" #include "taler_curl_lib.h" #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_templating_lib.h" #include #include "taler_util.h" @@ -60,6 +61,13 @@ struct PluginState */ struct GNUNET_CURL_RescheduleContext *curl_rc; + /** + * Authorization token to use when receiving webhooks from the Persona service. Optional. Note that + * webhooks are *global* and not per template. + */ + char *webhook_token; + + }; @@ -233,7 +241,7 @@ struct TALER_KYCLOGIC_ProofHandle /** * Inquiry ID at the provider. */ - char *provider_legitimization_id; + char *inquiry_id; }; @@ -293,16 +301,21 @@ struct TALER_KYCLOGIC_WebhookHandle */ struct MHD_Response *resp; + /** + * ID of the template the webhook is about, + * according to the service. + */ + const char *template_id; + /** * Our account ID. */ struct TALER_PaytoHashP h_payto; /** - * Row in legitimizations for the given - * @e verification_id. + * UUID being checked. */ - uint64_t legi_row; + uint64_t legitimization_uuid; /** * HTTP response code to return asynchronously. @@ -807,7 +820,7 @@ persona_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph) } GNUNET_free (ph->url); GNUNET_free (ph->provider_user_id); - GNUNET_free (ph->provider_legitimization_id); + GNUNET_free (ph->inquiry_id); GNUNET_free (ph); } @@ -835,7 +848,12 @@ proof_generic_reply (struct TALER_KYCLOGIC_ProofHandle *ph, { struct MHD_Response *resp; enum GNUNET_GenericReturnValue ret; + struct GNUNET_TIME_Absolute expiration; + if (TALER_KYCLOGIC_STATUS_SUCCESS == status) + expiration = GNUNET_TIME_relative_to_absolute (ph->pd->validity); + else + expiration = GNUNET_TIME_UNIT_ZERO_ABS; ret = TALER_TEMPLATING_build (ph->connection, &http_status, template, @@ -852,7 +870,7 @@ proof_generic_reply (struct TALER_KYCLOGIC_ProofHandle *ph, status, account_id, inquiry_id, - GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + expiration, http_status, resp); } @@ -1033,7 +1051,7 @@ handle_proof_finished (void *cls, } if (0 != strcmp (inquiry_id, - ph->provider_legitimization_id)) + ph->inquiry_id)) { GNUNET_break_op (0); proof_reply_error (ph, @@ -1146,7 +1164,7 @@ handle_proof_finished (void *cls, stderr, JSON_INDENT (2)); proof_reply_error (ph, - ph->provider_legitimization_id, + ph->inquiry_id, MHD_HTTP_BAD_GATEWAY, "persona-logic-failure", GNUNET_JSON_PACK ( @@ -1166,7 +1184,7 @@ handle_proof_finished (void *cls, "Refused access with HTTP status code %u\n", (unsigned int) response_code); proof_reply_error (ph, - ph->provider_legitimization_id, + ph->inquiry_id, MHD_HTTP_INTERNAL_SERVER_ERROR, "persona-exchange-unauthorized", GNUNET_JSON_PACK ( @@ -1186,7 +1204,7 @@ handle_proof_finished (void *cls, (unsigned int) response_code); proof_reply_error (ph, - ph->provider_legitimization_id, + ph->inquiry_id, MHD_HTTP_INTERNAL_SERVER_ERROR, "persona-exchange-unpaid", GNUNET_JSON_PACK ( @@ -1208,7 +1226,7 @@ handle_proof_finished (void *cls, stderr, JSON_INDENT (2)); proof_reply_error (ph, - ph->provider_legitimization_id, + ph->inquiry_id, MHD_HTTP_GATEWAY_TIMEOUT, "persona-network-timeout", GNUNET_JSON_PACK ( @@ -1230,7 +1248,7 @@ handle_proof_finished (void *cls, stderr, JSON_INDENT (2)); proof_reply_error (ph, - ph->provider_legitimization_id, + ph->inquiry_id, MHD_HTTP_SERVICE_UNAVAILABLE, "persona-load-failure", GNUNET_JSON_PACK ( @@ -1252,7 +1270,7 @@ handle_proof_finished (void *cls, stderr, JSON_INDENT (2)); proof_reply_error (ph, - ph->provider_legitimization_id, + ph->inquiry_id, MHD_HTTP_BAD_GATEWAY, "persona-provider-failure", GNUNET_JSON_PACK ( @@ -1274,7 +1292,7 @@ handle_proof_finished (void *cls, stderr, JSON_INDENT (2)); proof_reply_error (ph, - ph->provider_legitimization_id, + ph->inquiry_id, MHD_HTTP_BAD_GATEWAY, "persona-invalid-response", GNUNET_JSON_PACK ( @@ -1304,7 +1322,7 @@ handle_proof_finished (void *cls, * @param account_id which account to trigger process for * @param legi_row row in the table the legitimization is for * @param provider_user_id user ID (or NULL) the proof is for - * @param provider_legitimization_id legitimization ID the proof is for + * @param inquiry_id legitimization ID the proof is for * @param cb function to call with the result * @param cb_cls closure for @a cb * @return handle to cancel operation early @@ -1317,7 +1335,7 @@ persona_proof (void *cls, const struct TALER_PaytoHashP *account_id, uint64_t legi_row, const char *provider_user_id, - const char *provider_legitimization_id, + const char *inquiry_id, TALER_KYCLOGIC_ProofCallback cb, void *cb_cls) { @@ -1339,15 +1357,14 @@ persona_proof (void *cls, ph->connection = connection; ph->legitimization_uuid = legi_row; ph->h_payto = *account_id; - /* NOTE: we do not expect this to be non-NULL */ + /* Note: we do not expect this to be non-NULL */ if (NULL != provider_user_id) ph->provider_user_id = GNUNET_strdup (provider_user_id); - /* This should be the inquiry ID; FIXME: rename variable? */ - if (NULL != provider_legitimization_id) - ph->provider_legitimization_id = GNUNET_strdup (provider_legitimization_id); + if (NULL != inquiry_id) + ph->inquiry_id = GNUNET_strdup (inquiry_id); GNUNET_asprintf (&ph->url, "https://withpersona.com/api/v1/inquiries/%s", - provider_legitimization_id); + inquiry_id); GNUNET_break (CURLE_OK == curl_easy_setopt (eh, CURLOPT_VERBOSE, @@ -1393,6 +1410,70 @@ persona_webhook_cancel (struct TALER_KYCLOGIC_WebhookHandle *wh) } +/** + * Call @a wh callback with the operation result. + * + * @param wh proof handle to generate reply for + * @param status status to return + * @param account_id account to return + * @param inquiry_id inquiry ID to supply + * @param http_status HTTP status to use + * @param template template to instantiate + * @param[in] body body for the template to use (reference + * is consumed) + */ +static void +webhook_generic_reply (struct TALER_KYCLOGIC_WebhookHandle *wh, + enum TALER_KYCLOGIC_KycStatus status, + const char *account_id, + const char *inquiry_id, + unsigned int http_status) +{ + struct MHD_Response *resp; + struct GNUNET_TIME_Absolute expiration; + + if (TALER_KYCLOGIC_STATUS_SUCCESS == status) + expiration = GNUNET_TIME_relative_to_absolute (wh->pd->validity); + else + expiration = GNUNET_TIME_UNIT_ZERO_ABS; + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + wh->cb (wh->cb_cls, + wh->legitimization_uuid, + &wh->h_payto, + account_id, + inquiry_id, + status, + expiration, + http_status, + resp); +} + + +/** + * Call @a wh callback with HTTP error response. + * + * @param wh proof handle to generate reply for + * @param inquiry_id inquiry ID to supply + * @param http_status HTTP status to use + * @param template template to instantiate + * @param[in] body body for the template to use (reference + * is consumed) + */ +static void +webhook_reply_error (struct TALER_KYCLOGIC_WebhookHandle *wh, + const char *inquiry_id, + unsigned int http_status) +{ + webhook_generic_reply (wh, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + NULL, /* user id */ + inquiry_id, + http_status); +} + + /** * Function called when we're done processing the * HTTP "/verifications/{verification_id}" request. @@ -1408,234 +1489,243 @@ handle_webhook_finished (void *cls, { struct TALER_KYCLOGIC_WebhookHandle *wh = cls; const json_t *j = response; + const json_t *data = json_object_get (j, + "data"); wh->job = NULL; json_dumpf (j, stderr, JSON_INDENT (2)); -#if 0 - struct MHD_Response *resp; - switch (response_code) { case MHD_HTTP_OK: { - const char *applicant_id; - const char *verification_id; - const char *status; - bool verified; - json_t *verifications; + const char *inquiry_id; + const char *account_id; + const char *type = NULL; + json_t *attributes; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("applicant_id", - &applicant_id), - GNUNET_JSON_spec_string ("verification_id", - &verification_id), - GNUNET_JSON_spec_string ("status", - &status), /* completed, pending, ... */ - GNUNET_JSON_spec_bool ("verified", - &verified), - GNUNET_JSON_spec_json ("verifications", - &verifications), + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_string ("id", + &inquiry_id), + GNUNET_JSON_spec_json ("attributes", + &attributes), GNUNET_JSON_spec_end () }; - struct GNUNET_TIME_Absolute expiration; - if (GNUNET_OK != - GNUNET_JSON_parse (j, - spec, - NULL, NULL)) + if ( (NULL == data) || + (GNUNET_OK != + GNUNET_JSON_parse (data, + spec, + NULL, NULL)) || + (0 != strcmp (type, + "inquiry")) ) { GNUNET_break_op (0); json_dumpf (j, stderr, JSON_INDENT (2)); - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_object_incref ("persona_body", - (json_t *) j)); - wh->cb (wh->cb_cls, - wh->legi_row, - &wh->h_payto, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, - GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ - MHD_HTTP_BAD_GATEWAY, - resp); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_BAD_GATEWAY); break; } - if (! verified) - { - log_failure (verifications); - } - resp = MHD_create_response_from_buffer (0, - "", - MHD_RESPMEM_PERSISTENT); - if (verified) - { - expiration = GNUNET_TIME_relative_to_absolute (wh->pd->validity); - wh->cb (wh->cb_cls, - wh->legi_row, - &wh->h_payto, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_SUCCESS, - expiration, - MHD_HTTP_NO_CONTENT, - resp); - } - else + { - wh->cb (wh->cb_cls, - wh->legi_row, - &wh->h_payto, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_USER_ABORTED, - GNUNET_TIME_UNIT_ZERO_ABS, - MHD_HTTP_NO_CONTENT, - resp); + const char *status; /* "completed", what else? */ + const char *reference_id; /* or legitimization number */ + const char *expired_at = NULL; /* often 'null' format: "2022-08-18T10:14:26.000Z" */ + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("status", + &status), + GNUNET_JSON_spec_string ("reference_id", + &reference_id), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("expired_at", + &expired_at), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (attributes, + ispec, + NULL, NULL)) + { + GNUNET_break_op (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_BAD_GATEWAY); + GNUNET_JSON_parse_free (ispec); + GNUNET_JSON_parse_free (spec); + break; + } + { + unsigned long long idr; + char dummy; + + if ( (1 != sscanf (reference_id, + "%llu%c", + &idr, + &dummy)) || + (idr != wh->legitimization_uuid) ) + { + GNUNET_break_op (0); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_BAD_GATEWAY); + GNUNET_JSON_parse_free (ispec); + GNUNET_JSON_parse_free (spec); + break; + } + } + + if (0 != strcmp (inquiry_id, + wh->inquiry_id)) + { + GNUNET_break_op (0); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_BAD_GATEWAY); + GNUNET_JSON_parse_free (ispec); + GNUNET_JSON_parse_free (spec); + break; + } + + account_id = json_string_value ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + data, + "relationships"), + "account"), + "data"), + "id")); + + if (0 != strcmp (status, + "completed")) + { + webhook_generic_reply (wh, + TALER_KYCLOGIC_STATUS_FAILED, + account_id, + inquiry_id, + MHD_HTTP_OK); + GNUNET_JSON_parse_free (ispec); + GNUNET_JSON_parse_free (spec); + break; + } + + if (NULL == account_id) + { + GNUNET_break_op (0); + json_dumpf (data, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_BAD_GATEWAY); + break; + } + + webhook_generic_reply (wh, + TALER_KYCLOGIC_STATUS_SUCCESS, + account_id, + inquiry_id, + MHD_HTTP_OK); + GNUNET_JSON_parse_free (ispec); } GNUNET_JSON_parse_free (spec); + break; } - break; case MHD_HTTP_BAD_REQUEST: case MHD_HTTP_NOT_FOUND: case MHD_HTTP_CONFLICT: + case MHD_HTTP_UNPROCESSABLE_ENTITY: + /* These are errors with this code */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "PERSONA failed with response %u:\n", (unsigned int) response_code); json_dumpf (j, stderr, JSON_INDENT (2)); - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code)); - wh->cb (wh->cb_cls, - wh->legi_row, - &wh->h_payto, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, - GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ - MHD_HTTP_INTERNAL_SERVER_ERROR, - resp); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_BAD_GATEWAY); break; case MHD_HTTP_UNAUTHORIZED: + /* These are failures of the exchange operator */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refused access with HTTP status code %u\n", + (unsigned int) response_code); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_INTERNAL_SERVER_ERROR); + break; case MHD_HTTP_PAYMENT_REQUIRED: + /* These are failures of the exchange operator */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Refused access with HTTP status code %u\n", (unsigned int) response_code); - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_object_incref ("persona_body", - (json_t *) j)); - wh->cb (wh->cb_cls, - wh->legi_row, - &wh->h_payto, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, - GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ - MHD_HTTP_NETWORK_AUTHENTICATION_REQUIRED, - resp); + + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_INTERNAL_SERVER_ERROR); break; case MHD_HTTP_REQUEST_TIMEOUT: - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_object_incref ("persona_body", - (json_t *) j)); - wh->cb (wh->cb_cls, - wh->legi_row, - &wh->h_payto, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, - GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ - MHD_HTTP_GATEWAY_TIMEOUT, - resp); - break; - case MHD_HTTP_UNPROCESSABLE_ENTITY: /* validation */ + /* These are networking issues */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "PERSONA failed with response %u:\n", (unsigned int) response_code); json_dumpf (j, stderr, JSON_INDENT (2)); - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_object_incref ("persona_body", - (json_t *) j)); - wh->cb (wh->cb_cls, - wh->legi_row, - &wh->h_payto, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, - GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ - MHD_HTTP_BAD_GATEWAY, - resp); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_GATEWAY_TIMEOUT); break; case MHD_HTTP_TOO_MANY_REQUESTS: - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_object_incref ("persona_body", - (json_t *) j)); - wh->cb (wh->cb_cls, - wh->legi_row, - &wh->h_payto, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, - GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ - MHD_HTTP_SERVICE_UNAVAILABLE, - resp); + /* This is a load issue */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_SERVICE_UNAVAILABLE); break; case MHD_HTTP_INTERNAL_SERVER_ERROR: - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_object_incref ("persona_body", - (json_t *) j)); - wh->cb (wh->cb_cls, - wh->legi_row, - &wh->h_payto, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, - GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ - MHD_HTTP_BAD_GATEWAY, - resp); + /* This is an issue with Persona */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_BAD_GATEWAY); break; default: - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_object_incref ("persona_body", - (json_t *) j)); + /* This is an issue with Persona */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected PERSONA response %u:\n", + "PERSONA failed with response %u:\n", (unsigned int) response_code); json_dumpf (j, stderr, JSON_INDENT (2)); - wh->cb (wh->cb_cls, - wh->legi_row, - &wh->h_payto, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, - GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ - MHD_HTTP_BAD_GATEWAY, - resp); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_BAD_GATEWAY); break; } -#endif + persona_webhook_cancel (wh); } @@ -1651,8 +1741,8 @@ async_webhook_reply (void *cls) struct TALER_KYCLOGIC_WebhookHandle *wh = cls; wh->cb (wh->cb_cls, - wh->legi_row, - (0 == wh->legi_row) + wh->legitimization_uuid, + (0 == wh->legitimization_uuid) ? NULL : &wh->h_payto, NULL, /* FIXME: never known here, but maybe prevent clearing it in the DB as it should already be there? */ @@ -1665,6 +1755,35 @@ async_webhook_reply (void *cls) } +/** + * Function called with the provider details and + * associated plugin closures for matching logics. + * + * @param cls closure + * @param pd provider details of a matching logic + * @param plugin_cls closure of the plugin + * @return #GNUNET_OK to continue to iterate + */ +static enum GNUNET_GenericReturnValue +locate_details_cb ( + void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + void *plugin_cls) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + + /* This type-checks 'pd' */ + GNUNET_assert (plugin_cls == wh->ps); + if (0 == strcmp (pd->template_id, + wh->template_id)) + { + wh->pd = pd; + return GNUNET_NO; + } + return GNUNET_OK; +} + + /** * Check KYC status and return result for Webhook. We do NOT implement the * authentication check proposed by the PERSONA documentation, as it would @@ -1701,14 +1820,79 @@ persona_webhook (void *cls, CURL *eh; enum GNUNET_DB_QueryStatus qs; const char *persona_inquiry_id; + const char *auth_header; - // FIXME: check webhook 'Authorization' header first! + /* Persona webhooks are expected by logic, not by template */ + GNUNET_break_op (NULL == pd); wh = GNUNET_new (struct TALER_KYCLOGIC_WebhookHandle); wh->cb = cb; wh->cb_cls = cb_cls; wh->ps = ps; - wh->pd = pd; wh->connection = connection; + wh->pd = pd; + + auth_header = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_AUTHORIZATION); + if ( (NULL != ps->webhook_token) && + (0 != strcmp (ps->webhook_token, + auth_header)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid authorization header `%s' received for Persona webhook\n", + auth_header); + wh->resp = TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_WEBHOOK_UNAUTHORIZED), + GNUNET_JSON_pack_string ("detail", + "unexpected 'Authorization' header")); + wh->response_code = MHD_HTTP_UNAUTHORIZED; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + + wh->template_id + = json_string_value ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + body, + "data"), + "attributes"), + "payload"), + "data"), + "relationships"), + "template"), + "data"), + "id")); + TALER_KYCLOGIC_kyc_get_details ("persona", + &locate_details_cb, + wh); + if (NULL == wh->pd) + { + GNUNET_break_op (0); + json_dumpf (body, + stderr, + JSON_INDENT (2)); + wh->resp = TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN), + GNUNET_JSON_pack_string ("detail", + wh->template_id), + GNUNET_JSON_pack_object_incref ("webhook_body", + (json_t *) body)); + wh->response_code = MHD_HTTP_BAD_REQUEST; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + persona_inquiry_id = json_string_value ( @@ -1730,6 +1914,10 @@ persona_webhook (void *cls, stderr, JSON_INDENT (2)); wh->resp = TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), + GNUNET_JSON_pack_string ("detail", + "data-attributes-payload-data-id"), GNUNET_JSON_pack_object_incref ("webhook_body", (json_t *) body)); wh->response_code = MHD_HTTP_BAD_REQUEST; @@ -1741,7 +1929,7 @@ persona_webhook (void *cls, pd->section, persona_inquiry_id, &wh->h_payto, - &wh->legi_row); + &wh->legitimization_uuid); if (qs < 0) { wh->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, @@ -1754,7 +1942,7 @@ persona_webhook (void *cls, if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Received webhook for unknown verification ID `%s'\n", + "Received Persona kyc-webhook for unknown verification ID `%s'\n", persona_inquiry_id); wh->resp = TALER_MHD_make_error ( TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, @@ -1830,6 +2018,15 @@ libtaler_plugin_kyclogic_persona_init (void *cls) GNUNET_free (ps); return NULL; } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + "kyclogic-persona", + "WEBHOOK_AUTH_TOKEN", + &ps->webhook_token)) + { + /* optional */ + ps->webhook_token = NULL; + } ps->curl_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, @@ -1888,6 +2085,7 @@ libtaler_plugin_kyclogic_persona_done (void *cls) ps->curl_rc = NULL; } GNUNET_free (ps->exchange_base_url); + GNUNET_free (ps->webhook_token); GNUNET_free (ps); GNUNET_free (plugin); return NULL; -- cgit v1.2.3