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 + ## 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 ++ ++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 <{'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