donau

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

commit 4d40e5a4aa2047c2fd5b055a30797835752443e2
parent 037a791121a81a78c45b1be96ba3599a5e1103f8
Author: Christian Grothoff <christian@grothoff.org>
Date:   Fri,  7 Aug 2026 00:38:00 +0200

prevent charity deletion if it has issued receipts we still care about

Diffstat:
Msrc/donau/donau-httpd_delete-charities-CHARITY_ID.c | 15++++++++++++++-
Msrc/donaudb/delete_charity.c | 32++++++++++++++++++++++++++------
Asrc/donaudb/delete_charity.sql | 40++++++++++++++++++++++++++++++++++++++++
Asrc/donaudb/donau-0005.sql | 35+++++++++++++++++++++++++++++++++++
Msrc/donaudb/meson.build | 2++
Msrc/donaudb/test_donaudb.c | 51++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/include/donau-database/delete_charity.h | 10++++++++--
7 files changed, 175 insertions(+), 10 deletions(-)

diff --git a/src/donau/donau-httpd_delete-charities-CHARITY_ID.c b/src/donau/donau-httpd_delete-charities-CHARITY_ID.c @@ -54,9 +54,22 @@ DH_handler_delete_charities ( { enum GNUNET_DB_QueryStatus qs; + bool in_use = false; qs = DONAUDB_delete_charity (DH_context, - (uint64_t) charity_id); + (uint64_t) charity_id, + &in_use); + if (in_use) + { + /* FIXME: this deserves a dedicated error code in GANA + (e.g. DONAU_CHARITY_IN_USE); TALER_EC_GENERIC_DB_STORE_FAILED is + used here only because no donau-specific code exists yet. */ + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_GENERIC_DB_STORE_FAILED, + "charity has issued receipts and cannot be deleted"); + } switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: diff --git a/src/donaudb/delete_charity.c b/src/donaudb/delete_charity.c @@ -28,18 +28,38 @@ enum GNUNET_DB_QueryStatus DONAUDB_delete_charity (struct DONAUDB_PostgresContext *ctx, - uint64_t charity_id) + uint64_t charity_id, + bool *in_use) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&charity_id), GNUNET_PQ_query_param_end }; + bool found; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("found", + &found), + GNUNET_PQ_result_spec_bool ("in_use", + in_use), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + *in_use = false; PREPARE (ctx, "delete_charity", - "DELETE FROM charities " - "WHERE charity_id=$1"); - return GNUNET_PQ_eval_prepared_non_select (ctx->conn, - "delete_charity", - params); + "SELECT " + " out_found AS found" + ",out_in_use AS in_use" + " FROM do_delete_charity" + " ($1);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (ctx->conn, + "delete_charity", + params, + rs); + if (qs <= 0) + return qs; + if (! found) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } diff --git a/src/donaudb/delete_charity.sql b/src/donaudb/delete_charity.sql @@ -0,0 +1,40 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 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/> +-- + +DROP FUNCTION IF EXISTS do_delete_charity; +CREATE FUNCTION do_delete_charity ( + IN in_charity_id INT8 + ,OUT out_found BOOLEAN + ,OUT out_in_use BOOLEAN +) +LANGUAGE plpgsql +AS $$ +BEGIN + out_in_use = FALSE; + DELETE FROM charities + WHERE charity_id=in_charity_id; + out_found = FOUND; +EXCEPTION + WHEN foreign_key_violation THEN + -- receipts_issued references this charity with ON DELETE RESTRICT, + -- because receipts_issued.receipt_hash is the idempotence key of + -- POST /batch-issue and must outlive the charity row. + out_found = TRUE; + out_in_use = TRUE; +END $$; + +COMMENT ON FUNCTION do_delete_charity + IS 'Deletes a charity. out_found is FALSE if no charity with the given charity_id exists; out_in_use is TRUE if the charity still has issued receipts and was therefore NOT deleted.'; diff --git a/src/donaudb/donau-0005.sql b/src/donaudb/donau-0005.sql @@ -0,0 +1,35 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 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/> +-- + +BEGIN; + +SELECT _v.register_patch('donau-0005', NULL, NULL); +SET search_path TO donau; + +-- receipts_issued.receipt_hash is the idempotence key of POST /batch-issue: +-- it is what stops a charity from having the same blinded UDIs signed twice. +-- With ON DELETE CASCADE that guarantee lasted only as long as the charity +-- row: deleting and re-registering a charity freed every one of its receipt +-- hashes for replay, and destroyed the record of what had been issued. +ALTER TABLE receipts_issued + DROP CONSTRAINT receipts_issued_charity_id_fkey; +ALTER TABLE receipts_issued + ADD CONSTRAINT receipts_issued_charity_id_fkey + FOREIGN KEY (charity_id) + REFERENCES charities (charity_id) + ON DELETE RESTRICT; + +COMMIT; diff --git a/src/donaudb/meson.build b/src/donaudb/meson.build @@ -22,6 +22,7 @@ procedures_sql = [ 'do_insert_receipt_issued.sql', 'insert_charity.sql', 'update_charity.sql', + 'delete_charity.sql', 'commit.sql', ] @@ -33,6 +34,7 @@ generated_sql = [ ['donau-0002.sql', donau_0002], ['donau-0003.sql', ['donau-0003.sql']], ['donau-0004.sql', ['donau-0004.sql']], + ['donau-0005.sql', ['donau-0005.sql']], ] diff --git a/src/donaudb/test_donaudb.c b/src/donaudb/test_donaudb.c @@ -502,6 +502,7 @@ run (void *cls) struct TALER_Amount amount_receipts; bool smaller_than_max_per_year; bool charity_unknown; + bool charity_in_use; struct TALER_DenominationPrivateKey denom_priv; struct TALER_DenominationPublicKey denom_pub; struct DONAU_DonationUnitPublicKey du_pub; @@ -617,7 +618,8 @@ run (void *cls) /* test delete charity */ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != DONAUDB_delete_charity (ctx, - charity_id)); + charity_id, + &charity_in_use)); /* test insert donation unit */ RND_BLK (&h_donation_unit_pub); @@ -1188,6 +1190,53 @@ run (void *cls) &zero)); } + /* D-13: receipts_issued.receipt_hash is the idempotence key of + POST /batch-issue. Deleting a charity used to cascade those rows + away, so re-registering the charity re-enabled replay of every + request it had ever made. The delete must now be refused, and + reported as such rather than as a hard error. */ + { + uint64_t busy_charity_id; + struct DONAUDB_CharityMetaData survivor; + bool under_limit = false; + bool in_use = false; + + FAILIF (GNUNET_OK != + make_charity (CURRENCY ":1000", + &busy_charity_id)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + issue_receipt (busy_charity_id, + GNUNET_TIME_get_current_year (), + CURRENCY ":7", + &under_limit)); + FAILIF (! under_limit); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + DONAUDB_delete_charity (ctx, + busy_charity_id, + &in_use)); + FAILIF (! in_use); + /* The charity -- and with it the issued-receipt record -- survives. */ + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + DONAUDB_get_charity (ctx, + busy_charity_id, + &survivor)); + GNUNET_free (survivor.charity_name); + GNUNET_free (survivor.charity_url); + /* A charity without issued receipts is still deletable. */ + { + uint64_t idle_charity_id; + + FAILIF (GNUNET_OK != + make_charity (CURRENCY ":1000", + &idle_charity_id)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + DONAUDB_delete_charity (ctx, + idle_charity_id, + &in_use)); + FAILIF (in_use); + } + } + result = 0; drop: diff --git a/src/include/donau-database/delete_charity.h b/src/include/donau-database/delete_charity.h @@ -29,11 +29,17 @@ * * @param ctx the @e cls of this struct with the plugin-specific state * @param charity_id charity to delete - * @return transaction status code + * @param[out] in_use set to true if the charity still has issued receipts + * and was therefore NOT deleted; the receipt hashes are the + * idempotence key of POST /batch-issue and must outlive the + * charity row + * @return transaction status code; + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if there was no such charity */ enum GNUNET_DB_QueryStatus DONAUDB_delete_charity ( struct DONAUDB_PostgresContext *ctx, - uint64_t charity_id); + uint64_t charity_id, + bool *in_use); #endif