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:
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