merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 551f2990953a273d4b58db00772aa812b7061f52
parent bc6141b504f280e2f70fb9f89db2789034d23f65
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Sun, 23 Mar 2025 19:40:36 +0100

Make all (non-harness) tests pass now.
Adds a variety of access token related APIs and testing commands.

Diffstat:
Msrc/backend/taler-merchant-httpd.c | 6++++++
Msrc/include/taler_merchant_service.h | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler_merchant_testing_lib.h | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/Makefile.am | 2++
Asrc/lib/merchant_api_delete_instance_token.c | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/merchant_api_post_instance_auth.c | 11-----------
Asrc/lib/merchant_api_post_instance_token.c | 235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/Makefile.am | 1+
Msrc/testing/test_merchant_api.c | 42+++++++++++++++++++++++++++++++++---------
Asrc/testing/testing_api_cmd_instance_token.c | 398+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 1008 insertions(+), 20 deletions(-)

diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -1931,6 +1931,9 @@ url_handler (void *cls, } else if (is_basic_auth) { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Got basic auth %s\n", + auth); /* Handle token endpoint slightly differently: Only allow * instance password (Basic auth) OR * refresh token (Bearer auth) to retrieve access token. @@ -1966,6 +1969,9 @@ url_handler (void *cls, } else /* Check bearer token */ { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Got bearer auth %s\n", + auth); if (NULL != hc->instance) { if (GNUNET_is_zero (&hc->instance->auth.auth_hash)) diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -733,6 +733,110 @@ void TALER_MERCHANT_instance_auth_post_cancel ( struct TALER_MERCHANT_InstanceAuthPostHandle *iaph); +/** + * Handle for an operation to get access token. + */ +struct TALER_MERCHANT_InstanceTokenPostHande; + + +/** + * Function called with the result of the GET /instances/$ID/private/token + * operation. + * + * @param cls closure + * @param hr HTTP response data + */ +typedef void +(*TALER_MERCHANT_InstanceTokenPostCallback)( + void *cls, + const struct TALER_MERCHANT_HttpResponse *hr); + +/** + * Get access token for an existing instance in the backend. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend (top-level "default" instance + * or base URL of an instance if @a instance_id is NULL) + * @param instance_id identity of the instance to patch the authentication for; NULL + * if the instance is identified as part of the @a backend_url + * @param scope authorization scope for token needed to access the instance, can be NULL + * @param duration requested authorization duration + * @param refreshable requesting a refreshable token or not + * @param cb function to call with the backend's response + * @param cb_cls closure for @a config_cb + * @return the instances handle; NULL upon error + */ +struct TALER_MERCHANT_InstanceTokenPostHandle * +TALER_MERCHANT_instance_token_post ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *instance_id, + const char *scope, + struct GNUNET_TIME_Relative duration, + bool refreshable, + TALER_MERCHANT_InstanceTokenPostCallback cb, + void *cb_cls); + + +/** + * Cancel /private/token request. Must not be called by clients after + * the callback was invoked. Afterwards, the authentication may or + * may not have been updated. + * + * @param iaph request to cancel. + */ +void +TALER_MERCHANT_instance_token_post_cancel ( + struct TALER_MERCHANT_InstanceTokenPostHandle *itph); + +/** + * Handle for a DELETE /instances/$ID/private/token operation. + */ +struct TALER_MERCHANT_InstanceTokenDeleteHandle; + + +/** + * Function called with the result of the DELETE /instances/$ID/private/token operation. + * + * @param cls closure + * @param par response data + */ +typedef void +(*TALER_MERCHANT_InstanceTokenDeleteCallback)( + void *cls, + const struct TALER_MERCHANT_HttpResponse *hr); + + +/** + * Remove token instance in the backend. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend + * @param instance_id identity of the instance to patch the authentication for; NULL + * if the instance is identified as part of the @a backend_url + * @param cb function to call with the response + * @param cb_cls closure for @a config_cb + * @return the instances handle; NULL upon error + */ +struct TALER_MERCHANT_InstanceTokenDeleteHandle * +TALER_MERCHANT_instance_token_delete ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *instance_id, + TALER_MERCHANT_InstanceTokenDeleteCallback cb, + void *cb_cls); + + +/** + * Cancel DELETE token request. Must not be called by clients after + * the callback was invoked. + * + * @param pah request to cancel. + */ +void +TALER_MERCHANT_instance_token_delete_cancel ( + struct TALER_MERCHANT_InstanceTokenDeleteHandle *tdh); + /** * Handle for a GET /instances/$ID operation. diff --git a/src/include/taler_merchant_testing_lib.h b/src/include/taler_merchant_testing_lib.h @@ -122,6 +122,56 @@ TALER_TESTING_cmd_merchant_post_instance_auth (const char *label, /** + * Define a "POST /private/token" CMD. + * + * @param label command label. + * @param merchant_url base URL of the merchant serving the + * POST /instances request. + * @param instance_id the ID of the instance, or NULL + * @param scope scope to request, can be NULL + * @param duration requested token validity duration + * @param refreshable should token be refreshable + * @param http_status expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_instance_token (const char *label, + const char *merchant_url, + const char *instance_id, + const char *scope, + struct GNUNET_TIME_Relative + duration, + bool refreshable, + unsigned int http_status); + +/** + * Set the access token gotten through another CMD + * + * @param label command label. + * @param other_cmd the other command exposing the bearer_token trait. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_set_instance_token (const char *label, + const char *cmd_job_label); + +/** + * Define a "DELETE /private/token" CMD. + * + * @param label command label. + * @param merchant_url base URL of the merchant serving the + * POST /instances request. + * @param instance_id the ID of the instance, or NULL + * @param http_status expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_delete_instance_token (const char *label, + const char *merchant_url, + const char *instance_id, + unsigned int http_status); + +/** * Define a "POST /instances" CMD. Comprehensive version. * * @param label command label. @@ -1850,6 +1900,7 @@ TALER_TESTING_cmd_merchant_get_statisticsamount (const char *label, op (reason, const char) \ op (lock_uuid, const char) \ op (auth_token, const char) \ + op (bearer_token, const char) \ op (paths_length, const uint32_t) \ op (payto_length, const uint32_t) \ op (num_planchets, const uint32_t) \ diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am @@ -18,6 +18,7 @@ libtalermerchant_la_SOURCES = \ merchant_api_common.c merchant_api_common.h \ merchant_api_delete_account.c \ merchant_api_delete_instance.c \ + merchant_api_delete_instance_token.c \ merchant_api_delete_order.c \ merchant_api_delete_otp_device.c \ merchant_api_delete_product.c \ @@ -52,6 +53,7 @@ libtalermerchant_la_SOURCES = \ merchant_api_patch_webhook.c \ merchant_api_post_account.c \ merchant_api_post_instance_auth.c \ + merchant_api_post_instance_token.c \ merchant_api_post_instances.c \ merchant_api_post_orders.c \ merchant_api_post_order_abort.c \ diff --git a/src/lib/merchant_api_delete_instance_token.c b/src/lib/merchant_api_delete_instance_token.c @@ -0,0 +1,178 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 2.1, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with + TALER; see the file COPYING.LGPL. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant_api_delete_instance_token.c + * @brief Implementation of the DELETE /instance/$ID/private/token request of the merchant's HTTP API + * @author Martin Schanzenbach + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler_merchant_service.h" +#include "merchant_api_curl_defaults.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_signatures.h> + + +/** + * Handle for a DELETE /instance/$ID/private/token operation. + */ +struct TALER_MERCHANT_InstanceTokenDeleteHandle +{ + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_InstanceTokenDeleteCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + +}; + + +/** + * Function called when we're done processing the + * HTTP DELETE /instance/$ID/private/token request. + * + * @param cls the `struct TALER_MERCHANT_TokenDeleteHandle` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_delete_token_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_MERCHANT_InstanceTokenDeleteHandle *tdh = cls; + const json_t *json = response; + struct TALER_MERCHANT_HttpResponse tdr = { + .http_status = (unsigned int) response_code, + .reply = json + }; + + tdh->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Got /instances/$ID/private/token response with status code %u\n", + (unsigned int) response_code); + switch (response_code) + { + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_UNAUTHORIZED: + tdr.ec = TALER_JSON_get_error_code (json); + tdr.hint = TALER_JSON_get_error_hint (json); + /* Nothing really to verify, merchant says we need to authenticate. */ + break; + case MHD_HTTP_NOT_FOUND: + break; + default: + /* unexpected response code */ + tdr.ec = TALER_JSON_get_error_code (json); + tdr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for DELETE /instance/$ID/private/token\n", + (unsigned int) response_code, + (int) tdr.ec); + break; + } + tdh->cb (tdh->cb_cls, + &tdr); + TALER_MERCHANT_instance_token_delete_cancel (tdh); +} + + +struct TALER_MERCHANT_InstanceTokenDeleteHandle * +TALER_MERCHANT_instance_token_delete ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *instance_id, + TALER_MERCHANT_InstanceTokenDeleteCallback cb, + void *cb_cls) +{ + struct TALER_MERCHANT_InstanceTokenDeleteHandle *tdh; + + tdh = GNUNET_new (struct TALER_MERCHANT_InstanceTokenDeleteHandle); + tdh->ctx = ctx; + tdh->cb = cb; + tdh->cb_cls = cb_cls; + { + char *path; + + GNUNET_asprintf (&path, + "instances/%s/private/token", + instance_id); + tdh->url = TALER_url_join (backend_url, + path, + NULL); + GNUNET_free (path); + } + if (NULL == tdh->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (tdh); + return NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + tdh->url); + { + CURL *eh; + + eh = TALER_MERCHANT_curl_easy_get_ (tdh->url); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_CUSTOMREQUEST, + MHD_HTTP_METHOD_DELETE)); + tdh->job = GNUNET_CURL_job_add (ctx, + eh, + &handle_delete_token_finished, + tdh); + } + return tdh; +} + + +void +TALER_MERCHANT_instance_token_delete_cancel ( + struct TALER_MERCHANT_InstanceTokenDeleteHandle *tdh) +{ + if (NULL != tdh->job) + GNUNET_CURL_job_cancel (tdh->job); + GNUNET_free (tdh->url); + GNUNET_free (tdh); +} diff --git a/src/lib/merchant_api_post_instance_auth.c b/src/lib/merchant_api_post_instance_auth.c @@ -175,17 +175,6 @@ TALER_MERCHANT_instance_auth_post ( } else { - if (0 != strncasecmp (RFC_8959_PREFIX, - auth_token, - strlen (RFC_8959_PREFIX))) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Authentication token must start with `%s'\n", - RFC_8959_PREFIX); - GNUNET_free (iaph->url); - GNUNET_free (iaph); - return NULL; - } req_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("method", "token"), diff --git a/src/lib/merchant_api_post_instance_token.c b/src/lib/merchant_api_post_instance_token.c @@ -0,0 +1,235 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 2.1, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with + TALER; see the file COPYING.LGPL. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant_api_post_instance_token.c + * @brief Implementation of the POST /instance/$ID/private/token request + * @author Martin Schanzenbach + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler_merchant_service.h" +#include "merchant_api_curl_defaults.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_signatures.h> +#include <taler/taler_curl_lib.h> + + +/** + * Handle for a POST /instances/$ID/private/token operation. + */ +struct TALER_MERCHANT_InstanceTokenPostHandle +{ + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_InstanceTokenPostCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Minor context that holds body and headers. + */ + struct TALER_CURL_PostContext post_ctx; + +}; + + +/** + * Function called when we're done processing the + * HTTP GET /instances/$ID/private/token request. + * + * @param cls the `struct TALER_MERCHANT_InstanceTokenPostHandle` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_post_instance_token_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_MERCHANT_InstanceTokenPostHandle *itph = cls; + const json_t *json = response; + struct TALER_MERCHANT_HttpResponse hr = { + .http_status = (unsigned int) response_code, + .reply = json + }; + + itph->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Got /instances/$ID response with status code %u\n", + (unsigned int) response_code); + switch (response_code) + { + case MHD_HTTP_OK: + break; + case MHD_HTTP_BAD_REQUEST: + /* happens if the auth token is malformed */ + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + /* Nothing really to verify, merchant says we need to authenticate. */ + break; + case MHD_HTTP_UNAUTHORIZED: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + /* Nothing really to verify, merchant says we need to authenticate. */ + break; + default: + /* unexpected response code */ + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d\n", + (unsigned int) response_code, + (int) hr.ec); + break; + } + itph->cb (itph->cb_cls, + &hr); + TALER_MERCHANT_instance_token_post_cancel (itph); +} + + +struct TALER_MERCHANT_InstanceTokenPostHandle * +TALER_MERCHANT_instance_token_post ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *instance_id, + const char *scope, + struct GNUNET_TIME_Relative duration, + bool refreshable, + TALER_MERCHANT_InstanceTokenPostCallback cb, + void *cb_cls) +{ + struct TALER_MERCHANT_InstanceTokenPostHandle *itph; + json_t *req_obj; + + itph = GNUNET_new (struct TALER_MERCHANT_InstanceTokenPostHandle); + itph->ctx = ctx; + itph->cb = cb; + itph->cb_cls = cb_cls; + if (NULL != instance_id) + { + char *path; + + GNUNET_asprintf (&path, + "instances/%s/private/token", + instance_id); + itph->url = TALER_url_join (backend_url, + path, + NULL); + GNUNET_free (path); + } + else + { + /* backend_url is already identifying the instance */ + itph->url = TALER_url_join (backend_url, + "private/token", + NULL); + } + if (NULL == itph->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (itph); + return NULL; + } + if (NULL == scope) + { + req_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_time_rel ("duration", + duration), + GNUNET_JSON_pack_bool ("refreshable", + refreshable), + GNUNET_JSON_pack_string ("scope", + "readonly")); + } + else + { + req_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_time_rel ("duration", + duration), + GNUNET_JSON_pack_bool ("refreshable", + refreshable), + GNUNET_JSON_pack_string ("scope", + scope)); + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + itph->url); + { + CURL *eh; + + eh = TALER_MERCHANT_curl_easy_get_ (itph->url); + if (GNUNET_OK != + TALER_curl_easy_post (&itph->post_ctx, + eh, + req_obj)) + { + GNUNET_break (0); + curl_easy_cleanup (eh); + json_decref (req_obj); + GNUNET_free (itph->url); + GNUNET_free (itph); + return NULL; + } + json_decref (req_obj); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_CUSTOMREQUEST, + MHD_HTTP_METHOD_POST)); + itph->job = GNUNET_CURL_job_add2 (ctx, + eh, + itph->post_ctx.headers, + &handle_post_instance_token_finished, + itph); + } + return itph; +} + + +void +TALER_MERCHANT_instance_token_post_cancel ( + struct TALER_MERCHANT_InstanceTokenPostHandle *itph) +{ + if (NULL != itph->job) + GNUNET_CURL_job_cancel (itph->job); + TALER_curl_easy_post_finished (&itph->post_ctx); + GNUNET_free (itph->url); + GNUNET_free (itph); +} diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am @@ -58,6 +58,7 @@ libtalermerchanttesting_la_SOURCES = \ testing_api_cmd_kyc_get.c \ testing_api_cmd_lock_product.c \ testing_api_cmd_instance_auth.c \ + testing_api_cmd_instance_token.c \ testing_api_cmd_merchant_get_order.c \ testing_api_cmd_patch_instance.c \ testing_api_cmd_patch_otp_device.c \ diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -1000,7 +1000,7 @@ run (void *cls, "instance-create-i1a-auth-ok", merchant_url, "i1a", - RFC_8959_PREFIX "my-secret", + "my-secret", MHD_HTTP_NO_CONTENT), TALER_TESTING_cmd_merchant_get_product ("get-nx-product-i1a-3", merchant_url_i1a, @@ -1012,7 +1012,20 @@ run (void *cls, MHD_HTTP_UNAUTHORIZED, NULL), TALER_TESTING_cmd_set_authorization ("set-auth-valid", - "Bearer " RFC_8959_PREFIX "my-secret"), + "Basic aTFhOm15LXNlY3JldA=="), + TALER_TESTING_cmd_merchant_post_instance_token ( + "instance-create-i1a-token-ok", + merchant_url, + "i1a", + "write", /* scope */ + GNUNET_TIME_UNIT_DAYS, /* duration */ + GNUNET_YES, /* refreshable */ + MHD_HTTP_OK), + TALER_TESTING_cmd_set_authorization ("unset-auth-valid", + NULL), // Unset header + TALER_TESTING_cmd_merchant_set_instance_token ( + "instance-create-i1a-token-set", + "instance-create-i1a-token-ok"), TALER_TESTING_cmd_merchant_get_product ("get-nx-product-i1a-4", merchant_url_i1a, "nx-product", @@ -1022,11 +1035,10 @@ run (void *cls, merchant_url_i1a, MHD_HTTP_OK, NULL), - TALER_TESTING_cmd_merchant_post_instance_auth ( - "instance-create-i1a-change-auth", - merchant_url_i1a, - NULL, - RFC_8959_PREFIX "my-other-secret", + TALER_TESTING_cmd_merchant_delete_instance_token ( + "instance-create-i1a-token-delete", + merchant_url, + "i1a", MHD_HTTP_NO_CONTENT), TALER_TESTING_cmd_merchant_get_product ("get-nx-product-i1a-5", merchant_url_i1a, @@ -1041,9 +1053,21 @@ run (void *cls, merchant_url_i1a, NULL, MHD_HTTP_UNAUTHORIZED), - TALER_TESTING_cmd_set_authorization ( + TALER_TESTING_cmd_set_authorization ("set-token-auth-valid-again", + "Basic aTFhOm15LXNlY3JldA=="), + TALER_TESTING_cmd_merchant_post_instance_token ( "set-auth-valid-again", - "Bearer " RFC_8959_PREFIX "my-other-secret"), + merchant_url, + "i1a", + "write", /* scope */ + GNUNET_TIME_UNIT_DAYS, /* duration */ + GNUNET_YES, /* refreshable */ + MHD_HTTP_OK), + TALER_TESTING_cmd_set_authorization ("unset-auth-valid2", + NULL), // Unset header + TALER_TESTING_cmd_merchant_set_instance_token ( + "instance-create-i1a-token-set-again", + "set-auth-valid-again"), TALER_TESTING_cmd_merchant_post_instance_auth ( "instance-create-i1a-auth-ok-idempotent", merchant_url_i1a, diff --git a/src/testing/testing_api_cmd_instance_token.c b/src/testing/testing_api_cmd_instance_token.c @@ -0,0 +1,398 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing_api_cmd_instance_token.c + * @brief command to test /private/token POSTing + * @author Martin Schanzenbach + */ +#include "platform.h" +#include <taler/taler_exchange_service.h> +#include <taler/taler_testing_lib.h> +#include "taler_merchant_service.h" +#include "taler_merchant_testing_lib.h" + + +/** + * State of a "POST /instances/$ID/private/token" CMD. + */ +struct TokenInstanceState +{ + + /** + * Handle for a "POST token" request. + */ + struct TALER_MERCHANT_InstanceTokenPostHandle *itph; + + /** + * Handle for a "DELETE token" request. + */ + struct TALER_MERCHANT_InstanceTokenDeleteHandle *itdh; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Base URL of the merchant serving the request. + */ + const char *merchant_url; + + /** + * ID of the instance to run GET for. + */ + const char *instance_id; + + /** + * The received token (if any). + */ + char *token; + + /** + * Desired scope. Can be NULL + */ + const char *scope; + + /** + * Desired duration. + */ + struct GNUNET_TIME_Relative duration; + + /** + * Refreshable? + */ + bool refreshable; + + /** + * Expected HTTP response code. + */ + unsigned int http_status; + + /** + * DELETE or POST. + */ + unsigned int is_delete; + +}; + +/** + * Callback for a POST /instances/$ID/private/token operation. + * + * @param cls closure for this function + * @param hr response being processed + */ +static void +token_instance_cb (void *cls, + const struct TALER_MERCHANT_HttpResponse *hr) +{ + struct TokenInstanceState *tis = cls; + const char *scope; + struct GNUNET_TIME_Timestamp duration; + bool refreshable; + const char *error_name; + unsigned int error_line; + + + tis->itph = NULL; + if (tis->http_status != hr->http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u (%d) to command %s\n", + hr->http_status, + (int) hr->ec, + TALER_TESTING_interpreter_get_current_label (tis->is)); + TALER_TESTING_interpreter_fail (tis->is); + return; + } + switch (hr->http_status) + { + case MHD_HTTP_NO_CONTENT: + GNUNET_assert (GNUNET_YES == tis->is_delete); + break; + case MHD_HTTP_OK: + GNUNET_assert (GNUNET_NO == tis->is_delete); + /* Get token */ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string_copy ("token", + &tis->token), + GNUNET_JSON_spec_string ("scope", + &scope), + GNUNET_JSON_spec_bool ("refreshable", + &refreshable), + GNUNET_JSON_spec_timestamp ("expiration", + &duration), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (hr->reply, + spec, + &error_name, + &error_line)) + { + char *js; + + js = json_dumps (hr->reply, + JSON_INDENT (1)); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Parser failed on %s:%u for input `%s'\n", + error_name, + error_line, + js); + free (js); + TALER_TESTING_FAIL (tis->is); + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Got token: `%s'\n", tis->token); + break; + case MHD_HTTP_BAD_REQUEST: + /* likely invalid auth value, we do not check client-side */ + break; + case MHD_HTTP_FORBIDDEN: + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unhandled HTTP status %u (%d) returned from /private/token operation.\n", + hr->http_status, + hr->ec); + } + + + TALER_TESTING_interpreter_next (tis->is); +} + + +/** + * set a token + * + * + * @param cls closure. + * @param cmd command being run now. + * @param is interpreter state. + */ +static void +set_token_instance_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + const char *token_job_label = cls; + const char *token; + const struct TALER_TESTING_Command *tok_cmd; + struct GNUNET_CURL_Context *cctx; + char *authorization; + + cctx = TALER_TESTING_interpreter_get_context (is); + GNUNET_assert (NULL != cctx); + tok_cmd = TALER_TESTING_interpreter_lookup_command ( + is, + token_job_label); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Getting token from: `%s'\n", token_job_label); + TALER_TESTING_get_trait_bearer_token (tok_cmd, + &token); + GNUNET_assert (NULL != token); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Got: `%s'\n", token); + + GNUNET_asprintf (&authorization, + "%s: Bearer %s", + MHD_HTTP_HEADER_AUTHORIZATION, + token); + GNUNET_assert (GNUNET_OK == + GNUNET_CURL_append_header (cctx, + authorization)); + GNUNET_free (authorization); + TALER_TESTING_interpreter_next (is); +} + + +/** + * Run the "token /instances/$ID" CMD. + * + * + * @param cls closure. + * @param cmd command being run now. + * @param is interpreter state. + */ +static void +token_instance_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct TokenInstanceState *tis = cls; + + tis->is = is; + if (GNUNET_NO == tis->is_delete) + tis->itph = TALER_MERCHANT_instance_token_post ( + TALER_TESTING_interpreter_get_context (is), + tis->merchant_url, + tis->instance_id, + tis->scope, + tis->duration, + tis->refreshable, + &token_instance_cb, + tis); + else + tis->itdh = TALER_MERCHANT_instance_token_delete ( + TALER_TESTING_interpreter_get_context (is), + tis->merchant_url, + tis->instance_id, + &token_instance_cb, + tis); + GNUNET_assert ((NULL != tis->itph) || (NULL != tis->itdh)); +} + + +/** + * Free the state of a "POST instance token" CMD, and possibly + * cancel a pending operation thereof. + * + * @param cls closure. + * @param cmd command being run. + */ +static void +token_instance_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct TokenInstanceState *tis = cls; + + if (NULL != tis->itph) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "%s /instance/$ID/token operation did not complete\n", + (GNUNET_NO == tis->is_delete) ? "DELETE" : "POST"); + if (GNUNET_NO == tis->is_delete) + TALER_MERCHANT_instance_token_post_cancel (tis->itph); + else + TALER_MERCHANT_instance_token_delete_cancel (tis->itdh); + } + GNUNET_free (tis); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param[out] ret result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +token_instance_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct TokenInstanceState *ais = cls; + struct TALER_TESTING_Trait traits[] = { + TALER_TESTING_make_trait_bearer_token (ais->token), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_delete_instance_token (const char *label, + const char *merchant_url, + const char *instance_id, + unsigned int http_status) +{ + struct TokenInstanceState *tis; + + tis = GNUNET_new (struct TokenInstanceState); + tis->merchant_url = merchant_url; + tis->instance_id = instance_id; + tis->is_delete = GNUNET_YES; + tis->http_status = http_status; + + { + struct TALER_TESTING_Command cmd = { + .cls = tis, + .label = label, + .run = &token_instance_run, + .cleanup = &token_instance_cleanup, + .traits = &token_instance_traits + }; + + return cmd; + } +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_set_instance_token (const char *label, + const char *token_job_label) +{ + { + struct TALER_TESTING_Command cmd = { + .cls = (void*) token_job_label, // FIXME scope + .label = label, + .run = &set_token_instance_run, + .cleanup = NULL, + .traits = NULL + }; + + return cmd; + } +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_instance_token (const char *label, + const char *merchant_url, + const char *instance_id, + const char *scope, + struct GNUNET_TIME_Relative + duration, + bool refreshable, + unsigned int http_status) +{ + struct TokenInstanceState *tis; + + tis = GNUNET_new (struct TokenInstanceState); + tis->merchant_url = merchant_url; + tis->instance_id = instance_id; + tis->scope = scope; + tis->duration = duration; + tis->refreshable = refreshable; + tis->is_delete = GNUNET_NO; + tis->http_status = http_status; + + { + struct TALER_TESTING_Command cmd = { + .cls = tis, + .label = label, + .run = &token_instance_run, + .cleanup = &token_instance_cleanup, + .traits = &token_instance_traits + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_token_instance.c */