From fb26bb8b21e2c873d5c4cfd8fd54be692e67aaff Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Wed, 1 Apr 2026 10:58:46 -0700
Subject: [PATCH v3.1 3/6] squash! Split PGOAUTHDEBUG=UNSAFE into multiple
 options

- Switch from a struct to bitflags. (OAUTHDEBUG_UNSAFE_MASK is
  introduced a commit early to avoid unnecessary work.)
---
 src/interfaces/libpq-oauth/oauth-utils.h     |  2 +-
 src/interfaces/libpq/fe-auth-oauth.h         | 39 +++++++++++++-------
 src/interfaces/libpq-oauth/oauth-curl.c      | 12 +++---
 src/interfaces/libpq-oauth/test-oauth-curl.c |  6 +--
 src/interfaces/libpq/fe-auth-oauth-debug.c   | 25 +++++--------
 src/interfaces/libpq/fe-auth-oauth.c         |  6 +--
 6 files changed, 46 insertions(+), 44 deletions(-)

diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
index dd4e38d525c..64a9235ee85 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.h
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -36,7 +36,7 @@ typedef enum
 	PG_BOOL_NO					/* No (false) */
 } PGTernaryBool;
 
-extern oauth_debug_flags oauth_get_debug_flags(void);
+extern uint32 oauth_get_debug_flags(void);
 extern int	pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
 extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe);
 
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 859975bcd67..a952fea09cf 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -40,23 +40,36 @@ typedef struct
 } fe_oauth_state;
 
 /*
- * Debug flags for PGOAUTHDEBUG environment variable.
- * Each flag controls a specific debug feature.
+ * Debug flags for the PGOAUTHDEBUG environment variable. Each flag controls a
+ * specific debug feature. OAUTHDEBUG_UNSAFE_* flags require the envvar to have
+ * a literal "UNSAFE:" prefix.
  */
-typedef struct oauth_debug_flags
-{
-	/* UNSAFE features - require UNSAFE: prefix */
-	bool		http;			/* allow HTTP (unencrypted) connections */
-	bool		trace;			/* log HTTP traffic (exposes secrets) */
 
-	/* SAFE features - allowed without UNSAFE: prefix */
-	bool		fast_retry;		/* allow zero-second retry intervals */
-	bool		poll_counts;	/* print poll() statistics */
-	bool		print_plugin_errors;	/* print plugin loading errors */
-} oauth_debug_flags;
+/* allow HTTP (unencrypted) connections */
+#define OAUTHDEBUG_UNSAFE_HTTP			(1<<0)
+/* log HTTP traffic (exposes secrets) */
+#define OAUTHDEBUG_UNSAFE_TRACE			(1<<1)
+/* allow zero-second retry intervals */
+#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT	(1<<2)
+
+/* mind the gap in values; see OAUTHDEBUG_UNSAFE_MASK below */
+
+/* print PQconnectPoll statistics */
+#define OAUTHDEBUG_CALL_COUNT			(1<<16)
+/* print plugin loading errors */
+#define OAUTHDEBUG_PLUGIN_ERRORS		(1<<17)
+
+/* all safe and unsafe flags, for the legacy UNSAFE behavior */
+#define OAUTHDEBUG_UNSAFE_ALL			((uint32) ~0)
+
+/* Flags are divided into "safe" and "unsafe" based on bit position. */
+#define OAUTHDEBUG_UNSAFE_MASK			((uint32) 0x0000FFFF)
+
+static_assert(OAUTHDEBUG_CALL_COUNT == OAUTHDEBUG_UNSAFE_MASK + 1,
+			  "the first safe OAUTHDEBUG flag should be above OAUTHDEBUG_UNSAFE_MASK");
 
 extern void pqClearOAuthToken(PGconn *conn);
-extern oauth_debug_flags oauth_get_debug_flags(void);
+extern uint32 oauth_get_debug_flags(void);
 
 /* Mechanisms in fe-auth-oauth.c */
 extern const pg_fe_sasl_mech pg_oauth_mech;
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 564d76cf063..7100824c560 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -274,7 +274,7 @@ struct async_ctx
 	int			running;		/* is asynchronous work in progress? */
 	bool		user_prompted;	/* have we already sent the authz prompt? */
 	bool		used_basic_auth;	/* did we send a client secret? */
-	oauth_debug_flags debug_flags;	/* can we give developer assistance */
+	uint32		debug_flags;	/* can we give developer assistance? */
 	int			dbg_num_calls;	/* (debug mode) how many times were we called? */
 };
 
@@ -1023,7 +1023,7 @@ parse_interval(struct async_ctx *actx, const char *interval_str)
 	parsed = ceil(parsed);
 
 	if (parsed < 1)
-		return actx->debug_flags.fast_retry ? 0 : 1;
+		return (actx->debug_flags & OAUTHDEBUG_UNSAFE_DOS_ENDPOINT) ? 0 : 1;
 
 	else if (parsed >= INT_MAX)
 		return INT_MAX;
@@ -1797,7 +1797,7 @@ setup_curl_handles(struct async_ctx *actx)
 	 */
 	CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
 
-	if (actx->debug_flags.trace)
+	if (actx->debug_flags & OAUTHDEBUG_UNSAFE_TRACE)
 	{
 		/*
 		 * Set a callback for retrieving error information from libcurl, the
@@ -1829,7 +1829,7 @@ setup_curl_handles(struct async_ctx *actx)
 		const long	unsafe = CURLPROTO_HTTPS | CURLPROTO_HTTP;
 #endif
 
-		if (actx->debug_flags.http)
+		if (actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP)
 			protos = unsafe;
 
 		CHECK_SETOPT(actx, popt, protos, return false);
@@ -2297,7 +2297,7 @@ check_for_device_flow(struct async_ctx *actx)
 	 * decent time to bail out if we're not using HTTPS for the endpoints
 	 * we'll use for the flow.
 	 */
-	if (!actx->debug_flags.http)
+	if ((actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP) == 0)
 	{
 		if (pg_strncasecmp(provider->device_authorization_endpoint,
 						   HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
@@ -3027,7 +3027,7 @@ pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request,
 	 * drain_timer_events(), when we're in debug mode, track the total number
 	 * of calls to this function and print that at the end of the flow.
 	 */
-	if (actx && actx->debug_flags.poll_counts)
+	if (actx->debug_flags & OAUTHDEBUG_CALL_COUNT)
 	{
 		actx->dbg_num_calls++;
 		if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
diff --git a/src/interfaces/libpq-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c
index 06815be9a0a..185c17e5807 100644
--- a/src/interfaces/libpq-oauth/test-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/test-oauth-curl.c
@@ -89,11 +89,7 @@ init_test_actx(void)
 
 	actx->mux = PGINVALID_SOCKET;
 	actx->timerfd = -1;
-	actx->debug_flags.http = true;
-	actx->debug_flags.trace = true;
-	actx->debug_flags.fast_retry = true;
-	actx->debug_flags.poll_counts = true;
-	actx->debug_flags.print_plugin_errors = true;
+	actx->debug_flags = OAUTHDEBUG_UNSAFE_ALL;
 
 	initPQExpBuffer(&actx->errbuf);
 
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
index 957da5d4068..c9a82b3f78e 100644
--- a/src/interfaces/libpq/fe-auth-oauth-debug.c
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -30,38 +30,38 @@
  * Sets *is_unsafe to indicate if this option requires the UNSAFE: prefix.
  */
 static bool
-parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe)
+parse_debug_option(const char *option, uint32 *flags, bool *is_unsafe)
 {
 	*is_unsafe = false;
 
 	/* Unsafe options */
 	if (strcmp(option, "http") == 0)
 	{
-		flags->http = true;
+		*flags |= OAUTHDEBUG_UNSAFE_HTTP;
 		*is_unsafe = true;
 		return true;
 	}
 	else if (strcmp(option, "trace") == 0)
 	{
-		flags->trace = true;
+		*flags |= OAUTHDEBUG_UNSAFE_TRACE;
 		*is_unsafe = true;
 		return true;
 	}
 	else if (strcmp(option, "dos-endpoint") == 0)
 	{
-		flags->fast_retry = true;
+		*flags |= OAUTHDEBUG_UNSAFE_DOS_ENDPOINT;
 		*is_unsafe = true;
 		return true;
 	}
 	/* Safe options */
 	else if (strcmp(option, "call-count") == 0)
 	{
-		flags->poll_counts = true;
+		*flags |= OAUTHDEBUG_CALL_COUNT;
 		return true;
 	}
 	else if (strcmp(option, "plugin-errors") == 0)
 	{
-		flags->print_plugin_errors = true;
+		*flags |= OAUTHDEBUG_PLUGIN_ERRORS;
 		return true;
 	}
 
@@ -80,10 +80,10 @@ parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe
  * - An unrecognized option is specified
  * - An unsafe option is specified without the UNSAFE: prefix
  */
-oauth_debug_flags
+uint32
 oauth_get_debug_flags(void)
 {
-	oauth_debug_flags flags = {0};
+	uint32		flags = 0;
 	const char *env = getenv("PGOAUTHDEBUG");
 	char	   *options_str;
 	char	   *option;
@@ -94,14 +94,7 @@ oauth_get_debug_flags(void)
 		return flags;
 
 	if (strcmp(env, "UNSAFE") == 0)
-	{
-		flags.http = true;
-		flags.trace = true;
-		flags.fast_retry = true;
-		flags.poll_counts = true;
-		flags.print_plugin_errors = true;
-		return flags;
-	}
+		return OAUTHDEBUG_UNSAFE_ALL;
 
 	if (strncmp(env, "UNSAFE:", 7) == 0)
 	{
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 5f5900a9ae7..6f7ec3a129e 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -389,7 +389,7 @@ issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
 		authority_start = wkuri + strlen(HTTPS_SCHEME);
 
 	if (!authority_start
-		&& oauth_get_debug_flags().http
+		&& (oauth_get_debug_flags() & OAUTHDEBUG_UNSAFE_HTTP)
 		&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
 	{
 		/* Allow http:// for testing only. */
@@ -900,7 +900,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
 		 *
 		 * Note that POSIX dlerror() isn't guaranteed to be threadsafe.
 		 */
-		if (oauth_get_debug_flags().print_plugin_errors)
+		if (oauth_get_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
 			fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
 
 		return 0;
@@ -922,7 +922,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
 		 * cause is still locked behind PGOAUTHDEBUG due to the dlerror()
 		 * threadsafety issue.
 		 */
-		if (oauth_get_debug_flags().print_plugin_errors)
+		if (oauth_get_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
 			fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
 
 		dlclose(state->flow_module);
-- 
2.34.1

