since-v16.diff.txt

text/plain

Filename: since-v16.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
 1:  dc523009f2 =  1:  00976d4f75 common/jsonapi: support FRONTEND clients
 -:  ---------- >  2:  d8b567dd55 Refactor SASL exchange to return tri-state status
 -:  ---------- >  3:  83d78f598c Explicitly require password for SCRAM exchange
 2:  af969e6cea !  4:  00c8073807 libpq: add OAUTHBEARER SASL mechanism
    @@ src/interfaces/libpq/fe-auth-oauth.h (new)
     +#endif							/* FE_AUTH_OAUTH_H */
     
      ## src/interfaces/libpq/fe-auth-sasl.h ##
    -@@
    - 
    - #include "libpq-fe.h"
    - 
    -+/* See pg_fe_sasl_mech.exchange(). */
    -+typedef enum
    -+{
    -+	SASL_COMPLETE,
    -+	SASL_FAILED,
    -+	SASL_CONTINUE,
    +@@ src/interfaces/libpq/fe-auth-sasl.h: typedef enum
    + 	SASL_COMPLETE = 0,
    + 	SASL_FAILED,
    + 	SASL_CONTINUE,
     +	SASL_ASYNC,
    -+} SASLStatus;
    -+
    + } SASLStatus;
    + 
      /*
    -  * Frontend SASL mechanism callbacks.
    -  *
     @@ src/interfaces/libpq/fe-auth-sasl.h: typedef struct pg_fe_sasl_mech
    - 	 * server response once at the start of the authentication exchange to
    - 	 * generate an initial response.
    - 	 *
    -+	 * Returns a SASLStatus:
    -+	 *
    -+	 *  SASL_CONTINUE: The output buffer is filled with a client response. An
    -+	 *				   additional server challenge is expected.
    -+	 *
    -+	 *  SASL_ASYNC:	   Some asynchronous processing external to the connection
    -+	 *				   needs to be done before a response can be generated. The
    -+	 *				   mechanism is responsible for setting up conn->async_auth
    -+	 *				   appropriately before returning.
    -+	 *
    -+	 *  SASL_COMPLETE: The SASL exchange has completed successfully.
    -+	 *
    -+	 *  SASL_FAILED:   The exchange has failed and the connection should be
    -+	 *				   dropped.
    -+	 *
    - 	 * Input parameters:
      	 *
      	 *	state:	   The opaque mechanism state returned by init()
      	 *
    @@ src/interfaces/libpq/fe-auth-sasl.h: typedef struct pg_fe_sasl_mech
      	 *			   the server expects the client to send a message to start
     @@ src/interfaces/libpq/fe-auth-sasl.h: typedef struct pg_fe_sasl_mech
      	 *
    - 	 *	output:	   A malloc'd buffer containing the client's response to
    - 	 *			   the server (can be empty), or NULL if the exchange should
    --	 *			   be aborted.  (*success should be set to false in the
    -+	 *			   be aborted.  (The callback should return SASL_FAILED in the
    - 	 *			   latter case.)
    - 	 *
    - 	 *	outputlen: The length (0 or higher) of the client response buffer,
    - 	 *			   ignored if output is NULL.
    --	 *
    --	 *	done:      Set to true if the SASL exchange should not continue,
    --	 *			   because the exchange is either complete or failed
    --	 *
    --	 *	success:   Set to true if the SASL exchange completed successfully.
    --	 *			   Ignored if *done is false.
    + 	 *	SASL_CONTINUE:	The output buffer is filled with a client response.
    + 	 *					Additional server challenge is expected
    ++	 *	SASL_ASYNC:		Some asynchronous processing external to the
    ++	 *					connection needs to be done before a response can be
    ++	 *					generated. The mechanism is responsible for setting up
    ++	 *					conn->async_auth appropriately before returning.
    + 	 *	SASL_COMPLETE:	The SASL exchange has completed successfully.
    +-	 *	SASL_FAILED:	The exchance has failed and the connection should be
    ++	 *	SASL_FAILED:	The exchange has failed and the connection should be
    + 	 *					dropped.
      	 *--------
      	 */
    --	void		(*exchange) (void *state, char *input, int inputlen,
    --							 char **output, int *outputlen,
    --							 bool *done, bool *success);
    +-	SASLStatus	(*exchange) (void *state, char *input, int inputlen,
     +	SASLStatus	(*exchange) (void *state, bool final,
     +							 char *input, int inputlen,
    -+							 char **output, int *outputlen);
    + 							 char **output, int *outputlen);
      
      	/*--------
    - 	 * channel_bound()
     
      ## src/interfaces/libpq/fe-auth-scram.c ##
     @@
      /* The exported SCRAM callback mechanism. */
      static void *scram_init(PGconn *conn, const char *password,
      						const char *sasl_mechanism);
    --static void scram_exchange(void *opaq, char *input, int inputlen,
    --						   char **output, int *outputlen,
    --						   bool *done, bool *success);
    +-static SASLStatus scram_exchange(void *opaq, char *input, int inputlen,
     +static SASLStatus scram_exchange(void *opaq, bool final,
     +								 char *input, int inputlen,
    -+								 char **output, int *outputlen);
    + 								 char **output, int *outputlen);
      static bool scram_channel_bound(void *opaq);
      static void scram_free(void *opaq);
    - 
     @@ src/interfaces/libpq/fe-auth-scram.c: scram_free(void *opaq)
    - /*
       * Exchange a SCRAM message with backend.
       */
    --static void
    + static SASLStatus
     -scram_exchange(void *opaq, char *input, int inputlen,
    --			   char **output, int *outputlen,
    --			   bool *done, bool *success)
    -+static SASLStatus
     +scram_exchange(void *opaq, bool final,
     +			   char *input, int inputlen,
    -+			   char **output, int *outputlen)
    + 			   char **output, int *outputlen)
      {
      	fe_scram_state *state = (fe_scram_state *) opaq;
    - 	PGconn	   *conn = state->conn;
    - 	const char *errstr = NULL;
    - 
    --	*done = false;
    --	*success = false;
    - 	*output = NULL;
    - 	*outputlen = 0;
    - 
    -@@ src/interfaces/libpq/fe-auth-scram.c: scram_exchange(void *opaq, char *input, int inputlen,
    - 		if (inputlen == 0)
    - 		{
    - 			libpq_append_conn_error(conn, "malformed SCRAM message (empty message)");
    --			goto error;
    -+			return SASL_FAILED;
    - 		}
    - 		if (inputlen != strlen(input))
    - 		{
    - 			libpq_append_conn_error(conn, "malformed SCRAM message (length mismatch)");
    --			goto error;
    -+			return SASL_FAILED;
    - 		}
    - 	}
    - 
    -@@ src/interfaces/libpq/fe-auth-scram.c: scram_exchange(void *opaq, char *input, int inputlen,
    - 			/* Begin the SCRAM handshake, by sending client nonce */
    - 			*output = build_client_first_message(state);
    - 			if (*output == NULL)
    --				goto error;
    -+				return SASL_FAILED;
    - 
    - 			*outputlen = strlen(*output);
    --			*done = false;
    - 			state->state = FE_SCRAM_NONCE_SENT;
    --			break;
    -+			return SASL_CONTINUE;
    - 
    - 		case FE_SCRAM_NONCE_SENT:
    - 			/* Receive salt and server nonce, send response. */
    - 			if (!read_server_first_message(state, input))
    --				goto error;
    -+				return SASL_FAILED;
    - 
    - 			*output = build_client_final_message(state);
    - 			if (*output == NULL)
    --				goto error;
    -+				return SASL_FAILED;
    - 
    - 			*outputlen = strlen(*output);
    --			*done = false;
    - 			state->state = FE_SCRAM_PROOF_SENT;
    --			break;
    -+			return SASL_CONTINUE;
    - 
    - 		case FE_SCRAM_PROOF_SENT:
    --			/* Receive server signature */
    --			if (!read_server_final_message(state, input))
    --				goto error;
    --
    --			/*
    --			 * Verify server signature, to make sure we're talking to the
    --			 * genuine server.
    --			 */
    --			if (!verify_server_signature(state, success, &errstr))
    --			{
    --				libpq_append_conn_error(conn, "could not verify server signature: %s", errstr);
    --				goto error;
    --			}
    --
    --			if (!*success)
    - 			{
    --				libpq_append_conn_error(conn, "incorrect server signature");
    -+				bool		match;
    -+
    -+				/* Receive server signature */
    -+				if (!read_server_final_message(state, input))
    -+					return SASL_FAILED;
    -+
    -+				/*
    -+				 * Verify server signature, to make sure we're talking to the
    -+				 * genuine server.
    -+				 */
    -+				if (!verify_server_signature(state, &match, &errstr))
    -+				{
    -+					libpq_append_conn_error(conn, "could not verify server signature: %s", errstr);
    -+					return SASL_FAILED;
    -+				}
    -+
    -+				if (!match)
    -+					libpq_append_conn_error(conn, "incorrect server signature");
    -+
    -+				state->state = FE_SCRAM_FINISHED;
    -+				state->conn->client_finished_auth = true;
    -+				return match ? SASL_COMPLETE : SASL_FAILED;
    - 			}
    --			*done = true;
    --			state->state = FE_SCRAM_FINISHED;
    --			state->conn->client_finished_auth = true;
    --			break;
    - 
    - 		default:
    - 			/* shouldn't happen */
    - 			libpq_append_conn_error(conn, "invalid SCRAM exchange state");
    --			goto error;
    -+			break;
    - 	}
    --	return;
    - 
    --error:
    --	*done = true;
    --	*success = false;
    -+	return SASL_FAILED;
    - }
    - 
    - /*
     
      ## src/interfaces/libpq/fe-auth.c ##
     @@
    @@ src/interfaces/libpq/fe-auth.c: pg_SSPI_startup(PGconn *conn, int use_negotiate,
      {
      	char	   *initialresponse = NULL;
      	int			initialresponselen;
    --	bool		done;
    --	bool		success;
      	const char *selected_mechanism;
      	PQExpBufferData mechanism_buf;
     -	char	   *password;
     +	char	   *password = NULL;
    -+	SASLStatus	status;
    + 	SASLStatus	status;
      
      	initPQExpBuffer(&mechanism_buf);
    - 
     @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
      		goto error;
      	}
    @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
      		libpq_append_conn_error(conn, "duplicate SASL authentication request");
      		goto error;
     @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
    - 	/*
    - 	 * Parse the list of SASL authentication mechanisms in the
    - 	 * AuthenticationSASL message, and select the best mechanism that we
    --	 * support.  SCRAM-SHA-256-PLUS and SCRAM-SHA-256 are the only ones
    --	 * supported at the moment, listed by order of decreasing importance.
    -+	 * support.  Mechanisms are listed by order of decreasing importance.
    - 	 */
    - 	selected_mechanism = NULL;
    - 	for (;;)
    -@@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
    - 				{
    - 					selected_mechanism = SCRAM_SHA_256_PLUS_NAME;
    - 					conn->sasl = &pg_scram_mech;
    -+					conn->password_needed = true;
    - 				}
    - #else
    - 				/*
    -@@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
    - 		{
    - 			selected_mechanism = SCRAM_SHA_256_NAME;
      			conn->sasl = &pg_scram_mech;
    -+			conn->password_needed = true;
    + 			conn->password_needed = true;
      		}
     +#ifdef USE_OAUTH
     +		else if (strcmp(mechanism_buf.data, OAUTHBEARER_NAME) == 0 &&
    @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
      	if (!selected_mechanism)
     @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
      
    - 	/*
    - 	 * First, select the password to use for the exchange, complaining if
    --	 * there isn't one.  Currently, all supported SASL mechanisms require a
    --	 * password, so we can just go ahead here without further distinction.
    -+	 * there isn't one and the SASL mechanism needs it.
    - 	 */
    --	conn->password_needed = true;
    --	password = conn->connhost[conn->whichhost].password;
    --	if (password == NULL)
    --		password = conn->pgpass;
    --	if (password == NULL || password[0] == '\0')
    -+	if (conn->password_needed)
    - 	{
    --		appendPQExpBufferStr(&conn->errorMessage,
    --							 PQnoPasswordSupplied);
    --		goto error;
    -+		password = conn->connhost[conn->whichhost].password;
    -+		if (password == NULL)
    -+			password = conn->pgpass;
    -+		if (password == NULL || password[0] == '\0')
    -+		{
    -+			appendPQExpBufferStr(&conn->errorMessage,
    -+								 PQnoPasswordSupplied);
    -+			goto error;
    -+		}
    - 	}
    - 
      	Assert(conn->sasl);
      
     -	/*
    @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
     +	}
      
      	/* Get the mechanism-specific Initial Client Response, if any */
    --	conn->sasl->exchange(conn->sasl_state,
    --						 NULL, -1,
    --						 &initialresponse, &initialresponselen,
    --						 &done, &success);
    +-	status = conn->sasl->exchange(conn->sasl_state,
     +	status = conn->sasl->exchange(conn->sasl_state, false,
    -+								  NULL, -1,
    -+								  &initialresponse, &initialresponselen);
    + 								  NULL, -1,
    + 								  &initialresponse, &initialresponselen);
      
    --	if (done && !success)
    -+	if (status == SASL_FAILED)
    + 	if (status == SASL_FAILED)
      		goto error;
      
     +	if (status == SASL_ASYNC)
    @@ src/interfaces/libpq/fe-auth.c: oom_error:
      {
      	char	   *output;
      	int			outputlen;
    --	bool		done;
    --	bool		success;
    - 	int			res;
    - 	char	   *challenge;
    -+	SASLStatus	status;
    - 
    - 	/* Read the SASL challenge from the AuthenticationSASLContinue message. */
    - 	challenge = malloc(payloadlen + 1);
     @@ src/interfaces/libpq/fe-auth.c: pg_SASL_continue(PGconn *conn, int payloadlen, bool final)
      	/* For safety and convenience, ensure the buffer is NULL-terminated. */
      	challenge[payloadlen] = '\0';
      
    --	conn->sasl->exchange(conn->sasl_state,
    --						 challenge, payloadlen,
    --						 &output, &outputlen,
    --						 &done, &success);
    +-	status = conn->sasl->exchange(conn->sasl_state,
     +	status = conn->sasl->exchange(conn->sasl_state, final,
    -+								  challenge, payloadlen,
    -+								  &output, &outputlen);
    + 								  challenge, payloadlen,
    + 								  &output, &outputlen);
      	free(challenge);			/* don't need the input anymore */
      
    --	if (final && !done)
     +	if (status == SASL_ASYNC)
     +	{
     +		/*
    @@ src/interfaces/libpq/fe-auth.c: pg_SASL_continue(PGconn *conn, int payloadlen, b
     +		return STATUS_OK;
     +	}
     +
    -+	if (final && !(status == SASL_FAILED || status == SASL_COMPLETE))
    + 	if (final && !(status == SASL_FAILED || status == SASL_COMPLETE))
      	{
      		if (outputlen != 0)
    - 			free(output);
    -@@ src/interfaces/libpq/fe-auth.c: pg_SASL_continue(PGconn *conn, int payloadlen, bool final)
    - 	 * If the exchange is not completed yet, we need to make sure that the
    - 	 * SASL mechanism has generated a message to send back.
    - 	 */
    --	if (output == NULL && !done)
    -+	if (output == NULL && status == SASL_CONTINUE)
    - 	{
    - 		libpq_append_conn_error(conn, "no client response found after SASL exchange success");
    - 		return STATUS_ERROR;
    -@@ src/interfaces/libpq/fe-auth.c: pg_SASL_continue(PGconn *conn, int payloadlen, bool final)
    - 			return STATUS_ERROR;
    - 	}
    - 
    --	if (done && !success)
    -+	if (status == SASL_FAILED)
    - 		return STATUS_ERROR;
    - 
    - 	return STATUS_OK;
     @@ src/interfaces/libpq/fe-auth.c: check_expected_areq(AuthRequest areq, PGconn *conn)
       * it. We are responsible for reading any remaining extra data, specific
       * to the authentication method. 'payloadlen' is the remaining length in
    @@ src/tools/pgindent/typedefs.list: PQArgBlock
      PQsslKeyPassHook_OpenSSL_type
      PREDICATELOCK
      PREDICATELOCKTAG
    -@@ src/tools/pgindent/typedefs.list: RuleLock
    - RuleStmt
    - RunningTransactions
    - RunningTransactionsData
    -+SASLStatus
    - SC_HANDLE
    - SECURITY_ATTRIBUTES
    - SECURITY_STATUS
     @@ src/tools/pgindent/typedefs.list: explain_get_index_name_hook_type
      f_smgr
      fasthash_state
 3:  8906c9d445 !  5:  29d7e3cbed backend: add OAUTHBEARER SASL mechanism
    @@ src/backend/libpq/auth-oauth.c (new)
     +	 *
     +	 * TODO: handle the Authorization spec, RFC 7235 Sec. 2.1.
     +	 */
    -+	if (strncasecmp(auth, BEARER_SCHEME, strlen(BEARER_SCHEME)))
    ++	if (pg_strncasecmp(auth, BEARER_SCHEME, strlen(BEARER_SCHEME)))
     +		return false;
     +
     +	/* Pull the bearer token out of the auth value. */
 4:  e2566ab594 !  6:  0661817808 Introduce OAuth validator libraries
    @@ Commit message
         for loading in extensions for validating bearer tokens. A lot of
         code is left to be written.
     
    +    Co-authored-by: Jacob Champion <jacob.champion@enterprisedb.com>
    +
      ## src/backend/libpq/auth-oauth.c ##
     @@
       * See the following RFC for more details:
    @@ src/backend/libpq/auth-oauth.c: generate_error_response(struct oauth_ctx *ctx, c
      	*outputlen = buf.len;
      }
      
    +-static bool
    +-validate(Port *port, const char *auth, const char **logdetail)
     +/*-----
    ++ * Validates the provided Authorization header and returns the token from
    ++ * within it. NULL is returned on validation failure.
    ++ *
     + * Only Bearer tokens are accepted. The ABNF is defined in RFC 6750, Sec.
     + * 2.1:
     + *
    @@ src/backend/libpq/auth-oauth.c: generate_error_response(struct oauth_ctx *ctx, c
     + *
     + * TODO: handle the Authorization spec, RFC 7235 Sec. 2.1.
     + */
    - static bool
    --validate(Port *port, const char *auth, const char **logdetail)
    ++static const char *
     +validate_token_format(const char *header)
      {
     -	static const char *const b64_set =
    @@ src/backend/libpq/auth-oauth.c: generate_error_response(struct oauth_ctx *ctx, c
     -	const char *token;
     -	size_t		span;
     -	int			ret;
    --
    --	/* TODO: handle logdetail when the test framework can check it */
     +	/* If the token is empty or simply too short to be correct */
     +	if (!header || strlen(header) <= 7)
     +	{
     +		ereport(COMMERROR,
     +				(errmsg("malformed OAuth bearer token 1")));
    -+		return false;
    ++		return NULL;
     +	}
      
    +-	/* TODO: handle logdetail when the test framework can check it */
    +-
     -	/*-----
     -	 * Only Bearer tokens are accepted. The ABNF is defined in RFC 6750, Sec.
     -	 * 2.1:
    @@ src/backend/libpq/auth-oauth.c: generate_error_response(struct oauth_ctx *ctx, c
     -	 *
     -	 * TODO: handle the Authorization spec, RFC 7235 Sec. 2.1.
     -	 */
    --	if (strncasecmp(auth, BEARER_SCHEME, strlen(BEARER_SCHEME)))
    -+	if (strncasecmp(header, BEARER_SCHEME, strlen(BEARER_SCHEME)))
    - 		return false;
    +-	if (pg_strncasecmp(auth, BEARER_SCHEME, strlen(BEARER_SCHEME)))
    +-		return false;
    ++	if (pg_strncasecmp(header, BEARER_SCHEME, strlen(BEARER_SCHEME)))
    ++		return NULL;
      
      	/* Pull the bearer token out of the auth value. */
     -	token = auth + strlen(BEARER_SCHEME);
    @@ src/backend/libpq/auth-oauth.c: generate_error_response(struct oauth_ctx *ctx, c
     -				 errmsg("malformed OAUTHBEARER message"),
     +				 errmsg("malformed OAuth bearer token 2"),
      				 errdetail("Bearer token is empty.")));
    - 		return false;
    +-		return false;
    ++		return NULL;
      	}
    -@@ src/backend/libpq/auth-oauth.c: validate(Port *port, const char *auth, const char **logdetail)
    + 
    + 	/*
      	 * Make sure the token contains only allowed characters. Tokens may end
      	 * with any number of '=' characters.
      	 */
    @@ src/backend/libpq/auth-oauth.c: validate(Port *port, const char *auth, const cha
     -				 errmsg("malformed OAUTHBEARER message"),
     +				 errmsg("malformed OAuth bearer token 3"),
      				 errdetail("Bearer token is not in the correct format.")));
    - 		return false;
    +-		return false;
    ++		return NULL;
      	}
      
     -	/* Have the validator check the token. */
     -	if (!run_validator_command(port, token))
    -+	return true;
    ++	return token;
     +}
     +
     +static bool
     +validate(Port *port, const char *auth)
     +{
    -+	int map_status;
    -+	ValidatorModuleResult	*ret;
    ++	int			map_status;
    ++	ValidatorModuleResult *ret;
    ++	const char *token;
     +
     +	/* Ensure that we have a correct token to validate */
    -+	if (!validate_token_format(auth))
    -+		return false;
    -+
    ++	if (!(token = validate_token_format(auth)))
    + 		return false;
    + 
     +	/* Call the validation function from the validator module */
     +	ret = ValidatorCallbacks->validate_cb(validator_module_state,
    -+										  auth + strlen(BEARER_SCHEME),
    -+										  port->user_name);
    ++										  token, port->user_name);
     +
     +	if (!ret->authenticated)
    - 		return false;
    - 
    ++		return false;
    ++
    ++	if (ret->authn_id)
    ++		set_authn_id(port, ret->authn_id);
    ++
      	if (port->hba->oauth_skip_usermap)
    + 	{
    + 		/*
     @@ src/backend/libpq/auth-oauth.c: validate(Port *port, const char *auth, const char **logdetail)
      	}
      
    @@ src/backend/libpq/auth-oauth.c: validate(Port *port, const char *auth, const cha
      		/* TODO: use logdetail; reduce message duplication */
      		ereport(LOG,
     @@ src/backend/libpq/auth-oauth.c: validate(Port *port, const char *auth, const char **logdetail)
    - 		return false;
      	}
      
    -+	/* Set the authenticated identity from the validator module */
    -+	set_authn_id(port, ret->authn_id);
    -+
      	/* Finally, check the user map. */
     -	ret = check_usermap(port->hba->usermap, port->user_name,
    -+	map_status = check_usermap(port->hba->usermap, port->user_name,
    - 						MyClientConnectionInfo.authn_id, false);
    +-						MyClientConnectionInfo.authn_id, false);
     -	return (ret == STATUS_OK);
    ++	map_status = check_usermap(port->hba->usermap, port->user_name,
    ++							   MyClientConnectionInfo.authn_id, false);
     +	return (map_status == STATUS_OK);
      }
      
    @@ src/backend/libpq/auth-oauth.c: validate(Port *port, const char *auth, const cha
     +load_validator_library(void)
      {
     -	int			rc;
    --
    ++	OAuthValidatorModuleInit validator_init;
    + 
     -	rc = ClosePipeStream(*fh);
     -	*fh = NULL;
    -+	OAuthValidatorModuleInit	validator_init;
    - 
    +-
     -	if (rc == -1)
     -	{
     -		/* pclose() itself failed. */
    @@ src/backend/libpq/auth-oauth.c: validate(Port *port, const char *auth, const cha
     -	else if (rc != 0)
     -	{
     -		char	   *reason = wait_result_to_str(rc);
    +-
    +-		ereport(COMMERROR,
    +-				(errmsg("failed to execute command \"%s\": %s",
    +-						command, reason)));
    +-
    +-		pfree(reason);
    +-	}
     +	if (OAuthValidatorLibrary[0] == '\0')
     +		ereport(ERROR,
     +				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     +				 errmsg("oauth_validator_library is not set")));
      
    --		ereport(COMMERROR,
    --				(errmsg("failed to execute command \"%s\": %s",
    --						command, reason)));
    +-	return (rc == 0);
    +-}
     +	validator_init = (OAuthValidatorModuleInit)
     +		load_external_function(OAuthValidatorLibrary,
     +							   "_PG_oauth_validator_module_init", false, NULL);
      
    --		pfree(reason);
    --	}
    --
    --	return (rc == 0);
    --}
    -+	if (validator_init == NULL)
    -+		ereport(ERROR,
    -+				(errmsg("%s module \"%s\" have to define the symbol %s",
    -+						"OAuth validator", OAuthValidatorLibrary, "_PG_oauth_validator_module_init")));
    - 
     -static bool
     -set_cloexec(int fd)
     -{
     -	int			flags;
     -	int			rc;
    -+	ValidatorCallbacks = (*validator_init) ();
    ++	if (validator_init == NULL)
    ++		ereport(ERROR,
    ++				(errmsg("%s module \"%s\" have to define the symbol %s",
    ++						"OAuth validator", OAuthValidatorLibrary, "_PG_oauth_validator_module_init")));
      
     -	flags = fcntl(fd, F_GETFD);
     -	if (flags == -1)
    @@ src/backend/libpq/auth-oauth.c: validate(Port *port, const char *auth, const cha
     -				 errmsg("could not get fd flags for child pipe: %m")));
     -		return false;
     -	}
    -+	validator_module_state = (ValidatorModuleState *) palloc0(sizeof(ValidatorModuleState));
    -+	if (ValidatorCallbacks->startup_cb != NULL)
    -+		ValidatorCallbacks->startup_cb(validator_module_state);
    ++	ValidatorCallbacks = (*validator_init) ();
      
     -	rc = fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
     -	if (rc < 0)
    @@ src/backend/libpq/auth-oauth.c: validate(Port *port, const char *auth, const cha
     -				 errmsg("could not set FD_CLOEXEC for child pipe: %m")));
     -		return false;
     -	}
    --
    ++	validator_module_state = (ValidatorModuleState *) palloc0(sizeof(ValidatorModuleState));
    ++	if (ValidatorCallbacks->startup_cb != NULL)
    ++		ValidatorCallbacks->startup_cb(validator_module_state);
    + 
     -	return true;
     +	before_shmem_exit(shutdown_validator_library, 0);
      }
    @@ src/include/libpq/oauth.h
      /* Implementation */
      extern const pg_be_sasl_mech pg_be_oauth_mech;
     
    + ## src/test/modules/meson.build ##
    +@@ src/test/modules/meson.build: subdir('gin')
    + subdir('injection_points')
    + subdir('ldap_password_func')
    + subdir('libpq_pipeline')
    ++subdir('oauth_validator')
    + subdir('plsample')
    + subdir('spgist_name_ops')
    + subdir('ssl_passphrase_callback')
    +
      ## src/test/modules/oauth_validator/.gitignore (new) ##
     @@
     +# Generated subdirectories
    @@ src/test/modules/oauth_validator/expected/validator.out (new)
     +(1 row)
     +
     
    + ## src/test/modules/oauth_validator/meson.build (new) ##
    +@@
    ++# Copyright (c) 2024, PostgreSQL Global Development Group
    ++
    ++validator_sources = files(
    ++  'validator.c',
    ++)
    ++
    ++if host_system == 'windows'
    ++  validator_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
    ++    '--NAME', 'validator',
    ++    '--FILEDESC', 'validator - test OAuth validator module',])
    ++endif
    ++
    ++validator = shared_module('validator',
    ++  validator_sources,
    ++  kwargs: pg_test_mod_args,
    ++)
    ++test_install_libs += validator
    ++
    ++tests += {
    ++  'name': 'oauth_validator',
    ++  'sd': meson.current_source_dir(),
    ++  'bd': meson.current_build_dir(),
    ++  'regress': {
    ++    'sql': [
    ++      'validator',
    ++    ],
    ++  },
    ++  'tap': {
    ++    'tests': [
    ++      't/001_server.pl',
    ++    ],
    ++  },
    ++}
    +
      ## src/test/modules/oauth_validator/sql/validator.sql (new) ##
     @@
     +SELECT 1;
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +$node->append_conf('postgresql.conf', "oauth_validator_library = 'validator'\n");
     +$node->start;
     +
    -+reset_pg_hba($node, 'all', 'all', 'oauth issuer="127.0.0.1" scope="openid postgres"');
    ++reset_pg_hba($node, 'all', 'all', 'oauth issuer="127.0.0.1:18080" scope="openid postgres"');
     +
    -+my $webserver = PostgreSQL::Test::OAuthServer->new(80);
    ++my $webserver = PostgreSQL::Test::OAuthServer->new(18080);
     +
     +my $port = $webserver->port();
     +
    -+is($port, 80, "Port is 80");
    ++is($port, 18080, "Port is 18080");
     +
     +$webserver->setup();
     +$webserver->run();
     +
    -+$node->connect_ok("dbname=postgres oauth_client_id=f02c6361-0635", "connect");
    ++$node->connect_ok("dbname=postgres oauth_client_id=f02c6361-0635", "connect",
    ++				  expected_stderr => qr@Visit https://example\.com/ and enter the code: postgresuser@);
     +
     +$node->stop;
     +
    @@ src/test/modules/oauth_validator/validator.c (new)
     +
     +static void validator_startup(ValidatorModuleState *state);
     +static void validator_shutdown(ValidatorModuleState *state);
    -+static ValidatorModuleResult *validate_token(ValidatorModuleState *state,
    -+											 const char *token,
    -+											 const char *role);
    ++static ValidatorModuleResult * validate_token(ValidatorModuleState *state,
    ++											  const char *token,
    ++											  const char *role);
     +
     +static const OAuthValidatorCallbacks validator_callbacks = {
     +	.startup_cb = validator_startup,
    @@ src/test/modules/oauth_validator/validator.c (new)
     +	return res;
     +}
     
    + ## src/test/perl/PostgreSQL/Test/Cluster.pm ##
    +@@ src/test/perl/PostgreSQL/Test/Cluster.pm: instead of the default.
    + 
    + If this regular expression is set, matches it with the output generated.
    + 
    ++=item expected_stderr => B<value>
    ++
    ++If this regular expression is set, matches it against the standard error
    ++stream; otherwise the stderr must be empty.
    ++
    + =item log_like => [ qr/required message/ ]
    + 
    + =item log_unlike => [ qr/prohibited message/ ]
    +@@ src/test/perl/PostgreSQL/Test/Cluster.pm: sub connect_ok
    + 		like($stdout, $params{expected_stdout}, "$test_name: stdout matches");
    + 	}
    + 
    +-	is($stderr, "", "$test_name: no stderr");
    ++	if (defined($params{expected_stderr}))
    ++	{
    ++		like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
    ++	}
    ++	else
    ++	{
    ++		is($stderr, "", "$test_name: no stderr");
    ++	}
    + 
    + 	$self->log_check($test_name, $log_location, %params);
    + }
    +
      ## src/test/perl/PostgreSQL/Test/OAuthServer.pm (new) ##
     @@
     +#!/usr/bin/perl
    @@ src/test/perl/PostgreSQL/Test/OAuthServer.pm (new)
     +			print $fh "\r\n";
     +			print $fh <<EOR;
     +			{
    -+				"issuer": "http://localhost:80",
    -+				"token_endpoint": "http://localhost:80/token",
    -+				"device_authorization_endpoint": "http://localhost:80/authorize",
    ++				"issuer": "http://localhost:$self->{'port'}",
    ++				"token_endpoint": "http://localhost:$self->{'port'}/token",
    ++				"device_authorization_endpoint": "http://localhost:$self->{'port'}/authorize",
     +				"response_types_supported": ["token"],
     +				"subject_types_supported": ["public"],
     +				"id_token_signing_alg_values_supported": ["RS256"],
    @@ src/test/perl/PostgreSQL/Test/OAuthServer.pm (new)
     +}
     +
     +1;
    +
    + ## src/tools/pgindent/typedefs.list ##
    +@@ src/tools/pgindent/typedefs.list: NumericSortSupport
    + NumericSumAccum
    + NumericVar
    + OAuthStep
    ++OAuthValidatorCallbacks
    + OM_uint32
    + OP
    + OSAPerGroupState
    +@@ src/tools/pgindent/typedefs.list: VacuumRelation
    + VacuumStmt
    + ValidIOData
    + ValidateIndexState
    ++ValidatorModuleState
    + ValuesScan
    + ValuesScanState
    + Var
 5:  26781a7f15 <  -:  ---------- squash! Introduce OAuth validator libraries
 6:  295de92a5a !  7:  45755e8461 Add pytest suite for OAuth
    @@ Metadata
      ## Commit message ##
         Add pytest suite for OAuth
     
    -    Requires Python 3; on the first run of `make installcheck` the
    -    dependencies will be installed into ./venv for you. See the README for
    -    more details.
    +    Requires Python 3. On the first run of `make installcheck` or `meson
    +    test` the dependencies will be installed into a local virtualenv for
    +    you. See the README for more details.
    +
    +    Cirrus has been updated to build OAuth support on Debian and FreeBSD.
    +
    +    The suite contains a --temp-instance option, analogous to pg_regress's
    +    option of the same name, which allows an ephemeral server to be spun up
    +    during a test run.
     
         For iddawc, asynchronous tests still hang, as expected. Bad-interval
         tests fail because iddawc apparently doesn't care that the interval is
         bad.
     
    +    TODOs:
    +    - The --tap-stream option to pytest-tap is slightly broken during test
    +      failures (it suppresses error information), which impedes debugging.
    +    - Unsurprisingly, Windows builds fail on the Linux-/BSD-specific backend
    +      changes. 32-bit builds on Ubuntu fail during testing as well.
    +    - pyca/cryptography is pinned at an old version. Since we use it for
    +      testing and not security, this isn't a critical problem yet, but it's
    +      not ideal. Newer versions require a Rust compiler to build, and while
    +      many platforms have precompiled wheels, some (FreeBSD) do not. Even
    +      with the Rust pieces bypassed, compilation on FreeBSD takes a while.
    +    - The with_oauth test skip logic should probably be integrated into the
    +      Makefile side as well...
    +
    + ## .cirrus.tasks.yml ##
    +@@ .cirrus.tasks.yml: env:
    +   MTEST_ARGS: --print-errorlogs --no-rebuild -C build
    +   PGCTLTIMEOUT: 120 # avoids spurious failures during parallel tests
    +   TEMP_CONFIG: ${CIRRUS_WORKING_DIR}/src/tools/ci/pg_ci_base.conf
    +-  PG_TEST_EXTRA: kerberos ldap ssl load_balance
    ++  PG_TEST_EXTRA: kerberos ldap ssl load_balance python
    + 
    + 
    + # What files to preserve in case tests fail
    +@@ .cirrus.tasks.yml: task:
    +     chown root:postgres /tmp/cores
    +     sysctl kern.corefile='/tmp/cores/%N.%P.core'
    +   setup_additional_packages_script: |
    +-    #pkg install -y ...
    ++    pkg install -y curl
    + 
    +   # NB: Intentionally build without -Dllvm. The freebsd image size is already
    +   # large enough to make VM startup slow, and even without llvm freebsd
    +@@ .cirrus.tasks.yml: task:
    +         -Dcassert=true -Dinjection_points=true \
    +         -Duuid=bsd -Dtcl_version=tcl86 -Ddtrace=auto \
    +         -DPG_TEST_EXTRA="$PG_TEST_EXTRA" \
    ++        -Doauth=curl \
    +         -Dextra_lib_dirs=/usr/local/lib -Dextra_include_dirs=/usr/local/include/ \
    +         build
    +     EOF
    +@@ .cirrus.tasks.yml: LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >-
    +   --with-libxslt
    +   --with-llvm
    +   --with-lz4
    ++  --with-oauth=curl
    +   --with-pam
    +   --with-perl
    +   --with-python
    +@@ .cirrus.tasks.yml: LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >-
    + 
    + LINUX_MESON_FEATURES: &LINUX_MESON_FEATURES >-
    +   -Dllvm=enabled
    ++  -Doauth=curl
    +   -Duuid=e2fs
    + 
    + 
    +@@ .cirrus.tasks.yml: task:
    +     EOF
    + 
    +   setup_additional_packages_script: |
    +-    #apt-get update
    +-    #DEBIAN_FRONTEND=noninteractive apt-get -y install ...
    ++    apt-get update
    ++    DEBIAN_FRONTEND=noninteractive apt-get -y install \
    ++      libcurl4-openssl-dev \
    ++      libcurl4-openssl-dev:i386 \
    ++      python3-venv \
    + 
    +   matrix:
    +     - name: Linux - Debian Bullseye - Autoconf
    +@@ .cirrus.tasks.yml: task:
    +     folder: $CCACHE_DIR
    + 
    +   setup_additional_packages_script: |
    +-    #apt-get update
    +-    #DEBIAN_FRONTEND=noninteractive apt-get -y install ...
    ++    apt-get update
    ++    DEBIAN_FRONTEND=noninteractive apt-get -y install libcurl4-openssl-dev
    + 
    +   ###
    +   # Test that code can be built with gcc/clang without warnings
    +
    + ## meson.build ##
    +@@ meson.build: else
    + endif
    + 
    + testwrap = files('src/tools/testwrap')
    ++make_venv = files('src/tools/make_venv')
    ++
    ++checked_working_venv = false
    + 
    + foreach test_dir : tests
    +   testwrap_base = [
    +@@ meson.build: foreach test_dir : tests
    +         )
    +       endforeach
    +       install_suites += test_group
    ++    elif kind == 'pytest'
    ++      venv_name = test_dir['name'] + '_venv'
    ++      venv_path = meson.build_root() / venv_name
    ++
    ++      # The Python tests require a working venv module. This is part of the
    ++      # standard library, but some platforms disable it until a separate package
    ++      # is installed. Those same platforms don't provide an easy way to check
    ++      # whether the venv command will work until the first time you try it, so
    ++      # we decide whether or not to enable these tests on the fly.
    ++      if not checked_working_venv
    ++        cmd = run_command(python, '-m', 'venv', venv_path, check: false)
    ++
    ++        have_working_venv = (cmd.returncode() == 0)
    ++        if not have_working_venv
    ++          warning('A working Python venv module is required to run Python tests.')
    ++        endif
    ++
    ++        checked_working_venv = true
    ++      endif
    ++
    ++      if not have_working_venv
    ++        continue
    ++      endif
    ++
    ++      # Make sure the temporary installation is in PATH (necessary both for
    ++      # --temp-instance and for any pip modules compiling against libpq, like
    ++      # psycopg2).
    ++      env = test_env
    ++      env.prepend('PATH', temp_install_bindir, test_dir['bd'])
    ++
    ++      foreach name, value : t.get('env', {})
    ++        env.set(name, value)
    ++      endforeach
    ++
    ++      reqs = files(t['requirements'])
    ++      test('install_' + venv_name,
    ++        python,
    ++        args: [ make_venv, '--requirements', reqs, venv_path ],
    ++        env: env,
    ++        priority: setup_tests_priority - 1,  # must run after tmp_install
    ++        is_parallel: false,
    ++        suite: ['setup'],
    ++        timeout: 60,  # 30s is too short for the cryptography package compile
    ++      )
    ++
    ++      test_group = test_dir['name']
    ++      test_output = test_result_dir / test_group / kind
    ++      test_kwargs = {
    ++        #'protocol': 'tap',
    ++        'suite': test_group,
    ++        'timeout': 1000,
    ++        'depends': test_deps,
    ++        'env': env,
    ++      }
    ++
    ++      pytest = venv_path / 'bin' / 'py.test'
    ++      test_command = [
    ++        pytest,
    ++        # Avoid running these tests against an existing database.
    ++        '--temp-instance', test_output / 'data',
    ++
    ++        # FIXME pytest-tap's stream feature accidentally suppresses errors that
    ++        # are critical for debugging:
    ++        #     https://github.com/python-tap/pytest-tap/issues/30
    ++        # Don't use the meson TAP protocol for now...
    ++        #'--tap-stream',
    ++      ]
    ++
    ++      foreach pyt : t['tests']
    ++        # Similarly to TAP, strip ./ and .py to make the names prettier
    ++        pyt_p = pyt
    ++        if pyt_p.startswith('./')
    ++          pyt_p = pyt_p.split('./')[1]
    ++        endif
    ++        if pyt_p.endswith('.py')
    ++          pyt_p = fs.stem(pyt_p)
    ++        endif
    ++
    ++        test(test_group / pyt_p,
    ++          python,
    ++          kwargs: test_kwargs,
    ++          args: testwrap_base + [
    ++            '--testgroup', test_group,
    ++            '--testname', pyt_p,
    ++            '--', test_command,
    ++            test_dir['sd'] / pyt,
    ++          ],
    ++        )
    ++      endforeach
    ++      install_suites += test_group
    +     else
    +       error('unknown kind @0@ of test in @1@'.format(kind, test_dir['sd']))
    +     endif
    +
    + ## src/test/meson.build ##
    +@@ src/test/meson.build: subdir('authentication')
    + subdir('recovery')
    + subdir('subscription')
    + subdir('modules')
    ++subdir('python')
    + 
    + if ssl.found()
    +   subdir('ssl')
    +
      ## src/test/python/.gitignore (new) ##
     @@
     +__pycache__/
    @@ src/test/python/README (new)
     +
     +    export PGDATABASE=postgres
     +
    -+but you can adjust as needed for your setup.
    ++but you can adjust as needed for your setup. See also 'Advanced Usage' below.
     +
     +## Requirements
     +
    @@ src/test/python/README (new)
     +can skip them by saying e.g.
     +
     +    $ py.test -m 'not slow'
    ++
    ++If you'd rather not test against an existing server, you can have the suite spin
    ++up a temporary one using whatever pg_ctl it finds in PATH:
    ++
    ++    $ py.test --temp-instance=./tmp_check
     
      ## src/test/python/client/__init__.py (new) ##
     
    @@ src/test/python/client/test_oauth.py (new)
     +
     +from .conftest import BLOCKING_TIMEOUT
     +
    ++# The client tests need libpq to have been compiled with OAuth support; skip
    ++# them otherwise.
    ++pytestmark = pytest.mark.skipif(
    ++    os.getenv("with_oauth") == "none",
    ++    reason="OAuth client tests require --with-oauth support",
    ++)
    ++
     +if platform.system() == "Darwin":
     +    libpq = ctypes.cdll.LoadLibrary("libpq.5.dylib")
     +else:
    @@ src/test/python/conftest.py (new)
     +import pytest
     +
     +
    ++def pytest_addoption(parser):
    ++    """
    ++    Adds custom command line options to py.test. We add one to signal temporary
    ++    Postgres instance creation for the server tests.
    ++
    ++    Per pytest documentation, this must live in the top level test directory.
    ++    """
    ++    parser.addoption(
    ++        "--temp-instance",
    ++        metavar="DIR",
    ++        help="create a temporary Postgres instance in DIR",
    ++    )
    ++
    ++
     +@pytest.fixture(scope="session", autouse=True)
     +def _check_PG_TEST_EXTRA(request):
     +    """
    @@ src/test/python/conftest.py (new)
     +    if "python" not in extra_tests:
     +        pytest.skip("Potentially unsafe test 'python' not enabled in PG_TEST_EXTRA")
     
    + ## src/test/python/meson.build (new) ##
    +@@
    ++# Copyright (c) 2023, PostgreSQL Global Development Group
    ++
    ++subdir('server')
    ++
    ++pytest_env = {
    ++  'with_oauth': oauth_library,
    ++
    ++  # Point to the default database; the tests will create their own databases as
    ++  # needed.
    ++  'PGDATABASE': 'postgres',
    ++
    ++  # Avoid the need for a Rust compiler on platforms without prebuilt wheels for
    ++  # pyca/cryptography.
    ++  'CRYPTOGRAPHY_DONT_BUILD_RUST': '1',
    ++}
    ++
    ++# Some modules (psycopg2) need OpenSSL at compile time; for platforms where we
    ++# might have multiple implementations installed (macOS+brew), try to use the
    ++# same one that libpq is using.
    ++if ssl.found()
    ++  pytest_incdir = ssl.get_variable(pkgconfig: 'includedir', default_value: '')
    ++  if pytest_incdir != ''
    ++    pytest_env += { 'CPPFLAGS': '-I@0@'.format(pytest_incdir) }
    ++  endif
    ++
    ++  pytest_libdir = ssl.get_variable(pkgconfig: 'libdir', default_value: '')
    ++  if pytest_libdir != ''
    ++    pytest_env += { 'LDFLAGS': '-L@0@'.format(pytest_libdir) }
    ++  endif
    ++endif
    ++
    ++tests += {
    ++  'name': 'python',
    ++  'sd': meson.current_source_dir(),
    ++  'bd': meson.current_build_dir(),
    ++  'pytest': {
    ++	'requirements': meson.current_source_dir() / 'requirements.txt',
    ++    'tests': [
    ++      './client',
    ++      './server',
    ++      './test_internals.py',
    ++      './test_pq3.py',
    ++    ],
    ++    'env': pytest_env,
    ++  },
    ++}
    +
      ## src/test/python/pq3.py (new) ##
     @@
     +#
    @@ src/test/python/pytest.ini (new)
      ## src/test/python/requirements.txt (new) ##
     @@
     +black
    -+# cryptography 39.x removes a lot of platform support, beware
    -+cryptography~=38.0.4
    ++# cryptography 35.x and later add many platform/toolchain restrictions, beware
    ++cryptography~=3.4.8
     +construct~=2.10.61
     +isort~=5.6
     +# TODO: update to psycopg[c] 3.1
    -+psycopg2~=2.9.6
    ++psycopg2~=2.9.7
     +pytest~=7.3
     +pytest-asyncio~=0.21.0
     
    @@ src/test/python/server/__init__.py (new)
      ## src/test/python/server/conftest.py (new) ##
     @@
     +#
    -+# Copyright 2021 VMware, Inc.
    ++# Portions Copyright 2021 VMware, Inc.
    ++# Portions Copyright 2023 Timescale, Inc.
     +# SPDX-License-Identifier: PostgreSQL
     +#
     +
    ++import collections
     +import contextlib
    ++import os
    ++import shutil
     +import socket
    ++import subprocess
     +import sys
     +
     +import pytest
    @@ src/test/python/server/conftest.py (new)
     +BLOCKING_TIMEOUT = 2  # the number of seconds to wait for blocking calls
     +
     +
    ++def cleanup_prior_instance(datadir):
    ++    """
    ++    Clean up an existing data directory, but make sure it actually looks like a
    ++    data directory first. (Empty folders will remain untouched, since initdb can
    ++    populate them.)
    ++    """
    ++    required_entries = set(["base", "PG_VERSION", "postgresql.conf"])
    ++    empty = True
    ++
    ++    try:
    ++        with os.scandir(datadir) as entries:
    ++            for e in entries:
    ++                empty = False
    ++                required_entries.discard(e.name)
    ++
    ++    except FileNotFoundError:
    ++        return  # nothing to clean up
    ++
    ++    if empty:
    ++        return  # initdb can handle an empty datadir
    ++
    ++    if required_entries:
    ++        pytest.fail(
    ++            f"--temp-instance directory \"{datadir}\" is not empty and doesn't look like a data directory (missing {', '.join(required_entries)})"
    ++        )
    ++
    ++    # Okay, seems safe enough now.
    ++    shutil.rmtree(datadir)
    ++
    ++
    ++@pytest.fixture(scope="session")
    ++def postgres_instance(pytestconfig, unused_tcp_port_factory):
    ++    """
    ++    If --temp-instance has been passed to pytest, this fixture runs a temporary
    ++    Postgres instance on an available port. Otherwise, the fixture will attempt
    ++    to contact a running Postgres server on (PGHOST, PGPORT); dependent tests
    ++    will be skipped if the connection fails.
    ++
    ++    Yields a (host, port) tuple for connecting to the server.
    ++    """
    ++    PGInstance = collections.namedtuple("PGInstance", ["addr", "temporary"])
    ++
    ++    datadir = pytestconfig.getoption("temp_instance")
    ++    if datadir:
    ++        # We were told to create a temporary instance. Use pg_ctl to set it up
    ++        # on an unused port.
    ++        cleanup_prior_instance(datadir)
    ++        subprocess.run(["pg_ctl", "-D", datadir, "init"], check=True)
    ++
    ++        # The CI looks for *.log files to upload, so the file name here isn't
    ++        # completely arbitrary.
    ++        log = os.path.join(datadir, "postmaster.log")
    ++        port = unused_tcp_port_factory()
    ++
    ++        subprocess.run(
    ++            [
    ++                "pg_ctl",
    ++                "-D",
    ++                datadir,
    ++                "-l",
    ++                log,
    ++                "-o",
    ++                " ".join(
    ++                    [
    ++                        f"-c port={port}",
    ++                        "-c listen_addresses=localhost",
    ++                        "-c log_connections=on",
    ++                        "-c shared_preload_libraries=oauthtest",
    ++                        "-c oauth_validator_library=oauthtest",
    ++                    ]
    ++                ),
    ++                "start",
    ++            ],
    ++            check=True,
    ++        )
    ++
    ++        yield ("localhost", port)
    ++
    ++        subprocess.run(["pg_ctl", "-D", datadir, "stop"], check=True)
    ++
    ++    else:
    ++        # Try to contact an already running server; skip the suite if we can't
    ++        # find one.
    ++        addr = (pq3.pghost(), pq3.pgport())
    ++
    ++        try:
    ++            with socket.create_connection(addr, timeout=BLOCKING_TIMEOUT):
    ++                pass
    ++        except ConnectionError as e:
    ++            pytest.skip(f"unable to connect to Postgres server at {addr}: {e}")
    ++
    ++        yield addr
    ++
    ++
     +@pytest.fixture
    -+def connect():
    ++def connect(postgres_instance):
     +    """
     +    A factory fixture that, when called, returns a socket connected to a
    -+    Postgres server, wrapped in a pq3 connection. The calling test will be
    -+    skipped automatically if a server is not running at PGHOST:PGPORT, so it's
    -+    best to connect as soon as possible after the test case begins, to avoid
    -+    doing unnecessary work.
    ++    Postgres server, wrapped in a pq3 connection. Dependent tests will be
    ++    skipped if no server is available.
     +    """
    ++    addr = postgres_instance
    ++
     +    # Set up an ExitStack to handle safe cleanup of all of the moving pieces.
     +    with contextlib.ExitStack() as stack:
     +
     +        def conn_factory():
    -+            addr = (pq3.pghost(), pq3.pgport())
    -+
    -+            try:
    -+                sock = socket.create_connection(addr, timeout=BLOCKING_TIMEOUT)
    -+            except ConnectionError as e:
    -+                pytest.skip(f"unable to connect to {addr}: {e}")
    ++            sock = socket.create_connection(addr, timeout=BLOCKING_TIMEOUT)
     +
     +            # Have ExitStack close our socket.
     +            stack.enter_context(sock)
    @@ src/test/python/server/conftest.py (new)
     +
     +        yield conn_factory
     
    + ## src/test/python/server/meson.build (new) ##
    +@@
    ++# Copyright (c) 2024, PostgreSQL Global Development Group
    ++
    ++if not oauth.found()
    ++  subdir_done()
    ++endif
    ++
    ++oauthtest_sources = files(
    ++  'oauthtest.c',
    ++)
    ++
    ++if host_system == 'windows'
    ++  oauthtest_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
    ++    '--NAME', 'oauthtest',
    ++    '--FILEDESC', 'passthrough module to validate OAuth tests',
    ++  ])
    ++endif
    ++
    ++oauthtest = shared_module('oauthtest',
    ++  oauthtest_sources,
    ++  kwargs: pg_test_mod_args,
    ++)
    ++test_install_libs += oauthtest
    +
      ## src/test/python/server/oauthtest.c (new) ##
     @@
     +/*-------------------------------------------------------------------------
    @@ src/test/python/server/test_oauth.py (new)
     +MAX_UINT16 = 2**16 - 1
     +
     +
    -+def skip_if_no_postgres():
    -+    """
    -+    Used by the oauth_ctx fixture to skip this test module if no Postgres server
    -+    is running.
    -+
    -+    This logic is nearly duplicated with the conn fixture. Ideally oauth_ctx
    -+    would depend on that, but a module-scope fixture can't depend on a
    -+    test-scope fixture, and we haven't reached the rule of three yet.
    -+    """
    -+    addr = (pq3.pghost(), pq3.pgport())
    -+
    -+    try:
    -+        with socket.create_connection(addr, timeout=BLOCKING_TIMEOUT):
    -+            pass
    -+    except ConnectionError as e:
    -+        pytest.skip(f"unable to connect to {addr}: {e}")
    -+
    -+
     +@contextlib.contextmanager
     +def prepend_file(path, lines):
     +    """
    @@ src/test/python/server/test_oauth.py (new)
     +
     +
     +@pytest.fixture(scope="module")
    -+def oauth_ctx():
    ++def oauth_ctx(postgres_instance):
     +    """
     +    Creates a database and user that use the oauth auth method. The context
     +    object contains the dbname and user attributes as strings to be used during
    @@ src/test/python/server/test_oauth.py (new)
     +    server running on a local machine, and that the PGUSER has rights to create
     +    databases and roles.
     +    """
    -+    skip_if_no_postgres()  # don't bother running these tests without a server
    -+
     +    id = secrets.token_hex(4)
     +
     +    class Context:
    @@ src/test/python/server/test_oauth.py (new)
     +    )
     +    ident_lines = (r"oauth /^(.*)@example\.com$ \1",)
     +
    -+    conn = psycopg2.connect("")
    ++    host, port = postgres_instance
    ++    conn = psycopg2.connect(host=host, port=port)
     +    conn.autocommit = True
     +
     +    with contextlib.closing(conn):
    @@ src/test/python/server/test_oauth.py (new)
     +
     +
     +@pytest.fixture()
    -+def setup_validator():
    ++def setup_validator(postgres_instance):
     +    """
     +    A per-test fixture that sets up the test validator with expected behavior.
     +    The setting will be reverted during teardown.
     +    """
    -+    conn = psycopg2.connect("")
    ++    host, port = postgres_instance
    ++    conn = psycopg2.connect(host=host, port=port)
     +    conn.autocommit = True
     +
     +    with contextlib.closing(conn):
    @@ src/test/python/tls.py (new)
     +    "length" / Int16ub,
     +    "fragment" / FixedSized(this.length, GreedyBytes),
     +)
    +
    + ## src/tools/make_venv (new) ##
    +@@
    ++#!/usr/bin/env python3
    ++
    ++import argparse
    ++import subprocess
    ++import os
    ++import sys
    ++
    ++parser = argparse.ArgumentParser()
    ++
    ++parser.add_argument('--requirements', help='path to pip requirements file', type=str)
    ++parser.add_argument('--privatedir', help='private directory for target', type=str)
    ++parser.add_argument('venv_path', help='desired venv location')
    ++
    ++args = parser.parse_args()
    ++
    ++# Decide whether or not to capture stdout into a log file. We only do this if
    ++# we've been given our own private directory.
    ++#
    ++# FIXME Unfortunately this interferes with debugging on Cirrus, because the
    ++# private directory isn't uploaded in the sanity check's artifacts. When we
    ++# don't capture the log file, it gets spammed to stdout during build... Is there
    ++# a way to push this into the meson-log somehow? For now, the capture
    ++# implementation is commented out.
    ++logfile = None
    ++
    ++if args.privatedir:
    ++    if not os.path.isdir(args.privatedir):
    ++        os.mkdir(args.privatedir)
    ++
    ++    # FIXME see above comment
    ++    # logpath = os.path.join(args.privatedir, 'stdout.txt')
    ++    # logfile = open(logpath, 'w')
    ++
    ++def run(*args):
    ++    kwargs = dict(check=True)
    ++    if logfile:
    ++        kwargs.update(stdout=logfile)
    ++
    ++    subprocess.run(args, **kwargs)
    ++
    ++# Create the virtualenv first.
    ++run(sys.executable, '-m', 'venv', args.venv_path)
    ++
    ++# Update pip next. This helps avoid old pip bugs; the version inside system
    ++# Pythons tends to be pretty out of date.
    ++pip = os.path.join(args.venv_path, 'bin', 'pip')
    ++run(pip, 'install', '-U', 'pip')
    ++
    ++# Finally, install the test's requirements. We need pytest and pytest-tap, no
    ++# matter what the test needs.
    ++run(pip, 'install', 'pytest', 'pytest-tap')
    ++if args.requirements:
    ++    run(pip, 'install', '-r', args.requirements)
 7:  7d21be13c0 <  -:  ---------- squash! Add pytest suite for OAuth
 8:  26dcd5f828 !  8:  0f9f884856 XXX temporary patches to build and test
    @@ Commit message
         - the new pg_combinebackup utility uses JSON in the frontend without
           0001; has something changed?
         - construct 2.10.70 has some incompatibilities with the current tests
    +    - temporarily skip the exit check (from Daniel Gustafsson); this needs
    +      to be turned into an exception for curl rather than a plain exit call
     
      ## src/bin/pg_combinebackup/Makefile ##
     @@ src/bin/pg_combinebackup/Makefile: include $(top_builddir)/src/Makefile.global
    @@ src/bin/pg_verifybackup/Makefile: top_builddir = ../../..
      OBJS = \
      	$(WIN32RES) \
     
    + ## src/interfaces/libpq/Makefile ##
    +@@ src/interfaces/libpq/Makefile: libpq-refs-stamp: $(shlib)
    + ifneq ($(enable_coverage), yes)
    + ifeq (,$(filter aix solaris,$(PORTNAME)))
    + 	@if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \
    +-		echo 'libpq must not be calling any function which invokes exit'; exit 1; \
    ++		echo 'libpq must not be calling any function which invokes exit'; \
    + 	fi
    + endif
    + endif
    +
      ## src/test/python/requirements.txt ##
     @@
      black
 9:  0ff8e3786a <  -:  ---------- REVERT: temporarily skip the exit check
 -:  ---------- >  9:  de8f81bd7d WIP: Python OAuth provider implementation