since-v23.diff.txt
text/plain
Filename: since-v23.diff.txt
Type: text/plain
Part: 0
1: aa553b7700 = 1: 9fc1df7509 Revert ECPG's use of pnstrdup()
2: d6cae9157e = 2: fdd89bdee0 Remove fe_memutils from libpgcommon_shlib
3: 5543539169 = 3: ae3ae1cfaa common/jsonapi: support libpq as a client
4: 1639f7eb9a ! 4: 92b257643e libpq: add OAUTHBEARER SASL mechanism
@@ Commit message
- fix intermittent failure in the cleanup callback tests (race
condition?)
- support require_auth
+ - fill in documentation stubs
- ...and more.
Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
+ ## config/programs.m4 ##
+@@ config/programs.m4: if test "$pgac_cv_ldap_safe" != yes; then
+ *** also uses LDAP will crash on exit.])
+ fi])
+
++# PGAC_CHECK_LIBCURL
++# ------------------
++# Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability to
++# explicitly set TLS 1.3 ciphersuites).
+
++AC_DEFUN([PGAC_CHECK_LIBCURL],
++[AC_CACHE_CHECK([for compatible libcurl], [pgac_cv_check_libcurl],
++[AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
++[#include <curl/curlver.h>
++#if LIBCURL_VERSION_MAJOR < 7 || (LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR < 61)
++choke me
++#endif], [])],
++[pgac_cv_check_libcurl=yes],
++[pgac_cv_check_libcurl=no])])
++
++if test "$pgac_cv_check_libcurl" != yes; then
++ AC_MSG_ERROR([
++*** The installed version of libcurl is too old to use with PostgreSQL.
++*** libcurl version 7.61.0 or later is required.])
++fi])
+
+ # PGAC_CHECK_READLINE
+ # -------------------
+
## configure ##
@@ configure: with_uuid
with_readline
@@ configure: fi
+ as_fn_error $? "library 'curl' is required for --with-oauth=curl" "$LINENO" 5
+fi
+
++ { $as_echo "$as_me:${as_lineno-$LINENO}: checking for compatible libcurl" >&5
++$as_echo_n "checking for compatible libcurl... " >&6; }
++if ${pgac_cv_check_libcurl+:} false; then :
++ $as_echo_n "(cached) " >&6
++else
++ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
++/* end confdefs.h. */
++#include <curl/curlver.h>
++#if LIBCURL_VERSION_MAJOR < 7 || (LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR < 61)
++choke me
++#endif
++int
++main ()
++{
++
++ ;
++ return 0;
++}
++_ACEOF
++if ac_fn_c_try_compile "$LINENO"; then :
++ pgac_cv_check_libcurl=yes
++else
++ pgac_cv_check_libcurl=no
++fi
++rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
++fi
++{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_check_libcurl" >&5
++$as_echo "$pgac_cv_check_libcurl" >&6; }
++
++if test "$pgac_cv_check_libcurl" != yes; then
++ as_fn_error $? "
++*** The installed version of libcurl is too old to use with PostgreSQL.
++*** libcurl version 7.61.0 or later is required." "$LINENO" 5
++fi
+fi
+
# for contrib/sepgsql
@@ configure.ac: fi
+if test "$with_oauth" = curl ; then
+ AC_CHECK_LIB(curl, curl_multi_init, [], [AC_MSG_ERROR([library 'curl' is required for --with-oauth=curl])])
++ PGAC_CHECK_LIBCURL
+fi
+
# for contrib/sepgsql
@@ configure.ac: elif test "$with_uuid" = ossp ; then
AC_CHECK_HEADERS(crtdefs.h)
fi
+ ## doc/src/sgml/libpq.sgml ##
+@@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
+ </para>
+ </listitem>
+ </varlistentry>
++
++ <varlistentry id="libpq-connect-oauth-client-id" xreflabel="oauth_client_id">
++ <term><literal>oauth_client_id</literal></term>
++ <listitem>
++ <para>
++ TODO
++ </para>
++ </listitem>
++ </varlistentry>
++
++ <varlistentry id="libpq-connect-oauth-client-secret" xreflabel="oauth_client_secret">
++ <term><literal>oauth_client_secret</literal></term>
++ <listitem>
++ <para>
++ TODO
++ </para>
++ </listitem>
++ </varlistentry>
++
++ <varlistentry id="libpq-connect-oauth-issuer" xreflabel="oauth_issuer">
++ <term><literal>oauth_issuer</literal></term>
++ <listitem>
++ <para>
++ TODO
++ </para>
++ </listitem>
++ </varlistentry>
++
++ <varlistentry id="libpq-connect-oauth-scope" xreflabel="oauth_scope">
++ <term><literal>oauth_scope</literal></term>
++ <listitem>
++ <para>
++ TODO
++ </para>
++ </listitem>
++ </varlistentry>
++
+ </variablelist>
+ </para>
+ </sect2>
+@@ doc/src/sgml/libpq.sgml: void PQinitSSL(int do_ssl);
+
+ </sect1>
+
++ <sect1 id="libpq-oauth">
++ <title>OAuth Support</title>
++
++ <para>
++ TODO
++ </para>
++
++ <para>
++ <variablelist>
++ <varlistentry id="libpq-PQsetAuthDataHook">
++ <term><function>PQsetAuthDataHook</function><indexterm><primary>PQsetAuthDataHook</primary></indexterm></term>
++
++ <listitem>
++ <para>
++ TODO
++<synopsis>
++void PQsetAuthDataHook(PQauthDataHook_type hook);
++</synopsis>
++ </para>
++ </listitem>
++ </varlistentry>
++
++ <varlistentry id="libpq-PQgetAuthDataHook">
++ <term><function>PQgetAuthDataHook</function><indexterm><primary>PQgetAuthDataHook</primary></indexterm></term>
++
++ <listitem>
++ <para>
++ TODO
++<synopsis>
++PQauthDataHook_type PQgetAuthDataHook(void);
++</synopsis>
++ </para>
++ </listitem>
++ </varlistentry>
++ </variablelist>
++ </para>
++
++ </sect1>
++
+
+ <sect1 id="libpq-threading">
+ <title>Behavior in Threaded Programs</title>
+
## meson.build ##
@@ meson.build: endif
@@ meson.build: endif
+endif
+
+if oauthopt in ['auto', 'curl']
-+ oauth = dependency('libcurl', required: (oauthopt == 'curl'))
++ # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability
++ # to explicitly set TLS 1.3 ciphersuites).
++ oauth = dependency('libcurl', version: '>= 7.61.0', required: (oauthopt == 'curl'))
+
+ if oauth.found()
+ oauth_library = 'curl'
@@ src/include/common/oauth-common.h (new)
+ * oauth-common.h
+ * Declarations for helper functions used for OAuth/OIDC authentication
+ *
-+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
++ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/common/oauth-common.h
@@ src/interfaces/libpq/Makefile: endif
else
SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE)
endif
+@@ src/interfaces/libpq/Makefile: backend_src = $(top_srcdir)/src/backend
+ # which seems to insert references to that even in pure C code. Excluding
+ # __tsan_func_exit is necessary when using ThreadSanitizer data race detector
+ # which use this function for instrumentation of function exit.
++# libcurl registers an exit handler in the memory debugging code when running
++# with LeakSanitizer.
+ # Skip the test when profiling, as gcc may insert exit() calls for that.
+ # Also skip the test on platforms where libpq infrastructure may be provided
+ # by statically-linked libraries, as we can't expect them to honor this
+@@ src/interfaces/libpq/Makefile: backend_src = $(top_srcdir)/src/backend
+ libpq-refs-stamp: $(shlib)
+ ifneq ($(enable_coverage), yes)
+ ifeq (,$(filter solaris,$(PORTNAME)))
+- @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \
++ @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit -e _atexit | grep exit; then \
+ echo 'libpq must not be calling any function which invokes exit'; exit 1; \
+ fi
+ endif
## src/interfaces/libpq/exports.txt ##
@@ src/interfaces/libpq/exports.txt: PQcancelFinish 202
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ * fe-auth-oauth-curl.c
+ * The libcurl implementation of OAuth/OIDC authentication.
+ *
-+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
++ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+} OAuthStep;
+
+/*
-+ * The async_ctx holds onto state that needs to persist across multiple calls to
-+ * pg_fe_run_oauth_flow(). Almost everything interacts with this in some way.
++ * The async_ctx holds onto state that needs to persist across multiple calls
++ * to pg_fe_run_oauth_flow(). Almost everything interacts with this in some
++ * way.
+ */
+struct async_ctx
+{
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ int timerfd; /* a timerfd for signaling async timeouts */
+#endif
+ pgsocket mux; /* the multiplexer socket containing all
-+ * descriptors tracked by cURL, plus the
++ * descriptors tracked by libcurl, plus the
+ * timerfd */
-+ CURLM *curlm; /* top-level multi handle for cURL operations */
++ CURLM *curlm; /* top-level multi handle for libcurl
++ * operations */
+ CURL *curl; /* the (single) easy handle for serial
+ * requests */
+
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ * actx_error[_str] to manipulate this. This must be filled
+ * with something useful on an error.
+ *
-+ * - curl_err: an optional static error buffer used by cURL to put
++ * - curl_err: an optional static error buffer used by libcurl to put
+ * detailed information about failures. Unfortunately
+ * untranslatable.
+ *
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+
+ if (err)
+ libpq_append_conn_error(conn,
-+ "cURL easy handle removal failed: %s",
++ "libcurl easy handle removal failed: %s",
+ curl_multi_strerror(err));
+ }
+
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+
+ if (err)
+ libpq_append_conn_error(conn,
-+ "cURL multi handle cleanup failed: %s",
++ "libcurl multi handle cleanup failed: %s",
+ curl_multi_strerror(err));
+ }
+
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ appendPQExpBufferStr(&(ACTX)->errbuf, S)
+
+/*
-+ * Macros for getting and setting state for the connection's two cURL handles,
-+ * so you don't have to write out the error handling every time.
++ * Macros for getting and setting state for the connection's two libcurl
++ * handles, so you don't have to write out the error handling every time.
+ */
+
+#define CHECK_MSETOPT(ACTX, OPT, VAL, FAILACTION) \
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+}
+
+/*
++ * Checks the Content-Type header against the expected type. Parameters are
++ * allowed but ignored.
++ */
++static bool
++check_content_type(struct async_ctx *actx, const char *type)
++{
++ const size_t type_len = strlen(type);
++ char *content_type;
++
++ CHECK_GETINFO(actx, CURLINFO_CONTENT_TYPE, &content_type, return false);
++
++ if (!content_type)
++ {
++ actx_error(actx, "no content type was provided");
++ return false;
++ }
++
++ /*
++ * We need to perform a length limited comparison and not compare the whole
++ * string.
++ */
++ if (pg_strncasecmp(content_type, type, type_len) != 0)
++ goto fail;
++
++ /* On an exact match, we're done. */
++ Assert(strlen(content_type) >= type_len);
++ if (content_type[type_len] == '\0')
++ return true;
++
++ /*
++ * Only a semicolon (optionally preceded by HTTP optional whitespace) is
++ * acceptable after the prefix we checked. This marks the start of media
++ * type parameters, which we currently have no use for.
++ */
++ for (size_t i = type_len; content_type[i]; ++i)
++ {
++ switch (content_type[i])
++ {
++ case ';':
++ return true; /* success! */
++
++ /* HTTP optional whitespace allows only spaces and htabs. */
++ case ' ':
++ case '\t':
++ break;
++
++ default:
++ goto fail;
++ }
++ }
++
++fail:
++ actx_error(actx, "unexpected content type: \"%s\"", content_type);
++ return false;
++}
++
++/*
+ * A helper function for general JSON parsing. fields is the array of field
+ * definitions with their backing pointers. The response will be parsed from
+ * actx->curl and actx->work_data (as set up by start_request()), and any
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+parse_oauth_json(struct async_ctx *actx, const struct json_field *fields)
+{
+ PQExpBuffer resp = &actx->work_data;
-+ char *content_type;
+ JsonLexContext lex = {0};
+ JsonSemAction sem = {0};
+ JsonParseErrorType err;
+ struct oauth_parse ctx = {0};
+ bool success = false;
+
-+ /* Make sure the server thinks it's given us JSON. */
-+ CHECK_GETINFO(actx, CURLINFO_CONTENT_TYPE, &content_type, return false);
-+
-+ if (!content_type)
-+ {
-+ actx_error(actx, "no content type was provided");
-+ goto cleanup;
-+ }
-+ else if (strcasecmp(content_type, "application/json") != 0)
-+ {
-+ actx_error(actx, "unexpected content type \"%s\"", content_type);
-+ goto cleanup;
-+ }
++ if (!check_content_type(actx, "application/json"))
++ return false;
+
+ if (strlen(resp->data) != resp->len)
+ {
+ actx_error(actx, "response contains embedded NULLs");
-+ goto cleanup;
++ return false;
+ }
+
+ makeJsonLexContextCstringLen(&lex, resp->data, resp->len, PG_UTF8, true);
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ authz->interval = parse_interval(authz->interval_str);
+ else
+ {
-+ /* TODO: handle default interval of 5 seconds */
++ /*
++ * RFC 8628 specify 5 seconds as the default value if the server
++ * doesn't provide an interval.
++ */
++ authz->interval = 5;
+ }
+
+ return true;
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+}
+
+/*
-+ * cURL Multi Setup/Callbacks
++ * libcurl Multi Setup/Callbacks
+ */
+
+/*
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+
+/*
+ * Adds and removes sockets from the multiplexer set, as directed by the
-+ * cURL multi handle.
++ * libcurl multi handle.
+ */
+static int
+register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ break;
+
+ default:
-+ actx_error(actx, "unknown cURL socket operation (%d)", what);
++ actx_error(actx, "unknown libcurl socket operation: %d", what);
+ return -1;
+ }
+
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ break;
+
+ default:
-+ actx_error(actx, "unknown cURL socket operation (%d)", what);
++ actx_error(actx, "unknown libcurl socket operation: %d", what);
+ return -1;
+ }
+
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ /*
+ * EV_RECEIPT should guarantee one EV_ERROR result for every change,
+ * whether successful or not. Failed entries contain a non-zero errno
-+ * in the `data` field.
++ * in the data field.
+ */
+ Assert(ev_out[i].flags & EV_ERROR);
+
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+
+/*
+ * Adds or removes timeouts from the multiplexer set, as directed by the
-+ * cURL multi handle. Rather than continually adding and removing the timer,
-+ * we keep it in the set at all times and just disarm it when it's not
-+ * needed.
++ * libcurl multi handle. Rather than continually adding and removing the timer,
++ * we keep it in the set at all times and just disarm it when it's not needed.
+ */
+static int
+register_timer(CURLM *curlm, long timeout, void *ctx)
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ else if (timeout == 0)
+ {
+ /*
-+ * A zero timeout means cURL wants us to call back immediately. That's
-+ * not technically an option for timerfd, but we can make the timeout
-+ * ridiculously short.
++ * A zero timeout means libcurl wants us to call back immediately.
++ * That's not technically an option for timerfd, but we can make the
++ * timeout ridiculously short.
+ *
+ * TODO: maybe just signal drive_request() to immediately call back in
+ * this case?
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+}
+
+/*
-+ * Initializes the two cURL handles in the async_ctx. The multi handle,
++ * Initializes the two libcurl handles in the async_ctx. The multi handle,
+ * actx->curlm, is what drives the asynchronous engine and tells us what to do
+ * next. The easy handle, actx->curl, encapsulates the state for a single
+ * request/response. It's added to the multi handle as needed, during
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+static bool
+setup_curl_handles(struct async_ctx *actx)
+{
-+ curl_version_info_data *curl_info;
++ curl_version_info_data *curl_info;
+
+ /*
+ * Create our multi handle. This encapsulates the entire conversation with
-+ * cURL for this connection.
++ * libcurl for this connection.
+ */
+ actx->curlm = curl_multi_init();
+ if (!actx->curlm)
+ {
+ /* We don't get a lot of feedback on the failure reason. */
-+ actx_error(actx, "failed to create cURL multi handle");
++ actx_error(actx, "failed to create libcurl multi handle");
+ return false;
+ }
+
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ actx->curl = curl_easy_init();
+ if (!actx->curl)
+ {
-+ actx_error(actx, "failed to create cURL handle");
++ actx_error(actx, "failed to create libcurl handle");
+ return false;
+ }
+
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ */
+
+/*
-+ * Response callback from cURL; appends the response body into actx->work_data.
-+ * See start_request().
++ * Response callback from libcurl which appends the response body into
++ * actx->work_data (see start_request()). The maximum size of the data is
++ * defined by CURL_MAX_WRITE_SIZE which by default is 16kb (and can only be
++ * changed by recompiling libcurl).
+ */
+static size_t
+append_data(char *buf, size_t size, size_t nmemb, void *userdata)
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ if (msg->msg != CURLMSG_DONE)
+ {
+ /*
-+ * Future cURL versions may define new message types; we don't
++ * Future libcurl versions may define new message types; we don't
+ * know how to handle them, so we'll ignore them.
+ */
+ continue;
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ err = curl_multi_remove_handle(actx->curlm, msg->easy_handle);
+ if (err)
+ {
-+ actx_error(actx, "cURL easy handle removal failed: %s",
++ actx_error(actx, "libcurl easy handle removal failed: %s",
+ curl_multi_strerror(err));
+ return PGRES_POLLING_FAILED;
+ }
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ return true;
+}
+
++
+/*
-+ * The top-level, nonblocking entry point for the cURL implementation. This will
-+ * be called several times to pump the async engine.
++ * The top-level, nonblocking entry point for the libcurl implementation. This
++ * will be called several times to pump the async engine.
+ *
+ * The architecture is based on PQconnectPoll(). The first half drives the
+ * connection state forward as necessary, returning if we're not ready to
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ struct token tok = {0};
+
+ /*
-+ * XXX This is not safe. cURL has stringent requirements for the thread
++ * XXX This is not safe. libcurl has stringent requirements for the thread
+ * context in which you call curl_global_init(), because it's going to try
+ * initializing a bunch of other libraries (OpenSSL, Winsock...). And we
+ * probably need to consider both the TLS backend libcurl is compiled
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ * Recent versions of libcurl have improved the thread-safety situation,
+ * but you apparently can't check at compile time whether the
+ * implementation is thread-safe, and there's a chicken-and-egg problem
-+ * where you can't check the thread safety until you've initialized cURL,
-+ * which you can't do before you've made sure it's thread-safe...
++ * where you can't check the thread safety until you've initialized
++ * libcurl, which you can't do before you've made sure it's thread-safe...
+ *
+ * We know we've already initialized Winsock by this point, so we should
-+ * be able to safely skip that bit. But we have to tell cURL to initialize
-+ * everything else, because other pieces of our client executable may
-+ * already be using cURL for their own purposes. If we initialize libcurl
-+ * first, with only a subset of its features, we could break those other
-+ * clients nondeterministically, and that would probably be a nightmare to
-+ * debug.
++ * be able to safely skip that bit. But we have to tell libcurl to
++ * initialize everything else, because other pieces of our client
++ * executable may already be using libcurl for their own purposes. If we
++ * initialize libcurl first, with only a subset of its features, we could
++ * break those other clients nondeterministically, and that would probably
++ * be a nightmare to debug.
+ */
+ curl_global_init(CURL_GLOBAL_ALL
+ & ~CURL_GLOBAL_WIN32); /* we already initialized Winsock */
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ */
+ if (strcmp(err->error, "slow_down") == 0)
+ {
-+ actx->authz.interval += 5; /* TODO check for overflow? */
++ int prev_interval = actx->authz.interval;
++
++ actx->authz.interval += 5;
++ if (actx->authz.interval < prev_interval)
++ {
++ actx_error(actx, "slow_down interval overflow");
++ goto error_return;
++ }
+ }
+
+ /*
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ * fe-auth-oauth.c
+ * The front-end (client) implementation of OAuth/OIDC authentication.
+ *
-+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
++ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
@@ src/interfaces/libpq/fe-auth-oauth.h (new)
+ *
+ * Definitions for OAuth authentication implementations
+ *
-+ * Portions Copyright (c) 2023, PostgreSQL Global Development Group
++ * Portions Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * src/interfaces/libpq/fe-auth-oauth.h
+ *
5: 044d8b08e9 ! 5: 16994b449d backend: add OAUTHBEARER SASL mechanism
@@ Commit message
- use logdetail during auth failures
- allow passing the configured issuer to the oauth_validator_command, to
deal with multi-issuer setups
+ - fill in documentation stubs
- ...and more.
Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
@@ .cirrus.tasks.yml: task:
###
# Test that code can be built with gcc/clang without warnings
+ ## doc/src/sgml/client-auth.sgml ##
+@@ doc/src/sgml/client-auth.sgml: include_dir <replaceable>directory</replaceable>
+ </para>
+ </listitem>
+ </varlistentry>
++
++ <varlistentry>
++ <term><literal>oauth</literal></term>
++ <listitem>
++ <para>
++ Authorize and optionally authenticate using a third-party OAuth 2.0
++ identity provider. See <xref linkend="auth-oauth"/> for details.
++ </para>
++ </listitem>
++ </varlistentry>
+ </variablelist>
+
+ </para>
+@@ doc/src/sgml/client-auth.sgml: omicron bryanh guest1
+ only on OpenBSD).
+ </para>
+ </listitem>
++ <listitem>
++ <para>
++ <link linkend="auth-oauth">OAuth authorization/authentication</link>,
++ which relies on an external OAuth 2.0 identity provider.
++ </para>
++ </listitem>
+ </itemizedlist>
+ </para>
+
+@@ doc/src/sgml/client-auth.sgml: host ... radius radiusservers="server1,server2" radiussecrets="""secret one"",""
+ </note>
+ </sect1>
+
++ <sect1 id="auth-oauth">
++ <title>OAuth Authorization/Authentication</title>
++
++ <indexterm zone="auth-oauth">
++ <primary>OAuth Authorization/Authentication</primary>
++ </indexterm>
++
++ <para>
++ TODO
++ </para>
++ </sect1>
++
+ <sect1 id="client-authentication-problems">
+ <title>Authentication Problems</title>
+
+
+ ## doc/src/sgml/filelist.sgml ##
+@@
+ <!ENTITY generic-wal SYSTEM "generic-wal.sgml">
+ <!ENTITY custom-rmgr SYSTEM "custom-rmgr.sgml">
+ <!ENTITY backup-manifest SYSTEM "backup-manifest.sgml">
++<!ENTITY oauth-validators SYSTEM "oauth-validators.sgml">
+
+ <!-- contrib information -->
+ <!ENTITY contrib SYSTEM "contrib.sgml">
+
+ ## doc/src/sgml/oauth-validators.sgml (new) ##
+@@
++<!-- doc/src/sgml/oauth-validators.sgml -->
++
++<chapter id="oauth-validators">
++ <title>Implementing OAuth Validator Modules</title>
++
++ <para>
++ TODO
++ </para>
++</chapter>
+
+ ## doc/src/sgml/postgres.sgml ##
+@@ doc/src/sgml/postgres.sgml: break is not needed in a wider output rendering.
+ &bki;
+ &planstats;
+ &backup-manifest;
++ &oauth-validators;
+
+ </part>
+
+
## src/backend/libpq/Makefile ##
@@ src/backend/libpq/Makefile: include $(top_builddir)/src/Makefile.global
# be-fsstubs is here for historical reasons, probably belongs elsewhere
@@ src/backend/libpq/auth-oauth.c (new)
+ initStringInfo(&buf);
+
+ /*
-+ * TODO: note that escaping here should be belt-and-suspenders, since
-+ * escapable characters aren't valid in either the issuer URI or the scope
-+ * list, but the HBA doesn't enforce that yet.
++ * Escaping the string here is belt-and-suspenders defensive programming
++ * since escapable characters aren't valid in either the issuer URI or the
++ * scope list, but the HBA doesn't enforce that yet.
+ */
+ appendStringInfoString(&buf, "{ \"status\": \"invalid_token\", ");
+
@@ src/include/libpq/sasl.h: typedef struct pg_be_sasl_mech
/* Common implementation for auth.c */
+ ## src/interfaces/libpq/fe-auth-oauth-curl.c ##
+@@ src/interfaces/libpq/fe-auth-oauth-curl.c: free_token(struct token *tok)
+ /* States for the overall async machine. */
+ typedef enum
+ {
+- OAUTH_STEP_INIT,
++ OAUTH_STEP_INIT = 0,
+ OAUTH_STEP_DISCOVERY,
+ OAUTH_STEP_DEVICE_AUTHORIZATION,
+ OAUTH_STEP_TOKEN_REQUEST,
+
## src/test/modules/Makefile ##
@@ src/test/modules/Makefile: SUBDIRS = \
dummy_index_am \
@@ src/test/modules/oauth_validator/t/001_server.pl (new)
+use strict;
+use warnings FATAL => 'all';
+
++use JSON::PP qw(encode_json);
++use MIME::Base64 qw(encode_base64);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::OAuthServer;
@@ src/test/modules/oauth_validator/t/001_server.pl (new)
+
+$node->safe_psql('postgres', 'CREATE USER test;');
+$node->safe_psql('postgres', 'CREATE USER testalt;');
++$node->safe_psql('postgres', 'CREATE USER testparam;');
+
+my $webserver = PostgreSQL::Test::OAuthServer->new();
+$webserver->run();
+
++END
++{
++ my $exit_code = $?;
++
++ $webserver->stop() if defined $webserver; # might have been SKIP'd
++
++ $? = $exit_code;
++}
++
+my $port = $webserver->port();
+my $issuer = "127.0.0.1:$port";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{
-+local all test oauth issuer="$issuer" scope="openid postgres"
-+local all testalt oauth issuer="$issuer/alternate" scope="openid postgres alt"
++local all test oauth issuer="$issuer" scope="openid postgres"
++local all testalt oauth issuer="$issuer/alternate" scope="openid postgres alt"
++local all testparam oauth issuer="$issuer/param" scope="openid postgres"
+});
+$node->reload;
+
@@ src/test/modules/oauth_validator/t/001_server.pl (new)
+$log_start = $node->wait_for_log(qr/reloading configuration files/);
+
+my $user = "test";
-+$node->connect_ok("user=$user dbname=postgres oauth_client_id=f02c6361-0635", "connect",
-+ expected_stderr => qr@Visit https://example\.com/ and enter the code: postgresuser@);
-+
-+$log_end = $node->wait_for_log(qr/connection authorized/, $log_start);
-+$node->log_check("user $user: validator receives correct parameters", $log_start,
-+ log_like => [
-+ qr/oauth_validator: token="9243959234", role="$user"/,
-+ qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/,
-+ ]);
-+$node->log_check("user $user: validator sets authenticated identity", $log_start,
-+ log_like => [
-+ qr/connection authenticated: identity="test" method=oauth/,
-+ ]);
-+$log_start = $log_end;
++if ($node->connect_ok("user=$user dbname=postgres oauth_client_id=f02c6361-0635", "connect",
++ expected_stderr => qr@Visit https://example\.com/ and enter the code: postgresuser@))
++{
++ $log_end = $node->wait_for_log(qr/connection authorized/, $log_start);
++ $node->log_check("user $user: validator receives correct parameters", $log_start,
++ log_like => [
++ qr/oauth_validator: token="9243959234", role="$user"/,
++ qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/,
++ ]);
++ $node->log_check("user $user: validator sets authenticated identity", $log_start,
++ log_like => [
++ qr/connection authenticated: identity="test" method=oauth/,
++ ]);
++ $log_start = $log_end;
++}
+
+# The /alternate issuer uses slightly different parameters.
+$user = "testalt";
-+$node->connect_ok("user=$user dbname=postgres oauth_client_id=f02c6361-0636", "connect",
-+ expected_stderr => qr@Visit https://example\.org/ and enter the code: postgresuser@);
-+
-+$log_end = $node->wait_for_log(qr/connection authorized/, $log_start);
-+$node->log_check("user $user: validator receives correct parameters", $log_start,
-+ log_like => [
-+ qr/oauth_validator: token="9243959234-alt", role="$user"/,
-+ qr|oauth_validator: issuer="\Q$issuer/alternate\E", scope="openid postgres alt"|,
-+ ]);
-+$node->log_check("user $user: validator sets authenticated identity", $log_start,
-+ log_like => [
-+ qr/connection authenticated: identity="testalt" method=oauth/,
-+ ]);
-+$log_start = $log_end;
-+
-+$webserver->stop();
++if ($node->connect_ok("user=$user dbname=postgres oauth_client_id=f02c6361-0636", "connect",
++ expected_stderr => qr@Visit https://example\.org/ and enter the code: postgresuser@))
++{
++ $log_end = $node->wait_for_log(qr/connection authorized/, $log_start);
++ $node->log_check("user $user: validator receives correct parameters", $log_start,
++ log_like => [
++ qr/oauth_validator: token="9243959234-alt", role="$user"/,
++ qr|oauth_validator: issuer="\Q$issuer/alternate\E", scope="openid postgres alt"|,
++ ]);
++ $node->log_check("user $user: validator sets authenticated identity", $log_start,
++ log_like => [
++ qr/connection authenticated: identity="testalt" method=oauth/,
++ ]);
++ $log_start = $log_end;
++}
++
++#
++# Further tests rely on support for specific behaviors in oauth_server.py. To
++# trigger these behaviors, we ask for the special issuer .../param (which is set
++# up in HBA for the testparam user) and encode magic instructions into the
++# oauth_client_id.
++#
++
++my $common_connstr = "user=testparam dbname=postgres ";
++
++sub connstr
++{
++ my (%params) = @_;
++
++ my $json = encode_json(\%params);
++ my $encoded = encode_base64($json, "");
++
++ return "$common_connstr oauth_client_id=$encoded";
++}
++
++# Make sure the param system works end-to-end first.
++$node->connect_ok(
++ connstr(),
++ "connect to /param",
++ expected_stderr => qr@Visit https://example\.com/ and enter the code: postgresuser@
++);
++
++$node->connect_ok(
++ connstr(stage => 'token', retries => 1),
++ "token retry",
++ expected_stderr => qr@Visit https://example\.com/ and enter the code: postgresuser@
++);
++$node->connect_ok(
++ connstr(stage => 'token', retries => 2),
++ "token retry (twice)",
++ expected_stderr => qr@Visit https://example\.com/ and enter the code: postgresuser@
++);
++$node->connect_ok(
++ connstr(stage => 'all', retries => 1, interval => 2),
++ "token retry (two second interval)",
++ expected_stderr => qr@Visit https://example\.com/ and enter the code: postgresuser@
++);
++$node->connect_ok(
++ connstr(stage => 'all', retries => 1, interval => JSON::PP::null),
++ "token retry (default interval)",
++ expected_stderr => qr@Visit https://example\.com/ and enter the code: postgresuser@
++);
++
++$node->connect_ok(
++ connstr(stage => 'all', content_type => 'application/json;charset=utf-8'),
++ "content type with charset",
++ expected_stderr => qr@Visit https://example\.com/ and enter the code: postgresuser@
++);
++$node->connect_ok(
++ connstr(stage => 'all', content_type => "application/json \t;\t charset=utf-8"),
++ "content type with charset (whitespace)",
++ expected_stderr => qr@Visit https://example\.com/ and enter the code: postgresuser@
++);
++
++$node->connect_fails(
++ connstr(stage => 'device', content_type => 'text/plain'),
++ "bad device authz response: wrong content type",
++ expected_stderr => qr/failed to parse device authorization: unexpected content type/
++);
++$node->connect_fails(
++ connstr(stage => 'token', content_type => 'text/plain'),
++ "bad token response: wrong content type",
++ expected_stderr => qr/failed to parse access token response: unexpected content type/
++);
++$node->connect_fails(
++ connstr(stage => 'token', content_type => 'application/jsonx'),
++ "bad token response: wrong content type (correct prefix)",
++ expected_stderr => qr/failed to parse access token response: unexpected content type/
++);
++
++$node->connect_fails(
++ connstr(stage => 'all', interval => ~0, retries => 1, retry_code => "slow_down"),
++ "bad token response: server overflows the device authz interval",
++ expected_stderr => qr/failed to obtain access token: slow_down interval overflow/
++);
++
+$node->stop;
+
+done_testing();
@@ src/test/modules/oauth_validator/t/oauth_server.py (new)
@@
+#! /usr/bin/env python3
+
++import base64
+import http.server
+import json
+import os
+import sys
++import time
++import urllib.parse
++from collections import defaultdict
+
+
+class OAuthHandler(http.server.BaseHTTPRequestHandler):
@@ src/test/modules/oauth_validator/t/oauth_server.py (new)
+ Switches the behavior of the provider depending on the issuer URI.
+ """
+ self._alt_issuer = self.path.startswith("/alternate/")
++ self._parameterized = self.path.startswith("/param/")
++
+ if self._alt_issuer:
+ self.path = self.path.removeprefix("/alternate")
++ elif self._parameterized:
++ self.path = self.path.removeprefix("/param")
+
+ def do_GET(self):
++ self._response_code = 200
+ self._check_issuer()
+
+ if self.path == "/.well-known/openid-configuration":
@@ src/test/modules/oauth_validator/t/oauth_server.py (new)
+
+ self._send_json(resp)
+
++ def _parse_params(self) -> dict[str, str]:
++ """
++ Parses apart the form-urlencoded request body and returns the resulting
++ dict. For use by do_POST().
++ """
++ size = int(self.headers["Content-Length"])
++ form = self.rfile.read(size)
++
++ assert self.headers["Content-Type"] == "application/x-www-form-urlencoded"
++ return urllib.parse.parse_qs(form.decode("utf-8"), strict_parsing=True)
++
++ @property
++ def client_id(self) -> str:
++ """
++ Returns the client_id sent in the POST body. self._parse_params() must
++ have been called first.
++ """
++ return self._params["client_id"][0]
++
+ def do_POST(self):
++ self._response_code = 200
+ self._check_issuer()
+
++ self._params = self._parse_params()
++ if self._parameterized:
++ # Pull encoded test parameters out of the peer's client_id field.
++ # This is expected to be Base64-encoded JSON.
++ js = base64.b64decode(self.client_id)
++ self._test_params = json.loads(js)
++
+ if self.path == "/authorize":
+ resp = self.authorization()
+ elif self.path == "/token":
+ resp = self.token()
+ else:
-+ self.send_error(404, "Not Found")
++ self.send_error(404)
+ return
+
+ self._send_json(resp)
+
++ def _should_modify(self) -> bool:
++ """
++ Returns True if the client has requested a modification to this stage of
++ the exchange.
++ """
++ if not hasattr(self, "_test_params"):
++ return False
++
++ stage = self._test_params.get("stage")
++
++ return (
++ stage == "all"
++ or (stage == "device" and self.path == "/authorize")
++ or (stage == "token" and self.path == "/token")
++ )
++
++ def _content_type(self) -> str:
++ """
++ Returns "application/json" unless the test has requested something
++ different.
++ """
++ if self._should_modify() and "content_type" in self._test_params:
++ return self._test_params["content_type"]
++
++ return "application/json"
++
++ def _interval(self) -> int:
++ """
++ Returns 0 unless the test has requested something different.
++ """
++ if self._should_modify() and "interval" in self._test_params:
++ return self._test_params["interval"]
++
++ return 0
++
++ def _retry_code(self) -> str:
++ """
++ Returns "authorization_pending" unless the test has requested something
++ different.
++ """
++ if self._should_modify() and "retry_code" in self._test_params:
++ return self._test_params["retry_code"]
++
++ return "authorization_pending"
++
+ def _send_json(self, js: JsonObject) -> None:
+ """
+ Sends the provided JSON dict as an application/json response.
++ self._response_code can be modified to send JSON error responses.
+ """
-+
+ resp = json.dumps(js).encode("ascii")
++ self.log_message("sending JSON response: %s", resp)
+
-+ self.send_response(200, "OK")
-+ self.send_header("Content-Type", "application/json")
++ self.send_response(self._response_code)
++ self.send_header("Content-Type", self._content_type())
+ self.send_header("Content-Length", str(len(resp)))
+ self.end_headers()
+
@@ src/test/modules/oauth_validator/t/oauth_server.py (new)
+
+ def config(self) -> JsonObject:
+ port = self.server.socket.getsockname()[1]
++
+ issuer = f"http://localhost:{port}"
+ if self._alt_issuer:
+ issuer += "/alternate"
++ elif self._parameterized:
++ issuer += "/param"
+
+ return {
+ "issuer": issuer,
@@ src/test/modules/oauth_validator/t/oauth_server.py (new)
+ "grant_types_supported": ["urn:ietf:params:oauth:grant-type:device_code"],
+ }
+
++ @property
++ def _token_state(self):
++ """
++ A cached _TokenState object for the connected client (as determined by
++ the request's client_id), or a new one if it doesn't already exist.
++
++ This relies on the existence of a defaultdict attached to the server;
++ see main() below.
++ """
++ return self.server.token_state[self.client_id]
++
++ def _remove_token_state(self):
++ """
++ Removes any cached _TokenState for the current client_id. Call this
++ after the token exchange ends to get rid of unnecessary state.
++ """
++ if self.client_id in self.server.token_state:
++ del self.server.token_state[self.client_id]
++
+ def authorization(self) -> JsonObject:
+ uri = "https://example.com/"
+ if self._alt_issuer:
+ uri = "https://example.org/"
+
-+ return {
++ resp = {
+ "device_code": "postgres",
+ "user_code": "postgresuser",
-+ "interval": 0,
+ "verification_uri": uri,
+ "expires-in": 5,
+ }
+
++ interval = self._interval()
++ if interval is not None:
++ resp["interval"] = interval
++ self._token_state.min_delay = interval
++ else:
++ self._token_state.min_delay = 5 # default
++
++ return resp
++
+ def token(self) -> JsonObject:
++ if self._should_modify() and "retries" in self._test_params:
++ retries = self._test_params["retries"]
++
++ # Check to make sure the token interval is being respected.
++ now = time.monotonic()
++ if self._token_state.last_try is not None:
++ delay = now - self._token_state.last_try
++ assert (
++ delay > self._token_state.min_delay
++ ), f"client waited only {delay} seconds between token requests (expected {self._token_state.min_delay})"
++
++ self._token_state.last_try = now
++
++ # If we haven't reached the required number of retries yet, return a
++ # "pending" response.
++ if self._token_state.retries < retries:
++ self._token_state.retries += 1
++
++ self._response_code = 400
++ return {"error": self._retry_code()}
++
++ # Clean up any retry tracking state now that the exchange is ending.
++ self._remove_token_state()
++
+ token = "9243959234"
+ if self._alt_issuer:
+ token += "-alt"
@@ src/test/modules/oauth_validator/t/oauth_server.py (new)
+def main():
+ s = http.server.HTTPServer(("127.0.0.1", 0), OAuthHandler)
+
++ # Attach a "cache" dictionary to the server to allow the OAuthHandlers to
++ # track state across token requests. The use of defaultdict ensures that new
++ # entries will be created automatically.
++ class _TokenState:
++ retries = 0
++ min_delay = None
++ last_try = None
++
++ s.token_state = defaultdict(_TokenState)
++
+ # Give the parent the port number to contact (this is also the signal that
+ # we're ready to receive requests).
+ port = s.socket.getsockname()[1]
@@ src/test/modules/oauth_validator/validator.c (new)
+ /* Check to make sure our private state still exists. */
+ if (state->private_data != PRIVATE_COOKIE)
+ elog(ERROR, "oauth_validator: private state cookie changed to %p",
-+ state->private_data);
++ state->private_data);
+
+ res = palloc(sizeof(ValidatorModuleResult));
+
@@ src/test/perl/PostgreSQL/Test/OAuthServer.pm (new)
+
+use warnings;
+use strict;
-+use threads;
+use Scalar::Util;
+use Socket;
+use IO::Select;
++use Test::More;
+
+local *server_socket;
+
@@ src/test/perl/PostgreSQL/Test/OAuthServer.pm (new)
+ my $port;
+
+ my $pid = open(my $read_fh, "-|", $ENV{PYTHON}, "t/oauth_server.py")
-+ // die "failed to start OAuth server: $!";
++ or die "failed to start OAuth server: $!";
+
+ read($read_fh, $port, 7) // die "failed to read port number: $!";
+ chomp $port;
@@ src/test/perl/PostgreSQL/Test/OAuthServer.pm (new)
+ $self->{'port'} = $port;
+ $self->{'child'} = $read_fh;
+
-+ print("# OAuth provider (PID $pid) is listening on port $port\n");
++ diag("OAuth provider (PID $pid) is listening on port $port\n");
+}
+
+sub stop
+{
+ my $self = shift;
+
-+ print("# Sending SIGTERM to OAuth provider PID: $self->{'pid'}\n");
++ diag("Sending SIGTERM to OAuth provider PID: $self->{'pid'}\n");
+
+ kill(15, $self->{'pid'});
+ $self->{'pid'} = undef;
@@ src/tools/pgindent/typedefs.list: VacuumRelation
ValidIOData
ValidateIndexState
+ValidatorModuleState
++ValidatorModuleResult
ValuesScan
ValuesScanState
Var
6: 84c6893325 ! 6: d163d2ca0a Review comments
@@ Commit message
Fixes and tidy-ups following a review of v21, a few items
are (listed in no specific order):
- * Implement a version check for libcurl in autoconf, the
- equivalent check for Meson is still a TODO.
+ * Implement a version check for libcurl in autoconf, the equivalent
+ check for Meson is still a TODO. [ed: moved to an earlier commit]
* Address a few TODOs in the code
- * libpq JSON support memory management fixups [ed: these have been moved
- to an earlier commit]
-
- ## config/programs.m4 ##
-@@ config/programs.m4: if test "$pgac_cv_ldap_safe" != yes; then
- *** also uses LDAP will crash on exit.])
- fi])
-
-+# PGAC_CHECK_LIBCURL
-+# ------------------
-+# Check for libcurl 8.4.0 or higher since earlier versions can be compiled
-+# with a codepatch containing exit(), and PostgreSQL does not allow any lib
-+# linked to libpq which can call exit.
-
-+# PGAC_CHECK_LIBCURL
-+AC_DEFUN([PGAC_CHECK_LIBCURL],
-+[AC_CACHE_CHECK([for compatible libcurl], [pgac_cv_check_libcurl],
-+[AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
-+[#include <curl/curlver.h>
-+#if LIBCURL_VERSION_MAJOR <= 8 && LIBCURL_VERSION_MINOR < 4
-+choke me
-+#endif], [])],
-+[pgac_cv_check_libcurl=yes],
-+[pgac_cv_check_libcurl=no])])
-+
-+if test "$pgac_cv_check_libcurl" != yes; then
-+ AC_MSG_ERROR([
-+*** The installed version of libcurl is too old to use with PostgreSQL.
-+*** libcurl version 8.4.0 or later is required.])
-+fi])
-
- # PGAC_CHECK_READLINE
- # -------------------
-
- ## configure ##
-@@ configure: else
- as_fn_error $? "library 'curl' is required for --with-oauth=curl" "$LINENO" 5
- fi
-
-+ { $as_echo "$as_me:${as_lineno-$LINENO}: checking for compatible libcurl" >&5
-+$as_echo_n "checking for compatible libcurl... " >&6; }
-+if ${pgac_cv_check_libcurl+:} false; then :
-+ $as_echo_n "(cached) " >&6
-+else
-+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-+/* end confdefs.h. */
-+#include <curl/curlver.h>
-+#if LIBCURL_VERSION_MAJOR <= 8 && LIBCURL_VERSION_MINOR < 4
-+choke me
-+#endif
-+int
-+main ()
-+{
-+
-+ ;
-+ return 0;
-+}
-+_ACEOF
-+if ac_fn_c_try_compile "$LINENO"; then :
-+ pgac_cv_check_libcurl=yes
-+else
-+ pgac_cv_check_libcurl=no
-+fi
-+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-+fi
-+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_check_libcurl" >&5
-+$as_echo "$pgac_cv_check_libcurl" >&6; }
-+
-+if test "$pgac_cv_check_libcurl" != yes; then
-+ as_fn_error $? "
-+*** The installed version of libcurl is too old to use with PostgreSQL.
-+*** libcurl version 8.4.0 or later is required." "$LINENO" 5
-+fi
- fi
-
- # for contrib/sepgsql
-
- ## configure.ac ##
-@@ configure.ac: AC_SUBST(LDAP_LIBS_BE)
-
- if test "$with_oauth" = curl ; then
- AC_CHECK_LIB(curl, curl_multi_init, [], [AC_MSG_ERROR([library 'curl' is required for --with-oauth=curl])])
-+ PGAC_CHECK_LIBCURL
- fi
-
- # for contrib/sepgsql
+ * libpq JSON support memory management fixups [ed: moved to an earlier
+ commit]
## src/backend/libpq/auth-oauth.c ##
-@@ src/backend/libpq/auth-oauth.c: generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen)
- initStringInfo(&buf);
-
- /*
-- * TODO: note that escaping here should be belt-and-suspenders, since
-- * escapable characters aren't valid in either the issuer URI or the scope
-- * list, but the HBA doesn't enforce that yet.
-+ * Escaping the string here is belt-and-suspenders defensive programming
-+ * since escapable characters aren't valid in either the issuer URI or the
-+ * scope list, but the HBA doesn't enforce that yet.
- */
- appendStringInfoString(&buf, "{ \"status\": \"invalid_token\", ");
-
@@ src/backend/libpq/auth-oauth.c: validate_token_format(const char *header)
if (!header || strlen(header) <= 7)
{
@@ src/backend/libpq/auth-oauth.c: validate(Port *port, const char *auth)
}
- ## src/include/common/oauth-common.h ##
-@@
- * oauth-common.h
- * Declarations for helper functions used for OAuth/OIDC authentication
- *
-- * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
-+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
- * Portions Copyright (c) 1994, Regents of the University of California
- *
- * src/include/common/oauth-common.h
-
- ## src/interfaces/libpq/Makefile ##
-@@ src/interfaces/libpq/Makefile: backend_src = $(top_srcdir)/src/backend
- # which seems to insert references to that even in pure C code. Excluding
- # __tsan_func_exit is necessary when using ThreadSanitizer data race detector
- # which use this function for instrumentation of function exit.
-+# libcurl registers an exit handler in the memory debugging code when running
-+# with LeakSanitizer.
- # Skip the test when profiling, as gcc may insert exit() calls for that.
- # Also skip the test on platforms where libpq infrastructure may be provided
- # by statically-linked libraries, as we can't expect them to honor this
-@@ src/interfaces/libpq/Makefile: backend_src = $(top_srcdir)/src/backend
- libpq-refs-stamp: $(shlib)
- ifneq ($(enable_coverage), yes)
- ifeq (,$(filter solaris,$(PORTNAME)))
-- @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \
-+ @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit -e _atexit | grep exit; then \
- echo 'libpq must not be calling any function which invokes exit'; exit 1; \
- fi
- endif
-
## src/interfaces/libpq/fe-auth-oauth-curl.c ##
-@@
- * fe-auth-oauth-curl.c
- * The libcurl implementation of OAuth/OIDC authentication.
- *
-- * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
-+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
- * Portions Copyright (c) 1994, Regents of the University of California
- *
- * IDENTIFICATION
@@
#include "libpq-int.h"
#include "mb/pg_wchar.h"
@@ src/interfaces/libpq/fe-auth-oauth-curl.c
/*
* Parsed JSON Representations
*
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: free_token(struct token *tok)
- /* States for the overall async machine. */
- typedef enum
- {
-- OAUTH_STEP_INIT,
-+ OAUTH_STEP_INIT = 0,
- OAUTH_STEP_DISCOVERY,
- OAUTH_STEP_DEVICE_AUTHORIZATION,
- OAUTH_STEP_TOKEN_REQUEST,
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: typedef enum
- } OAuthStep;
-
- /*
-- * The async_ctx holds onto state that needs to persist across multiple calls to
-- * pg_fe_run_oauth_flow(). Almost everything interacts with this in some way.
-+ * The async_ctx holds onto state that needs to persist across multiple calls
-+ * to pg_fe_run_oauth_flow(). Almost everything interacts with this in some
-+ * way.
- */
- struct async_ctx
- {
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: struct async_ctx
- int timerfd; /* a timerfd for signaling async timeouts */
- #endif
- pgsocket mux; /* the multiplexer socket containing all
-- * descriptors tracked by cURL, plus the
-+ * descriptors tracked by libcurl, plus the
- * timerfd */
-- CURLM *curlm; /* top-level multi handle for cURL operations */
-+ CURLM *curlm; /* top-level multi handle for libcurl
-+ * operations */
- CURL *curl; /* the (single) easy handle for serial
- * requests */
-
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: struct async_ctx
- * actx_error[_str] to manipulate this. This must be filled
- * with something useful on an error.
- *
-- * - curl_err: an optional static error buffer used by cURL to put
-+ * - curl_err: an optional static error buffer used by libcurl to put
- * detailed information about failures. Unfortunately
- * untranslatable.
- *
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: struct async_ctx
- */
- const char *errctx; /* not freed; must point to static allocation */
- PQExpBufferData errbuf;
-- char curl_err[CURL_ERROR_SIZE];
-+ PQExpBufferData curl_err;
-
- /*
- * These documents need to survive over multiple calls, and are therefore
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: struct async_ctx
struct device_authz authz;
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: struct async_ctx
};
/*
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: free_curl_async_ctx(PGconn *conn, void *ctx)
-
- if (err)
- libpq_append_conn_error(conn,
-- "cURL easy handle removal failed: %s",
-+ "libcurl easy handle removal failed: %s",
- curl_multi_strerror(err));
- }
-
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: free_curl_async_ctx(PGconn *conn, void *ctx)
-
- if (err)
- libpq_append_conn_error(conn,
-- "cURL multi handle cleanup failed: %s",
-+ "libcurl multi handle cleanup failed: %s",
- curl_multi_strerror(err));
- }
-
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: free_curl_async_ctx(PGconn *conn, void *ctx)
- appendPQExpBufferStr(&(ACTX)->errbuf, S)
-
- /*
-- * Macros for getting and setting state for the connection's two cURL handles,
-- * so you don't have to write out the error handling every time.
-+ * Macros for getting and setting state for the connection's two libcurl
-+ * handles, so you don't have to write out the error handling every time.
- */
-
- #define CHECK_MSETOPT(ACTX, OPT, VAL, FAILACTION) \
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: parse_oauth_json(struct async_ctx *actx, const struct json_field *fields)
- actx_error(actx, "no content type was provided");
- goto cleanup;
- }
-- else if (strcasecmp(content_type, "application/json") != 0)
-+
-+ /*
-+ * We only check the media-type and not the parameters, so we need to
-+ * perform a length limited comparison and not compare the whole string.
-+ */
-+ if (pg_strncasecmp(content_type, "application/json", strlen("application/json")) != 0)
- {
-- actx_error(actx, "unexpected content type \"%s\"", content_type);
-- goto cleanup;
-+ actx_error(actx, "unexpected content type: \"%s\"", content_type);
-+ return false;
- }
-
- if (strlen(resp->data) != resp->len)
- {
- actx_error(actx, "response contains embedded NULLs");
-- goto cleanup;
-+ return false;
+ return false;
}
- makeJsonLexContextCstringLen(&lex, resp->data, resp->len, PG_UTF8, true);
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: parse_oauth_json(struct async_ctx *ac
ctx.errbuf = &actx->errbuf;
ctx.fields = fields;
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: parse_device_authz(struct async_ctx *actx, struct device_authz *authz)
- authz->interval = parse_interval(authz->interval_str);
- else
- {
-- /* TODO: handle default interval of 5 seconds */
-+ /*
-+ * RFC 8628 specify 5 seconds as the default value if the server
-+ * doesn't provide an interval.
-+ */
-+ authz->interval = 5;
- }
-
- return true;
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: parse_access_token(struct async_ctx *actx, struct token *tok)
- }
-
- /*
-- * cURL Multi Setup/Callbacks
-+ * libcurl Multi Setup/Callbacks
- */
-
- /*
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: setup_multiplexer(struct async_ctx *actx)
-
- /*
- * Adds and removes sockets from the multiplexer set, as directed by the
-- * cURL multi handle.
-+ * libcurl multi handle.
- */
- static int
- register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
- break;
-
- default:
-- actx_error(actx, "unknown cURL socket operation (%d)", what);
-+ actx_error(actx, "unknown libcurl socket operation: %d", what);
- return -1;
- }
-
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
- break;
-
- default:
-- actx_error(actx, "unknown cURL socket operation (%d)", what);
-+ actx_error(actx, "unknown libcurl socket operation: %d", what);
- return -1;
- }
-
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
- /*
- * EV_RECEIPT should guarantee one EV_ERROR result for every change,
- * whether successful or not. Failed entries contain a non-zero errno
-- * in the `data` field.
-+ * in the data field.
- */
- Assert(ev_out[i].flags & EV_ERROR);
-
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
-
- /*
- * Adds or removes timeouts from the multiplexer set, as directed by the
-- * cURL multi handle. Rather than continually adding and removing the timer,
-- * we keep it in the set at all times and just disarm it when it's not
-- * needed.
-+ * libcurl multi handle. Rather than continually adding and removing the timer,
-+ * we keep it in the set at all times and just disarm it when it's not needed.
- */
- static int
- register_timer(CURLM *curlm, long timeout, void *ctx)
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: register_timer(CURLM *curlm, long timeout, void *ctx)
- else if (timeout == 0)
- {
- /*
-- * A zero timeout means cURL wants us to call back immediately. That's
-- * not technically an option for timerfd, but we can make the timeout
-- * ridiculously short.
-+ * A zero timeout means libcurl wants us to call back immediately.
-+ * That's not technically an option for timerfd, but we can make the
-+ * timeout ridiculously short.
- *
- * TODO: maybe just signal drive_request() to immediately call back in
- * this case?
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: register_timer(CURLM *curlm, long timeout, void *ctx)
- return 0;
- }
-
-+static int
-+debug_callback(CURL *handle, curl_infotype *type, char *data, size_t size,
-+ void *clientp)
-+{
-+ struct async_ctx *actx = (struct async_ctx *) clientp;
-+
-+ /* For now we only store TEXT debug information, extending is a TODO */
-+ if (type == CURLINFO_TEXT)
-+ appendBinaryPQExpBuffer(&actx->curl_err, data, size);
-+
-+ return 0;
-+}
-+
- /*
-- * Initializes the two cURL handles in the async_ctx. The multi handle,
-+ * Initializes the two libcurl handles in the async_ctx. The multi handle,
- * actx->curlm, is what drives the asynchronous engine and tells us what to do
- * next. The easy handle, actx->curl, encapsulates the state for a single
- * request/response. It's added to the multi handle as needed, during
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: register_timer(CURLM *curlm, long timeout, void *ctx)
- static bool
- setup_curl_handles(struct async_ctx *actx)
- {
-- curl_version_info_data *curl_info;
-+ curl_version_info_data *curl_info;
-
- /*
- * Create our multi handle. This encapsulates the entire conversation with
-- * cURL for this connection.
-+ * libcurl for this connection.
- */
- actx->curlm = curl_multi_init();
- if (!actx->curlm)
- {
- /* We don't get a lot of feedback on the failure reason. */
-- actx_error(actx, "failed to create cURL multi handle");
-+ actx_error(actx, "failed to create libcurl multi handle");
- return false;
- }
-
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: setup_curl_handles(struct async_ctx *actx)
- actx->curl = curl_easy_init();
- if (!actx->curl)
- {
-- actx_error(actx, "failed to create cURL handle");
-+ actx_error(actx, "failed to create libcurl handle");
- return false;
- }
-
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: setup_curl_handles(struct async_ctx *actx)
- /* No alternative resolver, TODO: warn about timeouts */
- }
-
-- /* TODO investigate using conn->Pfdebug and CURLOPT_DEBUGFUNCTION here */
-+ /*
-+ * Set a callback for retrieving error information from libcurl, the
-+ * function only takes effect when CURLOPT_VERBOSE has been set so make
-+ * sure the order is kept.
-+ */
-+ CHECK_SETOPT(actx, CURLOPT_DEBUGDATA, actx, return false);
-+ CHECK_SETOPT(actx, CURLOPT_DEBUGFUNCTION, debug_callback, return false);
- CHECK_SETOPT(actx, CURLOPT_VERBOSE, 1L, return false);
-- CHECK_SETOPT(actx, CURLOPT_ERRORBUFFER, actx->curl_err, return false);
-
- /*
- * Only HTTP[S] is allowed. TODO: disallow HTTP without user opt-in
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: setup_curl_handles(struct async_ctx *actx)
* pretty strict when it comes to provider behavior, so we have to check
* what comes back anyway.)
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: setup_curl_handles(struct async_ctx *
CHECK_SETOPT(actx, CURLOPT_HTTPHEADER, actx->headers, return false);
return true;
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: setup_curl_handles(struct async_ctx *actx)
- */
-
- /*
-- * Response callback from cURL; appends the response body into actx->work_data.
-- * See start_request().
-+ * Response callback from libcurl which appends the response body into
-+ * actx->work_data (see start_request()). The maximum size of the data is
-+ * defined by CURL_MAX_WRITE_SIZE which by default is 16kb (and can only be
-+ * changed by recompiling libcurl).
- */
- static size_t
- append_data(char *buf, size_t size, size_t nmemb, void *userdata)
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: append_data(char *buf, size_t size, size_t nmemb, void *userdata)
PQExpBuffer resp = userdata;
size_t len = size * nmemb;
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: drive_request(struct async_ctx *actx)
{
/* We'll come back again. */
return PGRES_POLLING_READING;
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: drive_request(struct async_ctx *actx)
- if (msg->msg != CURLMSG_DONE)
- {
- /*
-- * Future cURL versions may define new message types; we don't
-+ * Future libcurl versions may define new message types; we don't
- * know how to handle them, so we'll ignore them.
- */
- continue;
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: drive_request(struct async_ctx *actx)
- err = curl_multi_remove_handle(actx->curlm, msg->easy_handle);
- if (err)
- {
-- actx_error(actx, "cURL easy handle removal failed: %s",
-+ actx_error(actx, "libcurl easy handle removal failed: %s",
- curl_multi_strerror(err));
- return PGRES_POLLING_FAILED;
- }
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: start_device_authz(struct async_ctx *actx, PGconn *conn)
appendPQExpBuffer(work_buffer, "client_id=%s", conn->oauth_client_id);
if (conn->oauth_scope)
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: finish_token_request(struct async_ctx
+ return false;
}
-+
- /*
-- * The top-level, nonblocking entry point for the cURL implementation. This will
-- * be called several times to pump the async engine.
-+ * The top-level, nonblocking entry point for the libcurl implementation. This
-+ * will be called several times to pump the async engine.
- *
- * The architecture is based on PQconnectPoll(). The first half drives the
- * connection state forward as necessary, returning if we're not ready to
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
- struct token tok = {0};
-
- /*
-- * XXX This is not safe. cURL has stringent requirements for the thread
-+ * XXX This is not safe. libcurl has stringent requirements for the thread
- * context in which you call curl_global_init(), because it's going to try
- * initializing a bunch of other libraries (OpenSSL, Winsock...). And we
- * probably need to consider both the TLS backend libcurl is compiled
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
- * Recent versions of libcurl have improved the thread-safety situation,
- * but you apparently can't check at compile time whether the
- * implementation is thread-safe, and there's a chicken-and-egg problem
-- * where you can't check the thread safety until you've initialized cURL,
-- * which you can't do before you've made sure it's thread-safe...
-+ * where you can't check the thread safety until you've initialized
-+ * libcurl, which you can't do before you've made sure it's thread-safe...
- *
- * We know we've already initialized Winsock by this point, so we should
-- * be able to safely skip that bit. But we have to tell cURL to initialize
-- * everything else, because other pieces of our client executable may
-- * already be using cURL for their own purposes. If we initialize libcurl
-- * first, with only a subset of its features, we could break those other
-- * clients nondeterministically, and that would probably be a nightmare to
-- * debug.
-+ * be able to safely skip that bit. But we have to tell libcurl to
-+ * initialize everything else, because other pieces of our client
-+ * executable may already be using libcurl for their own purposes. If we
-+ * initialize libcurl first, with only a subset of its features, we could
-+ * break those other clients nondeterministically, and that would probably
-+ * be a nightmare to debug.
- */
- curl_global_init(CURL_GLOBAL_ALL
- & ~CURL_GLOBAL_WIN32); /* we already initialized Winsock */
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
-
- initPQExpBuffer(&actx->work_data);
- initPQExpBuffer(&actx->errbuf);
-+ initPQExpBuffer(&actx->curl_err);
- if (!setup_multiplexer(actx))
- goto error_return;
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
* errors; anything else and we bail.
*/
@@ src/interfaces/libpq/fe-auth-oauth-curl.c: pg_fe_run_oauth_flow(PGconn *conn, pg
goto error_return;
}
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
- */
- if (strcmp(err->error, "slow_down") == 0)
- {
-- actx->authz.interval += 5; /* TODO check for overflow? */
-+ int prev_interval = actx->authz.interval;
-+
-+ actx->authz.interval += 5;
-+ if (actx->authz.interval < prev_interval)
-+ {
-+ actx_error(actx, "slow_down interval overflow");
-+ goto error_return;
-+ }
- }
-
- /*
-@@ src/interfaces/libpq/fe-auth-oauth-curl.c: error_return:
- else
- appendPQExpBufferStr(&conn->errorMessage, actx->errbuf.data);
-
-- if (actx->curl_err[0])
-- {
-- size_t len;
--
-- appendPQExpBuffer(&conn->errorMessage, " (%s)", actx->curl_err);
--
-- /* Sometimes libcurl adds a newline to the error buffer. :( */
-- len = conn->errorMessage.len;
-- if (len >= 2 && conn->errorMessage.data[len - 2] == '\n')
-- {
-- conn->errorMessage.data[len - 2] = ')';
-- conn->errorMessage.data[len - 1] = '\0';
-- conn->errorMessage.len--;
-- }
-- }
-+ if (actx->curl_err.len > 0)
-+ appendPQExpBuffer(&conn->errorMessage, " (%s)", actx->curl_err.data);
-
- appendPQExpBufferStr(&conn->errorMessage, "\n");
-
## src/interfaces/libpq/fe-auth-oauth.c ##
-@@
- * fe-auth-oauth.c
- * The front-end (client) implementation of OAuth/OIDC authentication.
- *
-- * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
-+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
- * Portions Copyright (c) 1994, Regents of the University of California
- *
- * IDENTIFICATION
@@ src/interfaces/libpq/fe-auth-oauth.c: handle_oauth_sasl_error(PGconn *conn, char *msg, int msglen)
return false;
}
@@ src/interfaces/libpq/fe-auth-oauth.c: handle_oauth_sasl_error(PGconn *conn, char
initPQExpBuffer(&ctx.errbuf);
sem.semstate = &ctx;
-
- ## src/interfaces/libpq/fe-auth-oauth.h ##
-@@
- *
- * Definitions for OAuth authentication implementations
- *
-- * Portions Copyright (c) 2023, PostgreSQL Global Development Group
-+ * Portions Copyright (c) 2024, PostgreSQL Global Development Group
- *
- * src/interfaces/libpq/fe-auth-oauth.h
- *
-
- ## src/test/modules/oauth_validator/validator.c ##
-@@ src/test/modules/oauth_validator/validator.c: validate_token(ValidatorModuleState *state, const char *token, const char *role)
- /* Check to make sure our private state still exists. */
- if (state->private_data != PRIVATE_COOKIE)
- elog(ERROR, "oauth_validator: private state cookie changed to %p",
-- state->private_data);
-+ state->private_data);
-
- res = palloc(sizeof(ValidatorModuleResult));
-
-
- ## src/test/perl/PostgreSQL/Test/OAuthServer.pm ##
-@@ src/test/perl/PostgreSQL/Test/OAuthServer.pm: package PostgreSQL::Test::OAuthServer;
-
- use warnings;
- use strict;
--use threads;
- use Scalar::Util;
- use Socket;
- use IO::Select;
-+use Test::More;
-
- local *server_socket;
-
-@@ src/test/perl/PostgreSQL/Test/OAuthServer.pm: sub run
- my $port;
-
- my $pid = open(my $read_fh, "-|", $ENV{PYTHON}, "t/oauth_server.py")
-- // die "failed to start OAuth server: $!";
-+ or die "failed to start OAuth server: $!";
-
-- read($read_fh, $port, 7) // die "failed to read port number: $!";
-+ read($read_fh, $port, 7) or die "failed to read port number: $!";
- chomp $port;
- die "server did not advertise a valid port"
- unless Scalar::Util::looks_like_number($port);
-@@ src/test/perl/PostgreSQL/Test/OAuthServer.pm: sub run
- $self->{'port'} = $port;
- $self->{'child'} = $read_fh;
-
-- print("# OAuth provider (PID $pid) is listening on port $port\n");
-+ diag("OAuth provider (PID $pid) is listening on port $port\n");
- }
-
- sub stop
- {
- my $self = shift;
-
-- print("# Sending SIGTERM to OAuth provider PID: $self->{'pid'}\n");
-+ diag("Sending SIGTERM to OAuth provider PID: $self->{'pid'}\n");
-
- kill(15, $self->{'pid'});
- $self->{'pid'} = undef;
-
- ## src/tools/pgindent/typedefs.list ##
-@@ src/tools/pgindent/typedefs.list: VacuumStmt
- ValidIOData
- ValidateIndexState
- ValidatorModuleState
-+ValidatorModuleResult
- ValuesScan
- ValuesScanState
- Var
-: ---------- > 7: 9117bf8be2 DO NOT MERGE: Add pytest suite for OAuth