challenger

OAuth 2.0-based authentication service that validates user can receive messages at a certain address
Log | Files | Refs | Submodules | README | LICENSE

commit 46d3e49ff9b9784e5e95875a7db1e39a5255a9cb
parent 929085d4489f0b15b2ca4337870e9e3e6b7c0571
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu,  6 Aug 2026 15:01:49 +0200

distinguish URI collision from case where client is unknown

Diffstat:
Msrc/challenger/challenger-admin.c | 15+++++++++++----
Msrc/challenger/meson.build | 1+
Asrc/challenger/test-challenger-admin.sh | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/challengerdb/update_client.c | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/include/challenger-database/update_client.h | 14++++++++++++--
5 files changed, 179 insertions(+), 14 deletions(-)

diff --git a/src/challenger/challenger-admin.c b/src/challenger/challenger-admin.c @@ -160,6 +160,7 @@ run (void *cls, { enum GNUNET_DB_QueryStatus qs; unsigned long long row_id; + bool uri_conflict = false; char dummy; if ( (! isdigit ((unsigned char) client_id[0])) || @@ -180,7 +181,8 @@ run (void *cls, qs = CHALLENGERDB_update_client (db, row_id, redirect_uri, - client_secret); + client_secret, + &uri_conflict); switch (qs) { case GNUNET_DB_STATUS_SOFT_ERROR: @@ -189,9 +191,14 @@ run (void *cls, global_ret = EXIT_FAILURE; goto cleanup; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Client %llu not found.\n", - row_id); + if (uri_conflict) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "CLIENT_REDIRECT_URI is already in use by another client. Client %llu was not modified.\n", + row_id); + else + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client %llu not found.\n", + row_id); global_ret = EXIT_FAILURE; goto cleanup; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: diff --git a/src/challenger/meson.build b/src/challenger/meson.build @@ -9,6 +9,7 @@ install_data('challenger.conf', install_dir: pkgcfgdir) check_SCRIPTS = [ 'test-challenger', + 'test-challenger-admin', 'test-challenger-dbconfig', 'test-challenger-pkce', 'test-challenger-pkce-downgrade', diff --git a/src/challenger/test-challenger-admin.sh b/src/challenger/test-challenger-admin.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# This file is in the public domain. +# +# Tests the diagnostics of challenger-admin. Needs a database, but no daemon. +# +# The interesting case is '-m' with a CLIENT_REDIRECT_URI that another client +# already uses: that violates clients_uri_key, which GNUnet reports as +# SUCCESS_NO_RESULTS -- the very same status as "no such client". Reporting +# it as "Client not found" tells the operator (and any deployment script) the +# wrong thing. + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " FAIL: $@" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + rm -f "$LAST_OUTPUT" +} + +LAST_OUTPUT=$(mktemp adminXXXXXX.log) + +# Install cleanup handler (except for kill -9) +trap cleanup EXIT + +export PATH="$PATH:." + +echo -n "Testing for challenger-admin ..." +challenger-admin -h > /dev/null || exit_skip "challenger-admin required" +echo " FOUND" + +CONF="test-challenger.conf" +URI_A="http://client-a.example.com/" +URI_B="http://client-b.example.com/" +URI_C="http://client-c.example.com/" +CLIENT_SECRET="secret-token:secret" + +echo -n "Initialize challenger database ..." +challenger-dbinit -r -c "${CONF}" &> dbinit.log +echo " OK" + +echo -n "Add two challenger clients ..." +ID_A=$(challenger-admin -c "${CONF}" -q -a "${CLIENT_SECRET}" "${URI_A}") +ID_B=$(challenger-admin -c "${CONF}" -q -a "${CLIENT_SECRET}" "${URI_B}") +if [ "${ID_A}" = "${ID_B}" ] +then + exit_fail "Expected two distinct client IDs, got ${ID_A} twice" +fi +echo " OK (${ID_A}, ${ID_B})" + +echo -n "Modifying client ${ID_B} to the URI of client ${ID_A} ..." +if challenger-admin -c "${CONF}" -m "${ID_B}" "${URI_A}" &> "$LAST_OUTPUT" +then + exit_fail "Expected failure, but the modification succeeded: $(cat "$LAST_OUTPUT")" +fi +if grep -q "not found" "$LAST_OUTPUT" +then + exit_fail "URI collision was misreported as a missing client: $(cat "$LAST_OUTPUT")" +fi +if ! grep -qi "already in use" "$LAST_OUTPUT" +then + exit_fail "Expected an error about the URI being in use, got: $(cat "$LAST_OUTPUT")" +fi +echo " OK" + +echo -n "Modifying a client that does not exist ..." +if challenger-admin -c "${CONF}" -m 999999 "${URI_C}" &> "$LAST_OUTPUT" +then + exit_fail "Expected failure, but the modification succeeded: $(cat "$LAST_OUTPUT")" +fi +if ! grep -q "not found" "$LAST_OUTPUT" +then + exit_fail "Expected 'not found', got: $(cat "$LAST_OUTPUT")" +fi +echo " OK" + +echo -n "Modifying client ${ID_B} to an unused URI ..." +challenger-admin -c "${CONF}" -m "${ID_B}" "${URI_C}" &> "$LAST_OUTPUT" \ + || exit_fail "Expected success, got: $(cat "$LAST_OUTPUT")" +echo " OK" + +echo -n "Re-using the now free URI for client ${ID_A} ..." +challenger-admin -c "${CONF}" -m "${ID_A}" "${URI_B}" &> "$LAST_OUTPUT" \ + || exit_fail "Expected success, got: $(cat "$LAST_OUTPUT")" +echo " OK" + +exit 0 diff --git a/src/challengerdb/update_client.c b/src/challengerdb/update_client.c @@ -29,7 +29,8 @@ enum GNUNET_DB_QueryStatus CHALLENGERDB_update_client (struct CHALLENGERDB_PostgresContext *ctx, uint64_t client_id, const char *client_redirect_uri, - const char *client_secret) + const char *client_secret, + bool *uri_conflict) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&client_id), @@ -39,14 +40,62 @@ CHALLENGERDB_update_client (struct CHALLENGERDB_PostgresContext *ctx, : GNUNET_PQ_query_param_string (client_secret), GNUNET_PQ_query_param_end }; + bool updated; + bool conflict; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("updated", + &updated), + GNUNET_PQ_result_spec_bool ("uri_conflict", + &conflict), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + *uri_conflict = false; + /* "No rows updated" has two very different causes here: the client does + not exist, or another client already uses @a client_redirect_uri (which + violates clients_uri_key, and GNUnet maps a unique violation to + NO_RESULTS -- indistinguishable from "not found" at the API). So detect + the collision ourselves and refuse the UPDATE in that case, and report + which of the two happened. */ PREPARE (ctx, "update_client", - "UPDATE clients" - " SET uri=$2" - " ,client_secret=COALESCE($3,client_secret)" - " WHERE client_serial_id=$1"); - return GNUNET_PQ_eval_prepared_non_select (ctx->conn, - "update_client", - params); + "WITH target AS (" + " SELECT client_serial_id" + " FROM clients" + " WHERE client_serial_id=$1" + "), taken AS (" + " SELECT client_serial_id" + " FROM clients" + " WHERE uri=$2" + " AND client_serial_id<>$1" + "), upd AS (" + " UPDATE clients" + " SET uri=$2" + " ,client_secret=COALESCE($3,client_secret)" + " WHERE client_serial_id=$1" + " AND NOT EXISTS (SELECT 1 FROM taken)" + " RETURNING client_serial_id" + ") SELECT" + " EXISTS (SELECT 1 FROM upd) AS updated" + " ,( EXISTS (SELECT 1 FROM target)" + " AND EXISTS (SELECT 1 FROM taken) ) AS uri_conflict;"); + qs = GNUNET_PQ_eval_prepared_singleton_select (ctx->conn, + "update_client", + params, + rs); + if (0 > qs) + return qs; + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* Our SELECT always yields exactly one row, so zero rows can only be a + unique violation on clients.uri: another transaction inserted the URI + after our snapshot was taken. */ + *uri_conflict = true; + return qs; + } + *uri_conflict = conflict; + return updated + ? GNUNET_DB_STATUS_SUCCESS_ONE_RESULT + : GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; } diff --git a/src/include/challenger-database/update_client.h b/src/include/challenger-database/update_client.h @@ -32,13 +32,23 @@ struct CHALLENGERDB_PostgresContext; * @param client_id the client ID on success * @param client_url URL of the client * @param client_secret authorization secret for the client, NULL to not modify the secret - * @return transaction status + * @param[out] uri_conflict set to true if @a client_url is already used by + * another client; only meaningful if the return value is + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, where it tells the two causes + * apart: true means the URI is taken, false that there is no client + * with row @a client_id + * @return transaction status: + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if the client was modified + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the client does not exist or + * @a client_url is already used by another client (see @a uri_conflict) + * #GNUNET_DB_STATUS_HARD_ERROR on failure */ enum GNUNET_DB_QueryStatus CHALLENGERDB_update_client (struct CHALLENGERDB_PostgresContext *ctx, uint64_t client_id, const char *client_url, - const char *client_secret); + const char *client_secret, + bool *uri_conflict); #endif