donau

Donation authority for GNU Taler (experimental)
Log | Files | Refs | Submodules | README | LICENSE

commit 3a6001e7d781da004fe23c52f678b8ff620804c1
parent fdc5aa79c200e8417b554f0b8ec58d0f40ed9456
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Sun, 28 Sep 2025 00:17:16 +0200

#0010461 adding patch charity/$id

Diffstat:
Msrc/donau/Makefile.am | 1+
Msrc/donau/donau-httpd.c | 33+++++++++++++++++++++++++--------
Msrc/donau/donau-httpd.h | 13+++++++++++++
Msrc/donau/donau-httpd_charity.h | 15+++++++++++++++
Asrc/donau/donau-httpd_charity_patch.c | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/donaudb/Makefile.am | 1+
Asrc/donaudb/pg_update_charity.c | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/donaudb/pg_update_charity.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Msrc/donaudb/plugin_donaudb_postgres.c | 19+++++++++++--------
Msrc/donaudb/test_donaudb.c | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/include/donau_testing_lib.h | 45+++++++++++++++++++++++++++++++++------------
Msrc/include/donaudb_plugin.h | 77+++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/lib/Makefile.am | 1+
Asrc/lib/donau_api_charity_patch.c | 273+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/Makefile.am | 2+-
Msrc/testing/test_donau_api.c | 14++++++++++++--
Asrc/testing/testing_api_cmd_charity_patch.c | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
17 files changed, 1029 insertions(+), 67 deletions(-)

diff --git a/src/donau/Makefile.am b/src/donau/Makefile.am @@ -45,6 +45,7 @@ donau_httpd_SOURCES = \ donau-httpd_charities_get.c donau_httpd_charity.h \ donau-httpd_charity_delete.c \ donau-httpd_charity_get.c donau-httpd_charity_insert.c \ + donau-httpd_charity_patch.c \ donau-httpd_history_get.c \ donau-httpd_donation-statement.c donau-httpd_donation-statement.h \ donau-httpd_batch-submit.c donau_httpd_batch-submit.h \ diff --git a/src/donau/donau-httpd.c b/src/donau/donau-httpd.c @@ -74,7 +74,7 @@ * Above what request latency do we start to log? */ #define WARN_LATENCY GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_MILLISECONDS, 500) + GNUNET_TIME_UNIT_MILLISECONDS, 500) /** * Are clients allowed to request /keys for times other than the @@ -316,10 +316,14 @@ proceed_with_handler (struct DH_RequestContext *rc, url); } - /* All POST endpoints come with a body in JSON format. So we parse + /* All POST and PATCH endpoints come with a body in JSON format. So we parse the JSON here. */ - if (0 == strcasecmp (rh->method, - MHD_HTTP_METHOD_POST)) + const bool is_post = (0 == strcasecmp (rh->method, + MHD_HTTP_METHOD_POST)); + const bool is_patch = (0 == strcasecmp (rh->method, + MHD_HTTP_METHOD_PATCH)); + + if (is_post || is_patch) { enum GNUNET_GenericReturnValue res; @@ -383,11 +387,14 @@ proceed_with_handler (struct DH_RequestContext *rc, /* Above logic ensures that 'root' is exactly non-NULL for POST operations, so we test for 'root' to decide which handler to invoke. */ - if (0 == strcasecmp (rh->method, - MHD_HTTP_METHOD_POST)) + if (is_post) ret = rh->handler.post (rc, root, args); + else if (is_patch) + ret = rh->handler.patch (rc, + root, + args); else if (0 == strcasecmp (rh->method, MHD_HTTP_METHOD_DELETE)) ret = rh->handler.delete (rc, @@ -511,6 +518,14 @@ handle_mhd_request (void *cls, .handler.post = &DH_handler_charity_post, .needs_authorization = true }, + /* PATCH charities */ + { + .url = "charities", + .method = MHD_HTTP_METHOD_PATCH, + .handler.patch = &DH_handler_charity_patch, + .nargs = 1, + .needs_authorization = true + }, /* DELETE charities */ { .url = "charities", @@ -583,8 +598,10 @@ handle_mhd_request (void *cls, } /* Check if upload is in bounds */ - if (0 == strcasecmp (method, - MHD_HTTP_METHOD_POST)) + if ( (0 == strcasecmp (method, + MHD_HTTP_METHOD_POST)) || + (0 == strcasecmp (method, + MHD_HTTP_METHOD_PATCH)) ) { TALER_MHD_check_content_length (connection, TALER_MHD_REQUEST_BUFFER_MAX); diff --git a/src/donau/donau-httpd.h b/src/donau/donau-httpd.h @@ -205,6 +205,19 @@ struct DH_RequestHandler const char *const args[]); /** + * Function to call to handle PATCH requests. + * + * @param rc context for the request + * @param json uploaded JSON data + * @param args array of arguments, needs to be of length @e nargs + * @return MHD result code + */ + MHD_RESULT + (*patch)(struct DH_RequestContext *rc, + const json_t *root, + const char *const args[]); + + /** * Function to call to handle DELETE requests. * * @param rc context for the request diff --git a/src/donau/donau-httpd_charity.h b/src/donau/donau-httpd_charity.h @@ -41,6 +41,21 @@ DH_handler_charity_post ( /** + * Handle a PATCH "/charities/$CHARITY_ID" request. + * + * @param rc request context + * @param root uploaded JSON data with updates + * @param args array with the charity identifier in args[0] + * @return MHD result code + */ +MHD_RESULT +DH_handler_charity_patch ( + struct DH_RequestContext *rc, + const json_t *root, + const char *const args[]); + + +/** * Handle a GET "/charities/$CHARITY_ID" request. * * @param rc request context diff --git a/src/donau/donau-httpd_charity_patch.c b/src/donau/donau-httpd_charity_patch.c @@ -0,0 +1,162 @@ +/* + 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file donau-httpd_charity_patch.c + * @brief Handle request to update a charity entry. + * @author Bohdan Potuzhnyi + */ +#include <donau_config.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_mhd_lib.h> +#include <taler/taler_util.h> +#include "donau-httpd_charity.h" +#include "donau-httpd_db.h" + + +MHD_RESULT +DH_handler_charity_patch (struct DH_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + struct DONAU_CharityPublicKeyP charity_pub; + uint64_t charity_id; + char dummy; + const char *charity_name = NULL; + const char *charity_url = NULL; + struct TALER_Amount max_per_year; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("charity_pub", + &charity_pub), + GNUNET_JSON_spec_string ("charity_name", + &charity_name), + GNUNET_JSON_spec_string ("charity_url", + &charity_url), + TALER_JSON_spec_amount ("max_per_year", + DH_currency, + &max_per_year), + GNUNET_JSON_spec_end () + }; + + if ( (NULL == args[0]) || + (1 != sscanf (args[0], + "%lu%c", + &charity_id, + &dummy)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "charity_id"); + } + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_SYSERR == res) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + + { + struct DONAUDB_CharityMetaData meta; + enum GNUNET_DB_QueryStatus qs; + + qs = DH_plugin->lookup_charity (DH_plugin->cls, + charity_id, + &meta); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_charity"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_DONAU_CHARITY_NOT_FOUND, + args[0]); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + if (0 < TALER_amount_cmp (&meta.receipts_to_date, + &max_per_year)) + { + GNUNET_break_op (0); + GNUNET_free (meta.charity_name); + GNUNET_free (meta.charity_url); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "max_per_year must NOT be SMALLER than receipts_to_date"); + } + + qs = DH_plugin->update_charity (DH_plugin->cls, + charity_id, + &charity_pub, + charity_name, + charity_url, + &max_per_year); + GNUNET_free (meta.charity_name); + GNUNET_free (meta.charity_url); + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "update_charity"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_DONAU_CHARITY_NOT_FOUND, + args[0]); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (rc->connection, + MHD_HTTP_OK, + NULL, + NULL, + 0); + } + } + + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "charity_patch"); +} + + +/* end of donau-httpd_charity_patch.c */ diff --git a/src/donaudb/Makefile.am b/src/donaudb/Makefile.am @@ -87,6 +87,7 @@ libdonau_plugin_donaudb_postgres_la_SOURCES = \ pg_get_history.h pg_get_history.c \ pg_get_charities.h pg_get_charities.c \ pg_insert_charity.h pg_insert_charity.c \ + pg_update_charity.h pg_update_charity.c \ pg_do_charity_delete.h pg_do_charity_delete.c \ pg_insert_history_entry.h pg_insert_history_entry.c \ pg_lookup_charity.h pg_lookup_charity.c \ diff --git a/src/donaudb/pg_update_charity.c b/src/donaudb/pg_update_charity.c @@ -0,0 +1,63 @@ +/* + 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 donaudb/pg_update_charity.c + * @brief Implementation of the update_charity function for Postgres + * @author Bohdan Potuzhnyi + */ +#include <donau_config.h> +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_update_charity.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +DH_PG_update_charity ( + void *cls, + uint64_t charity_id, + const struct DONAU_CharityPublicKeyP *charity_pub, + const char *charity_name, + const char *charity_url, + const struct TALER_Amount *max_per_year) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint64 (&charity_id), + GNUNET_PQ_query_param_auto_from_type (charity_pub), + GNUNET_PQ_query_param_string (charity_name), + GNUNET_PQ_query_param_string (charity_url), + TALER_PQ_query_param_amount (pg->conn, + max_per_year), + GNUNET_PQ_query_param_end + }; + + PREPARE (pg, + "update_charity", + "UPDATE charities" + " SET charity_pub = $2" + " ,charity_name = $3" + " ,charity_url = $4" + " ,max_per_year = $5" + " WHERE charity_id = $1;"); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "update_charity", + params); +} + + +/* end of pg_update_charity.c */ diff --git a/src/donaudb/pg_update_charity.h b/src/donaudb/pg_update_charity.h @@ -0,0 +1,47 @@ +/* + 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 donaudb/pg_update_charity.h + * @brief Implementation of the update_charity function for Postgres + * @author Bohdan Potuzhnyi + */ +#ifndef PG_UPDATE_CHARITY_H +#define PG_UPDATE_CHARITY_H + +#include <taler/taler_util.h> +#include "donaudb_plugin.h" + +/** + * Update an existing charity entry. + * + * @param cls closure + * @param charity_id identifier of the charity to update + * @param charity_pub new public key for the charity + * @param charity_name new name + * @param charity_url new landing page URL + * @param max_per_year yearly donation limit + * @return transaction status code + */ +enum GNUNET_DB_QueryStatus +DH_PG_update_charity ( + void *cls, + uint64_t charity_id, + const struct DONAU_CharityPublicKeyP *charity_pub, + const char *charity_name, + const char *charity_url, + const struct TALER_Amount *max_per_year); + +#endif diff --git a/src/donaudb/plugin_donaudb_postgres.c b/src/donaudb/plugin_donaudb_postgres.c @@ -54,6 +54,7 @@ #include "pg_lookup_issued_receipts.h" #include "pg_get_charities.h" #include "pg_insert_charity.h" +#include "pg_update_charity.h" #include "pg_do_charity_delete.h" /** @@ -71,14 +72,14 @@ * @param conn SQL connection that was used */ #define BREAK_DB_ERR(result,conn) do { \ - GNUNET_break (0); \ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \ - "Database failure: %s/%s/%s/%s/%s", \ - PQresultErrorField (result, PG_DIAG_MESSAGE_PRIMARY), \ - PQresultErrorField (result, PG_DIAG_MESSAGE_DETAIL), \ - PQresultErrorMessage (result), \ - PQresStatus (PQresultStatus (result)), \ - PQerrorMessage (conn)); \ + GNUNET_break (0); \ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \ + "Database failure: %s/%s/%s/%s/%s", \ + PQresultErrorField (result, PG_DIAG_MESSAGE_PRIMARY), \ + PQresultErrorField (result, PG_DIAG_MESSAGE_DETAIL), \ + PQresultErrorMessage (result), \ + PQresStatus (PQresultStatus (result)), \ + PQerrorMessage (conn)); \ } while (0) @@ -236,6 +237,8 @@ libdonau_plugin_donaudb_postgres_init (void *cls) = &DH_PG_lookup_charity; plugin->insert_charity = &DH_PG_insert_charity; + plugin->update_charity + = &DH_PG_update_charity; plugin->get_charities = &DH_PG_get_charities; plugin->do_charity_delete diff --git a/src/donaudb/test_donaudb.c b/src/donaudb/test_donaudb.c @@ -33,25 +33,25 @@ static int result; * Report line of error if @a cond is true, and jump to label "drop". */ #define FAILIF(cond) \ - do { \ - if (! (cond)) { break;} \ - GNUNET_break (0); \ - goto drop; \ - } while (0) + do { \ + if (! (cond)) { break;} \ + GNUNET_break (0); \ + goto drop; \ + } while (0) /** * Initializes @a ptr with random data. */ #define RND_BLK(ptr) \ - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, ptr, sizeof (* \ - ptr)) + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, ptr, sizeof (* \ + ptr)) /** * Initializes @a ptr with zeros. */ #define ZR_BLK(ptr) \ - memset (ptr, 0, sizeof (*ptr)) + memset (ptr, 0, sizeof (*ptr)) /** * How big do we make the RSA keys? @@ -238,6 +238,51 @@ run (void *cls) &charities_cb, charities)); + { + /* Update the charity and verify the new key and metadata persist. */ + const char *updated_charity_name = "charity_name_updated"; + const char *updated_charity_url = "charity_url_updated"; + struct TALER_Amount updated_max; + struct DONAU_CharityPrivateKeyP updated_charity_priv; + struct DONAU_CharityPublicKeyP updated_charity_pub; + + GNUNET_CRYPTO_eddsa_key_create (&updated_charity_priv.eddsa_priv); + GNUNET_CRYPTO_eddsa_key_get_public (&updated_charity_priv.eddsa_priv, + &updated_charity_pub.eddsa_pub); + + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (CURRENCY ":2.000000", + &updated_max)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->update_charity (plugin->cls, + charity_id, + &updated_charity_pub, + updated_charity_name, + updated_charity_url, + &updated_max)); + + ZR_BLK (&charity_meta); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->lookup_charity (plugin->cls, + charity_id, + &charity_meta)); + GNUNET_assert (0 == GNUNET_memcmp (&charity_meta.charity_pub, + &updated_charity_pub)); + GNUNET_assert (0 == strcmp (charity_meta.charity_name, + updated_charity_name)); + GNUNET_assert (0 == strcmp (charity_meta.charity_url, + updated_charity_url)); + GNUNET_assert (0 == TALER_amount_cmp (&charity_meta.max_per_year, + &updated_max)); + GNUNET_free (charity_meta.charity_name); + GNUNET_free (charity_meta.charity_url); + + charity_name = updated_charity_name; + charity_url = updated_charity_url; + max_per_year = updated_max; + charity_pub = updated_charity_pub; + } + /* test delete charity */ FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->do_charity_delete (plugin->cls, diff --git a/src/include/donau_testing_lib.h b/src/include/donau_testing_lib.h @@ -80,6 +80,27 @@ TALER_TESTING_cmd_charity_post (const char *label, unsigned int expected_response_code); /** + * Create a PATCH "charity" command. + * + * @param label the command label. + * @param charity_reference reference to an existing charity command + * @param name updated name for the charity + * @param url updated url for the charity + * @param max_per_year updated limit as amount string + * @param bearer authorization token + * @param expected_response_code expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_charity_patch (const char *label, + const char *charity_reference, + const char *name, + const char *url, + const char *max_per_year, + const struct DONAU_BearerToken *bearer, + unsigned int expected_response_code); + +/** * Create a DELETE "charity" command. * * @param label the command label. @@ -198,24 +219,24 @@ TALER_TESTING_get_donau_url ( * Call #op on all simple traits. */ #define DONAU_TESTING_SIMPLE_TRAITS(op) \ - op (charity_priv, const struct DONAU_CharityPrivateKeyP) \ - op (charity_pub, const struct DONAU_CharityPublicKeyP) \ - op (charity_id, const uint64_t) \ - op (donau_url, const char) \ - op (donau_keys, struct DONAU_Keys) \ - op (donor_salt, const char) \ - op (donor_tax_id, const char) \ - op (salted_tax_id_hash, const struct DONAU_HashDonorTaxId) \ - op (donation_receipts, const struct DONAU_DonationReceipt*) \ - op (number_receipts, const size_t) + op (charity_priv, const struct DONAU_CharityPrivateKeyP) \ + op (charity_pub, const struct DONAU_CharityPublicKeyP) \ + op (charity_id, const uint64_t) \ + op (donau_url, const char) \ + op (donau_keys, struct DONAU_Keys) \ + op (donor_salt, const char) \ + op (donor_tax_id, const char) \ + op (salted_tax_id_hash, const struct DONAU_HashDonorTaxId) \ + op (donation_receipts, const struct DONAU_DonationReceipt*) \ + op (number_receipts, const size_t) /** * Call #op on all indexed traits. */ #define DONAU_TESTING_INDEXED_TRAITS(op) \ - op (donation_unit_pub, const struct DONAU_DonationUnitInformation) \ - op (donau_pub, const struct TALER_ExchangePublicKeyP) + op (donation_unit_pub, const struct DONAU_DonationUnitInformation) \ + op (donau_pub, const struct TALER_ExchangePublicKeyP) DONAU_TESTING_SIMPLE_TRAITS (TALER_TESTING_MAKE_DECL_SIMPLE_TRAIT) DONAU_TESTING_INDEXED_TRAITS (TALER_TESTING_MAKE_DECL_INDEXED_TRAIT) diff --git a/src/include/donaudb_plugin.h b/src/include/donaudb_plugin.h @@ -216,7 +216,7 @@ struct DONAUDB_Plugin * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure */ enum GNUNET_GenericReturnValue - (*drop_tables)(void *cls); + (*drop_tables)(void *cls); /** * Create the necessary tables if they are not present @@ -229,7 +229,7 @@ struct DONAUDB_Plugin * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure */ enum GNUNET_GenericReturnValue - (*create_tables)(void *cls); + (*create_tables)(void *cls); /** @@ -241,8 +241,8 @@ struct DONAUDB_Plugin * @return #GNUNET_OK on success */ enum GNUNET_GenericReturnValue - (*start)(void *cls, - const char *name); + (*start)(void *cls, + const char *name); /** @@ -254,8 +254,8 @@ struct DONAUDB_Plugin * @return #GNUNET_OK on success */ enum GNUNET_GenericReturnValue - (*start_read_committed)(void *cls, - const char *name); + (*start_read_committed)(void *cls, + const char *name); /** * Start a READ ONLY serializable transaction. @@ -266,8 +266,8 @@ struct DONAUDB_Plugin * @return #GNUNET_OK on success */ enum GNUNET_GenericReturnValue - (*start_read_only)(void *cls, - const char *name); + (*start_read_only)(void *cls, + const char *name); /** @@ -277,7 +277,7 @@ struct DONAUDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*commit)(void *cls); + (*commit)(void *cls); /** @@ -291,7 +291,7 @@ struct DONAUDB_Plugin * #GNUNET_SYSERR on hard errors */ enum GNUNET_GenericReturnValue - (*preflight)(void *cls); + (*preflight)(void *cls); /** @@ -312,7 +312,7 @@ struct DONAUDB_Plugin * #GNUNET_SYSERR on DB errors */ enum GNUNET_GenericReturnValue - (*gc)(void *cls); + (*gc)(void *cls); /** @@ -367,7 +367,7 @@ struct DONAUDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_charity)( + (*lookup_charity)( void *cls, uint64_t charity_id, struct DONAUDB_CharityMetaData *meta); @@ -382,7 +382,7 @@ struct DONAUDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*do_charity_delete)( + (*do_charity_delete)( void *cls, uint64_t charity_id); @@ -395,7 +395,7 @@ struct DONAUDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*get_charities)( + (*get_charities)( void *cls, DONAUDB_GetCharitiesCallback cb, void *cb_cls); @@ -414,7 +414,7 @@ struct DONAUDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*insert_charity)( + (*insert_charity)( void *cls, const struct DONAU_CharityPublicKeyP *charity_pub, const char *charity_name, @@ -424,6 +424,27 @@ struct DONAUDB_Plugin /** + * Update existing charity meta data. + * + * @param cls closure + * @param charity_id identifier of the charity to update + * @param charity_pub new public key + * @param charity_name new human-readable name + * @param charity_url new URL of the charity + * @param max_per_year new yearly donation cap + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*update_charity)( + void *cls, + uint64_t charity_id, + const struct DONAU_CharityPublicKeyP *charity_pub, + const char *charity_name, + const char *charity_url, + const struct TALER_Amount *max_per_year); + + + /** * Iterate donation units. * * @param cls closure @@ -432,7 +453,7 @@ struct DONAUDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*iterate_donation_units)( + (*iterate_donation_units)( void *cls, DONAUDB_IterateDonationUnitsCallback cb, void *cb_cls); @@ -446,7 +467,7 @@ struct DONAUDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*get_history)( + (*get_history)( void *cls, DONAUDB_GetHistoryCallback cb, void *cb_cls); @@ -460,7 +481,7 @@ struct DONAUDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_history_entry)( + (*lookup_history_entry)( void *cls, const unsigned long long charity_id, const struct TALER_Amount *final_amount, @@ -474,7 +495,7 @@ struct DONAUDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*insert_donation_unit)( + (*insert_donation_unit)( void *cls, const struct DONAU_DonationUnitHashP *h_donation_unit_pub, const struct DONAU_DonationUnitPublicKey *donation_unit_pub, @@ -491,7 +512,7 @@ struct DONAUDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*insert_history_entry)( + (*insert_history_entry)( void *cls, const uint64_t charity_id, const struct TALER_Amount *final_amount, @@ -510,7 +531,7 @@ struct DONAUDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*insert_issued_receipt)( + (*insert_issued_receipt)( void *cls, const size_t num_blinded_sig, const struct DONAU_BlindedDonationUnitSignature signatures[num_blinded_sig], @@ -531,7 +552,7 @@ struct DONAUDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*insert_submitted_receipts)( + (*insert_submitted_receipts)( void *cls, struct DONAU_HashDonorTaxId *h_donor_tax_id, size_t num_dr, @@ -546,7 +567,7 @@ struct DONAUDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*iterate_submitted_receipts)( + (*iterate_submitted_receipts)( void *cls, const uint64_t donation_year, const struct DONAU_HashDonorTaxId *h_donor_tax_id, @@ -560,7 +581,7 @@ struct DONAUDB_Plugin * @param value the amount of the donation unit */ enum GNUNET_DB_QueryStatus - (*lookup_donation_unit_amount)( + (*lookup_donation_unit_amount)( void *cls, const struct DONAU_DonationUnitHashP *h_donation_unit_pub, struct TALER_Amount *value); @@ -574,7 +595,7 @@ struct DONAUDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*lookup_issued_receipts)( + (*lookup_issued_receipts)( void *cls, struct DONAU_DonationReceiptHashP *h_receitps, struct DONAUDB_IssuedReceiptsMetaData *meta); @@ -588,7 +609,7 @@ struct DONAUDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*insert_signing_key)( + (*insert_signing_key)( void *cls, const struct DONAU_DonauPublicKeyP *donau_pub, struct DONAUDB_SignkeyMetaData *meta); @@ -602,7 +623,7 @@ struct DONAUDB_Plugin * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*lookup_signing_key)( + (*lookup_signing_key)( void *cls, const struct DONAU_DonauPublicKeyP *donau_pub, struct DONAUDB_SignkeyMetaData *meta); @@ -616,7 +637,7 @@ struct DONAUDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*iterate_active_signing_keys)( + (*iterate_active_signing_keys)( void *cls, DONAUDB_IterateActiveSigningKeysCallback cb, void *cb_cls); diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am @@ -23,6 +23,7 @@ libdonau_la_SOURCES = \ donau_api_handle.c \ donau_api_charity_get.c \ donau_api_charity_post.c \ + donau_api_charity_patch.c \ donau_api_charity_delete.c \ donau_api_charities_get.c \ donau_api_curl_defaults.c donau_api_curl_defaults.h \ diff --git a/src/lib/donau_api_charity_patch.c b/src/lib/donau_api_charity_patch.c @@ -0,0 +1,273 @@ +/* + 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 lib/donau_api_charity_patch.c + * @brief Implementation of the PATCH /charities/$ID call for the donau HTTP API + * @author Bohdan Potuzhnyi + */ +#include <jansson.h> +#include <microhttpd.h> +#include <gnunet/gnunet_curl_lib.h> +#include <taler/taler_curl_lib.h> +#include <taler/taler_json_lib.h> +#include "donau_service.h" +#include "donau_api_curl_defaults.h" +#include "donau_json_lib.h" + + +/** + * Handle for a PATCH /charities/$ID request. + */ +struct DONAU_CharityPatchHandle +{ + /** + * Fully qualified request URL. + */ + char *url; + + /** + * CURL job for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Callback for the response. + */ + DONAU_PatchCharityResponseCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Reference to the CURL context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Helper context for POST-style uploads. + */ + struct TALER_CURL_PostContext post_ctx; +}; + + +/** + * Finalizer called once the PATCH request is complete. + */ +static void +handle_charity_patch_finished (void *cls, + long response_code, + const void *resp_obj) +{ + struct DONAU_CharityPatchHandle *cph = cls; + const json_t *j = resp_obj; + struct DONAU_PatchCharityResponse pcresp = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + cph->job = NULL; + switch (response_code) + { + case 0: + pcresp.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + GNUNET_break (0); + break; + case MHD_HTTP_OK: + case MHD_HTTP_NO_CONTENT: + /* nothing further to parse */ + break; + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_FORBIDDEN: + case MHD_HTTP_UNAUTHORIZED: + case MHD_HTTP_CONFLICT: + case MHD_HTTP_INTERNAL_SERVER_ERROR: + pcresp.hr.ec = TALER_JSON_get_error_code (j); + pcresp.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + pcresp.hr.ec = TALER_JSON_get_error_code (j); + pcresp.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %ld for PATCH %s\n", + response_code, + cph->url); + break; + } + + if (NULL != cph->cb) + { + cph->cb (cph->cb_cls, + &pcresp); + cph->cb = NULL; + } + DONAU_charity_patch_cancel (cph); +} + + +struct DONAU_CharityPatchHandle * +DONAU_charity_patch ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const uint64_t charity_id, + const char *charity_name, + const char *charity_url, + const struct TALER_Amount *max_per_year, + const struct DONAU_CharityPublicKeyP *charity_pub, + const struct DONAU_BearerToken *bearer, + DONAU_PatchCharityResponseCallback cb, + void *cb_cls) +{ + struct DONAU_CharityPatchHandle *cph; + CURL *eh; + json_t *body; + + if ( (NULL == charity_name) || + (NULL == charity_url) || + (NULL == max_per_year) || + (NULL == charity_pub) ) + { + /* Caller must provide the complete CharityRequest payload. */ + GNUNET_break_op (0); + return NULL; + } + + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("charity_pub", + charity_pub), + GNUNET_JSON_pack_string ("charity_url", + charity_url), + GNUNET_JSON_pack_string ("charity_name", + charity_name), + TALER_JSON_pack_amount ("max_per_year", + max_per_year)); + if (NULL == body) + { + GNUNET_break (0); + return NULL; + } + + cph = GNUNET_new (struct DONAU_CharityPatchHandle); + cph->ctx = ctx; + cph->cb = cb; + cph->cb_cls = cb_cls; + + { + char *path; + + GNUNET_asprintf (&path, + "charities/%llu", + (unsigned long long) charity_id); + cph->url = TALER_url_join (url, + path, + NULL); + GNUNET_free (path); + } + if (NULL == cph->url) + { + json_decref (body); + GNUNET_free (cph); + GNUNET_break (0); + return NULL; + } + + eh = DONAU_curl_easy_get_ (cph->url); + if (NULL == eh) + { + json_decref (body); + GNUNET_free (cph->url); + GNUNET_free (cph); + GNUNET_break (0); + return NULL; + } + if (GNUNET_OK != + TALER_curl_easy_post (&cph->post_ctx, + eh, + body)) + { + GNUNET_break (0); + curl_easy_cleanup (eh); + json_decref (body); + GNUNET_free (cph->url); + GNUNET_free (cph); + return NULL; + } + json_decref (body); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_CUSTOMREQUEST, + MHD_HTTP_METHOD_PATCH)); + cph->job = GNUNET_CURL_job_add2 (ctx, + eh, + cph->post_ctx.headers, + &handle_charity_patch_finished, + cph); + if (NULL == cph->job) + { + GNUNET_break (0); + TALER_curl_easy_post_finished (&cph->post_ctx); + curl_easy_cleanup (eh); + GNUNET_free (cph->url); + GNUNET_free (cph); + return NULL; + } + + if (NULL != bearer) + { + struct curl_slist *auth; + char *hdr; + + GNUNET_asprintf (&hdr, + "%s: Bearer %s", + MHD_HTTP_HEADER_AUTHORIZATION, + bearer->token); + auth = curl_slist_append (NULL, + hdr); + GNUNET_free (hdr); + GNUNET_CURL_extend_headers (cph->job, + auth); + curl_slist_free_all (auth); + } + + return cph; +} + + +void +DONAU_charity_patch_cancel ( + struct DONAU_CharityPatchHandle *cph) +{ + if (NULL == cph) + return; + if (NULL != cph->job) + { + GNUNET_CURL_job_cancel (cph->job); + cph->job = NULL; + } + TALER_curl_easy_post_finished (&cph->post_ctx); + GNUNET_free (cph->url); + GNUNET_free (cph); +} + + +/* end of donau_api_charity_patch.c */ diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am @@ -24,6 +24,7 @@ libdonautesting_la_SOURCES = \ testing_api_cmd_charities_get.c \ testing_api_cmd_charity_get.c \ testing_api_cmd_charity_post.c \ + testing_api_cmd_charity_patch.c \ testing_api_cmd_charity_delete.c \ testing_api_cmd_issue_receipts.c \ testing_api_cmd_submit_receipts.c \ @@ -78,4 +79,3 @@ EXTRA_DIST = \ coins-cs.conf \ coins-rsa.conf \ test_donau_api.conf - diff --git a/src/testing/test_donau_api.c b/src/testing/test_donau_api.c @@ -80,12 +80,22 @@ run (void *cls, TALER_TESTING_cmd_charity_get ("get-charity-by-id", "post-charity", // cmd trait reference MHD_HTTP_OK), + TALER_TESTING_cmd_charity_patch ("patch-charity", + "post-charity", + "example-updated", + "example.org", + "EUR:15", + &bearer, + MHD_HTTP_OK), + TALER_TESTING_cmd_charity_get ("get-charity-after-patch", + "patch-charity", + MHD_HTTP_OK), TALER_TESTING_cmd_charities_get ("get-charities", &bearer, MHD_HTTP_OK), // FIXME: CSR signatures TALER_TESTING_cmd_issue_receipts ("issue-receipts", - "post-charity", + "patch-charity", uses_cs, 2025, "7560001010000", // tax id @@ -100,7 +110,7 @@ run (void *cls, 2025, MHD_HTTP_OK), TALER_TESTING_cmd_charity_delete ("delete-charity", - "post-charity", // cmd trait reference + "patch-charity", // cmd trait reference &bearer, MHD_HTTP_NO_CONTENT), /* End the suite. */ diff --git a/src/testing/testing_api_cmd_charity_patch.c b/src/testing/testing_api_cmd_charity_patch.c @@ -0,0 +1,269 @@ +/* + 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 + 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/testing_api_cmd_charity_patch.c + * @brief Implement the PATCH /charities/$ID test command. + * @author Bohdan Potuzhnyi + */ +#include <donau_config.h> +#include <taler/taler_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include <taler/taler_testing_lib.h> +#include "donau_testing_lib.h" + + +/** + * Command state for PATCH /charities/$ID. + */ +struct CharityPatchState +{ + /** + * Handle for the in-flight PATCH request (if any). + */ + struct DONAU_CharityPatchHandle *cph; + + /** + * Reference label of the command that created the original charity. + */ + const char *charity_reference; + + /** + * Fresh charity key pair generated for the update. + */ + struct DONAU_CharityPrivateKeyP charity_priv; + + /** + * Corresponding public key transmitted in the PATCH body. + */ + struct DONAU_CharityPublicKeyP charity_pub; + + /** + * Updated yearly donation limit. + */ + struct TALER_Amount max_per_year; + + /** + * Updated human-readable name. + */ + const char *charity_name; + + /** + * Updated contact URL. + */ + const char *charity_url; + + /** + * Administrator bearer token used for authentication. + */ + const struct DONAU_BearerToken *bearer; + + /** + * Expected HTTP status code for the PATCH response. + */ + unsigned int expected_response_code; + + /** + * Database identifier of the charity being updated. + */ + uint64_t charity_id; + + /** + * Interpreter instance driving the command sequence. + */ + struct TALER_TESTING_Interpreter *is; +}; + + +/** + * Check HTTP response for the PATCH request and advance the interpreter on success. + * + * @param cls closure + * @param resp HTTP response details + */ +static void +charity_patch_cb (void *cls, + const struct DONAU_PatchCharityResponse *resp) +{ + struct CharityPatchState *ps = cls; + + ps->cph = NULL; + if (ps->expected_response_code != resp->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected HTTP response code: %u\n", + resp->hr.http_status); + json_dumpf (resp->hr.reply, + stderr, + 0); + TALER_TESTING_interpreter_fail (ps->is); + return; + } + TALER_TESTING_interpreter_next (ps->is); +} + + +/** + * Run the PATCH command after extracting the referenced charity id. + * + * @param cls closure + * @param cmd command definition + * @param is interpreter state + */ +static void +charity_patch_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct CharityPatchState *ps = cls; + const struct TALER_TESTING_Command *charity_cmd; + const uint64_t *charity_id; + + (void) cmd; + ps->is = is; + + charity_cmd = + TALER_TESTING_interpreter_lookup_command (is, + ps->charity_reference); + if ( (NULL == charity_cmd) || + (GNUNET_OK != + TALER_TESTING_get_trait_charity_id (charity_cmd, + &charity_id)) ) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } + ps->charity_id = *charity_id; + + ps->cph = DONAU_charity_patch ( + TALER_TESTING_interpreter_get_context (is), + TALER_TESTING_get_donau_url (is), + ps->charity_id, + ps->charity_name, + ps->charity_url, + &ps->max_per_year, + &ps->charity_pub, + ps->bearer, + &charity_patch_cb, + ps); + if (NULL == ps->cph) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + } +} + + +/** + * Cancel any outstanding PATCH request and release resources. + * + * @param cls closure + * @param cmd command being cleaned up + */ +static void +charity_patch_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct CharityPatchState *ps = cls; + + if (NULL != ps->cph) + { + TALER_TESTING_command_incomplete (ps->is, + cmd->label); + DONAU_charity_patch_cancel (ps->cph); + ps->cph = NULL; + } + GNUNET_free (ps); +} + + +/** + * Offer traits produced by the PATCH command to subsequent commands. + * + * @param cls closure + * @param[out] ret location to store the requested trait + * @param trait trait identifier + * @param index trait index + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +charity_patch_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct CharityPatchState *ps = cls; + struct TALER_TESTING_Trait traits[] = { + TALER_TESTING_make_trait_charity_priv (&ps->charity_priv), + TALER_TESTING_make_trait_charity_pub (&ps->charity_pub), + TALER_TESTING_make_trait_charity_id (&ps->charity_id), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_charity_patch (const char *label, + const char *charity_reference, + const char *name, + const char *url, + const char *max_per_year, + const struct DONAU_BearerToken *bearer, + unsigned int expected_response_code) +{ + struct CharityPatchState *ps; + + ps = GNUNET_new (struct CharityPatchState); + GNUNET_CRYPTO_eddsa_key_create (&ps->charity_priv.eddsa_priv); + GNUNET_CRYPTO_eddsa_key_get_public (&ps->charity_priv.eddsa_priv, + &ps->charity_pub.eddsa_pub); + ps->charity_reference = charity_reference; + ps->charity_name = name; + ps->charity_url = url; + ps->bearer = bearer; + ps->expected_response_code = expected_response_code; + if (GNUNET_OK != + TALER_string_to_amount (max_per_year, + &ps->max_per_year)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse amount `%s' for command %s\n", + max_per_year, + label); + GNUNET_free (ps); + GNUNET_assert (0); + } + + { + struct TALER_TESTING_Command cmd = { + .cls = ps, + .label = label, + .run = &charity_patch_run, + .cleanup = &charity_patch_cleanup, + .traits = &charity_patch_traits + }; + + return cmd; + } +}