since-v16.diff.txt
text/plain
Filename: since-v16.diff.txt
Type: text/plain
Part: 0
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