since-v35.diff.txt
text/plain
Filename: since-v35.diff.txt
Type: text/plain
Part: 0
1: dc4f869365 ! 1: 5730b875b8 Add OAUTHBEARER SASL mechanism
@@ Commit message
Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
## .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 libpq_encryption load_balance
++ PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance oauth
+
+
+ # 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'
@@ .cirrus.tasks.yml: task:
# 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:
+ --buildtype=debug \
-Dcassert=true -Dinjection_points=true \
-Duuid=bsd -Dtcl_version=tcl86 -Ddtrace=auto \
- -DPG_TEST_EXTRA="$PG_TEST_EXTRA" \
-+ -Doauth=curl \
++ -Dbuiltin_oauth=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-builtin-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
++ -Dbuiltin_oauth=curl
-Duuid=e2fs
@@ configure: with_uuid
with_readline
with_systemd
with_selinux
-+with_oauth
++with_builtin_oauth
with_ldap
with_krb_srvnam
krb_srvtab
@@ configure: with_krb_srvnam
with_pam
with_bsd_auth
with_ldap
-+with_oauth
++with_builtin_oauth
with_bonjour
with_selinux
with_systemd
@@ configure: Optional Packages:
--with-pam build with PAM support
--with-bsd-auth build with BSD Authentication support
--with-ldap build with LDAP support
-+ --with-oauth=LIB use LIB for OAuth 2.0 support (curl)
++ --with-builtin-oauth=LIB
++ use LIB for built-in OAuth 2.0 client flows (curl)
--with-bonjour build with Bonjour support
--with-selinux build with SELinux support
--with-systemd build with systemd support
@@ configure: $as_echo "$with_ldap" >&6; }
+#
+# OAuth 2.0
+#
-+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with OAuth support" >&5
-+$as_echo_n "checking whether to build with OAuth support... " >&6; }
++{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with built-in OAuth client support" >&5
++$as_echo_n "checking whether to build with built-in OAuth client support... " >&6; }
+
+
+
-+# Check whether --with-oauth was given.
-+if test "${with_oauth+set}" = set; then :
-+ withval=$with_oauth;
++# Check whether --with-builtin-oauth was given.
++if test "${with_builtin_oauth+set}" = set; then :
++ withval=$with_builtin_oauth;
+ case $withval in
+ yes)
-+ as_fn_error $? "argument required for --with-oauth option" "$LINENO" 5
++ as_fn_error $? "argument required for --with-builtin-oauth option" "$LINENO" 5
+ ;;
+ no)
-+ as_fn_error $? "argument required for --with-oauth option" "$LINENO" 5
++ as_fn_error $? "argument required for --with-builtin-oauth option" "$LINENO" 5
+ ;;
+ *)
+
@@ configure: $as_echo "$with_ldap" >&6; }
+fi
+
+
-+if test x"$with_oauth" = x"" ; then
-+ with_oauth=no
++if test x"$with_builtin_oauth" = x"" ; then
++ with_builtin_oauth=no
+fi
+
-+if test x"$with_oauth" = x"curl"; then
++if test x"$with_builtin_oauth" = x"curl"; then
+
-+$as_echo "#define USE_OAUTH 1" >>confdefs.h
++$as_echo "#define USE_BUILTIN_OAUTH 1" >>confdefs.h
+
+
+$as_echo "#define USE_OAUTH_CURL 1" >>confdefs.h
@@ configure: $as_echo "$with_ldap" >&6; }
+ { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests requires --with-python to run" >&5
+$as_echo "$as_me: WARNING: *** OAuth support tests requires --with-python to run" >&2;}
+ fi
-+elif test x"$with_oauth" != x"no"; then
-+ as_fn_error $? "--with-oauth must specify curl" "$LINENO" 5
++elif test x"$with_builtin_oauth" != x"no"; then
++ as_fn_error $? "--with-builtin-oauth must specify curl" "$LINENO" 5
+fi
+
-+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_oauth" >&5
-+$as_echo "$with_oauth" >&6; }
++{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_builtin_oauth" >&5
++$as_echo "$with_builtin_oauth" >&6; }
+
+
+
@@ configure: $as_echo "$with_ldap" >&6; }
#
@@ configure: fi
+ fi
-
-+if test "$with_oauth" = curl ; then
++# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
++# during gss_acquire_cred(). This is possibly related to Curl's Heimdal
++# dependency on that platform?
++if test "$with_builtin_oauth" = curl ; then
+ { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_multi_init in -lcurl" >&5
+$as_echo_n "checking for curl_multi_init in -lcurl... " >&6; }
+if ${ac_cv_lib_curl_curl_multi_init+:} false; then :
@@ configure: fi
+ LIBS="-lcurl $LIBS"
+
+else
-+ as_fn_error $? "library 'curl' is required for --with-oauth=curl" "$LINENO" 5
++ as_fn_error $? "library 'curl' is required for --with-builtin-oauth=curl" "$LINENO" 5
+fi
+
+ { $as_echo "$as_me:${as_lineno-$LINENO}: checking for compatible libcurl" >&5
@@ configure: fi
+fi
+fi
+
- # for contrib/sepgsql
- if test "$with_selinux" = yes; then
- { $as_echo "$as_me:${as_lineno-$LINENO}: checking for security_compute_create_name in -lselinux" >&5
+ if test "$with_gssapi" = yes ; then
+ if test "$PORTNAME" != "win32"; then
+ { $as_echo "$as_me:${as_lineno-$LINENO}: checking for library containing gss_store_cred_into" >&5
@@ configure: fi
done
+fi
+
-+if test "$with_oauth" = curl; then
++if test "$with_builtin_oauth" = curl; then
+ ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default"
+if test "x$ac_cv_header_curl_curl_h" = xyes; then :
+
+else
-+ as_fn_error $? "header file <curl/curl.h> is required for OAuth" "$LINENO" 5
++ as_fn_error $? "header file <curl/curl.h> is required for --with-builtin-oauth=curl" "$LINENO" 5
+fi
+
+
@@ configure.ac: AC_MSG_RESULT([$with_ldap])
+#
+# OAuth 2.0
+#
-+AC_MSG_CHECKING([whether to build with OAuth support])
-+PGAC_ARG_REQ(with, oauth, [LIB], [use LIB for OAuth 2.0 support (curl)])
-+if test x"$with_oauth" = x"" ; then
-+ with_oauth=no
++AC_MSG_CHECKING([whether to build with built-in OAuth client support])
++PGAC_ARG_REQ(with, builtin-oauth, [LIB], [use LIB for built-in OAuth 2.0 client flows (curl)])
++if test x"$with_builtin_oauth" = x"" ; then
++ with_builtin_oauth=no
+fi
+
-+if test x"$with_oauth" = x"curl"; then
-+ AC_DEFINE([USE_OAUTH], 1, [Define to 1 to build with OAuth 2.0 support. (--with-oauth)])
-+ AC_DEFINE([USE_OAUTH_CURL], 1, [Define to 1 to use libcurl for OAuth support.])
++if test x"$with_builtin_oauth" = x"curl"; then
++ AC_DEFINE([USE_BUILTIN_OAUTH], 1, [Define to 1 to build with OAuth 2.0 client flows. (--with-builtin-oauth)])
++ AC_DEFINE([USE_OAUTH_CURL], 1, [Define to 1 to use libcurl for OAuth client flows.])
+ # OAuth requires python for testing
+ if test "$with_python" != yes; then
+ AC_MSG_WARN([*** OAuth support tests requires --with-python to run])
+ fi
-+elif test x"$with_oauth" != x"no"; then
-+ AC_MSG_ERROR([--with-oauth must specify curl])
++elif test x"$with_builtin_oauth" != x"no"; then
++ AC_MSG_ERROR([--with-builtin-oauth must specify curl])
+fi
+
-+AC_MSG_RESULT([$with_oauth])
-+AC_SUBST(with_oauth)
++AC_MSG_RESULT([$with_builtin_oauth])
++AC_SUBST(with_builtin_oauth)
+
+
#
# Bonjour
#
-@@ configure.ac: fi
- AC_SUBST(LDAP_LIBS_FE)
- AC_SUBST(LDAP_LIBS_BE)
+@@ configure.ac: failure. It is possible the compiler isn't looking in the proper directory.
+ Use --without-zlib to disable zlib support.])])
+ 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])])
++# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
++# during gss_acquire_cred(). This is possibly related to Curl's Heimdal
++# dependency on that platform?
++if test "$with_builtin_oauth" = curl ; then
++ AC_CHECK_LIB(curl, curl_multi_init, [], [AC_MSG_ERROR([library 'curl' is required for --with-builtin-oauth=curl])])
+ PGAC_CHECK_LIBCURL
+fi
+
- # for contrib/sepgsql
- if test "$with_selinux" = yes; then
- AC_CHECK_LIB(selinux, security_compute_create_name, [],
+ if test "$with_gssapi" = yes ; then
+ if test "$PORTNAME" != "win32"; then
+ AC_SEARCH_LIBS(gss_store_cred_into, [gssapi_krb5 gss 'gssapi -lkrb5 -lcrypto'], [],
@@ configure.ac: elif test "$with_uuid" = ossp ; then
[AC_MSG_ERROR([header file <ossp/uuid.h> or <uuid.h> is required for OSSP UUID])])])
fi
-+if test "$with_oauth" = curl; then
-+ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> is required for OAuth])])
++if test "$with_builtin_oauth" = curl; then
++ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> is required for --with-builtin-oauth=curl])])
+fi
+
if test "$PORTNAME" = "win32" ; then
@@ doc/src/sgml/installation.sgml: build-postgresql:
</listitem>
</varlistentry>
-+ <varlistentry id="configure-option-with-oauth">
-+ <term><option>--with-oauth=<replaceable>LIBRARY</replaceable></option></term>
++ <varlistentry id="configure-option-with-builtin-oauth">
++ <term><option>--with-builtin-oauth=<replaceable>LIBRARY</replaceable></option></term>
+ <listitem>
+ <para>
-+ Build with OAuth authentication and authorization support. The only
++ Build with support for OAuth 2.0 client flows. The only
+ <replaceable>LIBRARY</replaceable> supported is <option>curl</option>.
+ This requires the <productname>curl</productname> package to be
+ installed. Building with this will check for the required header files
@@ doc/src/sgml/installation.sgml: ninja install
</listitem>
</varlistentry>
-+ <varlistentry id="configure-with-oauth">
-+ <term><option>-Doauth={ auto | <replaceable>LIBRARY</replaceable> | disabled }</option></term>
++ <varlistentry id="configure-with-builtin-oauth">
++ <term><option>-Dbuiltin_oauth={ auto | <replaceable>LIBRARY</replaceable> | disabled }</option></term>
+ <listitem>
+ <para>
-+ Build with OAuth authentication and authorization support. The only
++ Build with support for OAuth 2.0 client flows. The only
+ <replaceable>LIBRARY</replaceable> supported is <option>curl</option>.
+ This requires the <productname>curl</productname> package to be
+ installed. Building with this will check for the required header files
@@ meson.build: endif
+###############################################################
-+# Library: oauth
++# Library: OAuth (libcurl)
+###############################################################
+
-+oauth = not_found_dep
++libcurl = not_found_dep
+oauth_library = 'none'
-+oauthopt = get_option('oauth')
++oauthopt = get_option('builtin_oauth')
+
+if oauthopt == 'auto' and auto_features.disabled()
+ oauthopt = 'none'
@@ meson.build: endif
+if oauthopt in ['auto', '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()
++ libcurl = dependency('libcurl', version: '>= 7.61.0',
++ required: (oauthopt == 'curl'))
++ if libcurl.found()
+ oauth_library = 'curl'
-+ cdata.set('USE_OAUTH', 1)
++ cdata.set('USE_BUILTIN_OAUTH', 1)
+ cdata.set('USE_OAUTH_CURL', 1)
+ endif
+endif
+
-+if oauthopt == 'auto' and auto_features.enabled() and not oauth.found()
++if oauthopt == 'auto' and auto_features.enabled() and not libcurl.found()
+ error('no OAuth implementation library found')
+endif
+
@@ meson.build: endif
# Library: Tcl (for pltcl)
#
@@ meson.build: libpq_deps += [
+
gssapi,
ldap_r,
++ # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
++ # during gss_acquire_cred(). This is possibly related to Curl's Heimdal
++ # dependency on that platform?
++ libcurl,
libintl,
-+ oauth,
ssl,
]
-
@@ meson.build: if meson.version().version_compare('>=0.57')
+ 'gss': gssapi,
+ 'icu': icu,
+ 'ldap': ldap,
++ 'libcurl': libcurl,
+ 'libxml': libxml,
+ 'libxslt': libxslt,
'llvm': llvm,
- 'lz4': lz4,
- 'nls': libintl,
-+ 'oauth': oauth,
- 'openssl': ssl,
- 'pam': pam,
- 'plperl': perl_dep,
## meson_options.txt ##
-@@ meson_options.txt: option('lz4', type: 'feature', value: 'auto',
- option('nls', type: 'feature', value: 'auto',
- description: 'Native language support')
+@@ meson_options.txt: option('bonjour', type: 'feature', value: 'auto',
+ option('bsd_auth', type: 'feature', value: 'auto',
+ description: 'BSD Authentication support')
-+option('oauth', type : 'combo', choices : ['auto', 'none', 'curl'],
++option('builtin_oauth', type : 'combo', choices : ['auto', 'none', 'curl'],
+ value: 'auto',
-+ description: 'use LIB for OAuth 2.0 support (curl)')
++ description: 'use LIB for built-in OAuth 2.0 client flows (curl)')
+
- option('pam', type: 'feature', value: 'auto',
- description: 'PAM support')
+ option('docs', type: 'feature', value: 'auto',
+ description: 'Documentation in HTML and man page format')
## src/Makefile.global.in ##
@@ src/Makefile.global.in: with_ldap = @with_ldap@
with_libxml = @with_libxml@
with_libxslt = @with_libxslt@
with_llvm = @with_llvm@
-+with_oauth = @with_oauth@
++with_builtin_oauth = @with_builtin_oauth@
with_system_tzdata = @with_system_tzdata@
with_uuid = @with_uuid@
with_zlib = @with_zlib@
@@ src/include/pg_config.h.in
/* Define to 1 if you have the `ldap' library (-lldap). */
#undef HAVE_LIBLDAP
+@@
+ /* Define to 1 to build with BSD Authentication support. (--with-bsd-auth) */
+ #undef USE_BSD_AUTH
+
++/* Define to 1 to build with OAuth 2.0 client flows. (--with-builtin-oauth) */
++#undef USE_BUILTIN_OAUTH
++
+ /* Define to build with ICU support. (--with-icu) */
+ #undef USE_ICU
+
@@
/* Define to select named POSIX semaphores. */
#undef USE_NAMED_POSIX_SEMAPHORES
-+/* Define to 1 to build with OAuth 2.0 support. (--with-oauth) */
-+#undef USE_OAUTH
-+
-+/* Define to 1 to use libcurl for OAuth support. */
++/* Define to 1 to use libcurl for OAuth client flows. */
+#undef USE_OAUTH_CURL
+
/* Define to 1 to build with OpenSSL support. (--with-ssl=openssl) */
@@ src/include/pg_config.h.in
## src/interfaces/libpq/Makefile ##
+@@ src/interfaces/libpq/Makefile: endif
+
+ OBJS = \
+ $(WIN32RES) \
++ fe-auth-oauth.o \
+ fe-auth-scram.o \
+ fe-cancel.o \
+ fe-connect.o \
@@ src/interfaces/libpq/Makefile: OBJS += \
fe-secure-gssapi.o
endif
-+ifneq ($(with_oauth),no)
-+OBJS += fe-auth-oauth.o
-+
-+ifeq ($(with_oauth),curl)
++ifeq ($(with_builtin_oauth),curl)
+OBJS += fe-auth-oauth-curl.o
+endif
-+endif
+
ifeq ($(PORTNAME), cygwin)
override shlib = cyg$(NAME)$(DLSUFFIX)
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+
+ if (tok.access_token)
+ {
-+ /* Construct our Bearer token. */
-+ resetPQExpBuffer(&actx->work_data);
-+ appendPQExpBuffer(&actx->work_data, "Bearer %s", tok.access_token);
-+
-+ if (PQExpBufferDataBroken(actx->work_data))
-+ {
-+ actx_error(actx, "out of memory");
-+ goto token_cleanup;
-+ }
-+
-+ *token = strdup(actx->work_data.data);
-+ if (!*token)
-+ {
-+ actx_error(actx, "out of memory");
-+ goto token_cleanup;
-+ }
++ *token = tok.access_token;
++ tok.access_token = NULL;
+
+ success = true;
+ goto token_cleanup;
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+
+#define kvsep "\x01"
+
++/*
++ * Constructs an OAUTHBEARER client initial response (RFC 7628, Sec. 3.1).
++ *
++ * If discover is true, the token pointer will be ignored and the initial
++ * response will instead contain a request for the server's required OAuth
++ * parameters (Sec. 4.3). Otherwise, a bearer token must be provided.
++ *
++ * Returns the response as a null-terminated string, or NULL on error.
++ */
+static char *
-+client_initial_response(PGconn *conn, const char *token)
++client_initial_response(PGconn *conn, bool discover, const char *token)
+{
-+ static const char *const resp_format = "n,," kvsep "auth=%s" kvsep kvsep;
++ static const char *const resp_format = "n,," kvsep "auth=%s%s" kvsep kvsep;
+
+ PQExpBufferData buf;
++ const char *authn_scheme;
+ char *response = NULL;
+
-+ if (!token)
++ if (discover)
++ {
++ /* Parameter discovery uses a completely empty auth value. */
++ authn_scheme = token = "";
++ }
++ else
+ {
+ /*
-+ * Either programmer error, or something went badly wrong during the
-+ * asynchronous fetch.
-+ *
-+ * TODO: users shouldn't see this; what action should they take if
-+ * they do?
++ * Use a Bearer authentication scheme (RFC 6750, Sec. 2.1). A trailing
++ * space is used as a separator.
+ */
-+ libpq_append_conn_error(conn, "no OAuth token was set for the connection");
-+ return NULL;
++ authn_scheme = "Bearer ";
++
++ /* We must have a token. */
++ if (!token)
++ {
++ /*
++ * Either programmer error, or something went badly wrong during
++ * the asynchronous fetch.
++ *
++ * TODO: users shouldn't see this; what action should they take if
++ * they do?
++ */
++ libpq_append_conn_error(conn, "no OAuth token was set for the connection");
++ return NULL;
++ }
+ }
+
+ initPQExpBuffer(&buf);
-+ appendPQExpBuffer(&buf, resp_format, token);
++ appendPQExpBuffer(&buf, resp_format, authn_scheme, token);
+
+ if (!PQExpBufferDataBroken(buf))
+ response = strdup(buf.data);
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ * onto the original string, since it may not be safe for us to free()
+ * it.)
+ */
-+ PQExpBufferData token;
-+
+ if (!request->token)
+ {
+ libpq_append_conn_error(conn, "user-defined OAuth flow did not provide a token");
+ return PGRES_POLLING_FAILED;
+ }
+
-+ initPQExpBuffer(&token);
-+ appendPQExpBuffer(&token, "Bearer %s", request->token);
-+
-+ if (PQExpBufferDataBroken(token))
++ state->token = strdup(request->token);
++ if (!state->token)
+ {
+ libpq_append_conn_error(conn, "out of memory");
+ return PGRES_POLLING_FAILED;
+ }
+
-+ state->token = token.data;
+ return PGRES_POLLING_OK;
+ }
+
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ * hold onto the original string, since it may not be safe for us
+ * to free() it.)
+ */
-+ PQExpBufferData token;
-+
-+ initPQExpBuffer(&token);
-+ appendPQExpBuffer(&token, "Bearer %s", request.token);
-+
-+ if (PQExpBufferDataBroken(token))
++ state->token = strdup(request.token);
++ if (!state->token)
+ {
+ libpq_append_conn_error(conn, "out of memory");
+ goto fail;
+ }
+
-+ state->token = token.data;
-+
+ /* short-circuit */
+ if (request.cleanup)
+ request.cleanup(conn, &request);
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ }
+ else
+ {
++#if USE_BUILTIN_OAUTH
+ /*
-+ * Use our built-in OAuth flow.
++ * Hand off to our built-in OAuth flow.
+ *
+ * Only allow one try per connection, since we're not performing any
+ * caching at the moment. (Custom flows might be more sophisticated.)
+ */
+ conn->async_auth = pg_fe_run_oauth_flow;
+ conn->oauth_want_retry = PG_BOOL_NO;
++
++#else
++ libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built using --with-builtin-oauth");
++ goto fail;
++
++#endif
+ }
+
+ return true;
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+{
+ fe_oauth_state *state = opaq;
+ PGconn *conn = state->conn;
++ bool discover = false;
+
+ *output = NULL;
+ *outputlen = 0;
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ switch (state->state)
+ {
+ case FE_OAUTH_INIT:
++ /* We begin in the initial response phase. */
+ Assert(inputlen == -1);
+
+ if (!derive_discovery_uri(conn))
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ {
+ /*
+ * If we don't have a discovery URI to be able to request a
-+ * token, we ask the server for one explicitly with an empty
-+ * token. This doesn't require any asynchronous work.
++ * token, we ask the server for one explicitly. This doesn't
++ * require any asynchronous work.
+ */
-+ state->token = strdup("");
-+ if (!state->token)
-+ {
-+ libpq_append_conn_error(conn, "out of memory");
-+ return SASL_FAILED;
-+ }
++ discover = true;
+ }
+
+ /* fall through */
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ /* We should still be in the initial response phase. */
+ Assert(inputlen == -1);
+
-+ *output = client_initial_response(conn, state->token);
++ *output = client_initial_response(conn, discover, state->token);
+ if (!*output)
+ return SASL_FAILED;
+
@@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
conn->sasl = &pg_scram_mech;
conn->password_needed = true;
}
-+#ifdef USE_OAUTH
+ else if (strcmp(mechanism_buf.data, OAUTHBEARER_NAME) == 0 &&
+ !selected_mechanism)
+ {
@@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
+ conn->sasl = &pg_oauth_mech;
+ conn->password_needed = false;
+ }
-+#endif
}
if (!selected_mechanism)
@@ src/interfaces/libpq/fe-connect.c: keep_going: /* We will come back to here
/* Check to see if we should mention pgpassfile */
pgpassfileWarning(conn);
-+#ifdef USE_OAUTH
++ /*
++ * OAuth connections may perform two-step discovery, where
++ * the first connection is a dummy.
++ */
+ if (conn->sasl == &pg_oauth_mech
+ && conn->oauth_want_retry == PG_BOOL_YES)
+ {
@@ src/interfaces/libpq/fe-connect.c: keep_going: /* We will come back to here
+ need_new_connection = true;
+ goto keep_going;
+ }
-+#endif
+
CONNECTION_FAILED();
}
@@ src/interfaces/libpq/fe-misc.c: pqSocketCheck(PGconn *conn, int forRead, int for
if (result < 0)
## src/interfaces/libpq/libpq-fe.h ##
+@@ src/interfaces/libpq/libpq-fe.h: extern "C"
+ */
+ #include "postgres_ext.h"
+
++#ifdef WIN32
++#include <winsock2.h> /* for SOCKET */
++#endif
++
+ /*
+ * These symbols may be used in compile-time #ifdef tests for the availability
+ * of v14-and-newer libpq features.
@@ src/interfaces/libpq/libpq-fe.h: extern "C"
/* Features added in PostgreSQL v18: */
/* Indicates presence of PQfullProtocolVersion */
@@ src/interfaces/libpq/libpq-fe.h: extern int PQenv2encoding(void);
+ const char *user_code; /* user code to enter */
+} PQpromptOAuthDevice;
+
++/* for _PQoauthBearerRequest.async() */
++#ifdef WIN32
++#define SOCKTYPE SOCKET
++#else
++#define SOCKTYPE int
++#endif
++
+typedef struct _PQoauthBearerRequest
+{
+ /* Hook inputs (constant across all calls) */
@@ src/interfaces/libpq/libpq-fe.h: extern int PQenv2encoding(void);
+ */
+ PostgresPollingStatusType (*async) (PGconn *conn,
+ struct _PQoauthBearerRequest *request,
-+ int *altsock);
++ SOCKTYPE *altsock);
+
+ /*
+ * Callback to clean up custom allocations. A hook implementation may use
@@ src/interfaces/libpq/libpq-fe.h: extern int PQenv2encoding(void);
+ */
+ void *user;
+} PQoauthBearerRequest;
++
++#undef SOCKTYPE
+
extern char *PQencryptPassword(const char *passwd, const char *user);
extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm);
@@ src/interfaces/libpq/libpq-int.h: struct pg_conn
PGLoadBalanceType load_balance_type; /* desired load balancing
## src/interfaces/libpq/meson.build ##
+@@
+ # args for executables (which depend on libpq).
+
+ libpq_sources = files(
++ 'fe-auth-oauth.c',
+ 'fe-auth-scram.c',
+ 'fe-auth.c',
+ 'fe-cancel.c',
@@ src/interfaces/libpq/meson.build: if gssapi.found()
)
endif
-+if oauth.found()
-+ libpq_sources += files('fe-auth-oauth.c')
-+ if oauth_library == 'curl'
-+ libpq_sources += files('fe-auth-oauth-curl.c')
-+ endif
++if oauth_library == 'curl'
++ libpq_sources += files('fe-auth-oauth-curl.c')
+endif
+
export_file = custom_target('libpq.exports',
@@ src/interfaces/libpq/pqexpbuffer.h: extern void initPQExpBuffer(PQExpBuffer str)
* Reset a PQExpBuffer to empty
## src/makefiles/meson.build ##
+@@ src/makefiles/meson.build: pgxs_kv = {
+ 'SUN_STUDIO_CC': 'no', # not supported so far
+
+ # want the chosen option, rather than the library
++ 'with_builtin_oauth' : oauth_library,
+ 'with_ssl' : ssl_library,
+ 'with_uuid': uuidopt,
+
@@ src/makefiles/meson.build: pgxs_deps = {
+ 'gssapi': gssapi,
+ 'icu': icu,
+ 'ldap': ldap,
++ 'libcurl': libcurl,
+ 'libxml': libxml,
+ 'libxslt': libxslt,
'llvm': llvm,
- 'lz4': lz4,
- 'nls': libintl,
-+ 'oauth': oauth,
- 'pam': pam,
- 'perl': perl_dep,
- 'python': python3_dep,
## src/test/modules/Makefile ##
@@ src/test/modules/Makefile: SUBDIRS = \
@@ src/test/modules/oauth_validator/Makefile (new)
+MODULES = validator
+PGFILEDESC = "validator - test OAuth validator module"
+
++PROGRAM = oauth_hook_client
++PGAPPICON = win32
++OBJS = $(WIN32RES) oauth_hook_client.o
++
++PG_CPPFLAGS = -I$(libpq_srcdir)
++PG_LIBS_INTERNAL += $(libpq_pgport)
++
+NO_INSTALLCHECK = 1
+
+TAP_TESTS = 1
@@ src/test/modules/oauth_validator/Makefile (new)
+include $(top_srcdir)/contrib/contrib-global.mk
+
+export PYTHON
-+export with_oauth
++export with_builtin_oauth
+export with_python
+
+endif
@@ src/test/modules/oauth_validator/meson.build (new)
+)
+test_install_libs += validator
+
++oauth_hook_client_sources = files(
++ 'oauth_hook_client.c',
++)
++
++if host_system == 'windows'
++ oauth_hook_client_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
++ '--NAME', 'oauth_hook_client',
++ '--FILEDESC', 'oauth_hook_client - test program for libpq OAuth hooks',])
++endif
++
++oauth_hook_client = executable('oauth_hook_client',
++ oauth_hook_client_sources,
++ dependencies: [frontend_code, libpq],
++ kwargs: default_bin_args + {
++ 'install': false,
++ },
++)
++testprep_targets += oauth_hook_client
++
+tests += {
+ 'name': 'oauth_validator',
+ 'sd': meson.current_source_dir(),
@@ src/test/modules/oauth_validator/meson.build (new)
+ 'tap': {
+ 'tests': [
+ 't/001_server.pl',
++ 't/002_client.pl',
+ ],
+ 'env': {
+ 'PYTHON': python.path(),
-+ 'with_oauth': oauth_library,
++ 'with_builtin_oauth': oauth_library,
+ 'with_python': 'yes',
+ },
+ },
++}
+
+ ## src/test/modules/oauth_validator/oauth_hook_client.c (new) ##
+@@
++/*-------------------------------------------------------------------------
++ *
++ * oauth_hook_client.c
++ * Verify OAuth hook functionality in libpq
++ *
++ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
++ * Portions Copyright (c) 1994, Regents of the University of California
++ *
++ *
++ * IDENTIFICATION
++ * src/test/modules/oauth_validator/oauth_hook_client.c
++ *
++ *-------------------------------------------------------------------------
++ */
++
++#include "postgres_fe.h"
++
++#include <stdio.h>
++#include <stdlib.h>
++
++#include "getopt_long.h"
++#include "libpq-fe.h"
++
++static int handle_auth_data(PGAuthData type, PGconn *conn, void *data);
++
++static void
++usage(char *argv[])
++{
++ fprintf(stderr, "usage: %s [flags] CONNINFO\n\n", argv[0]);
++
++ fprintf(stderr, "recognized flags:\n");
++ fprintf(stderr, " -h, --help show this message\n");
++ fprintf(stderr, " --expected-scope SCOPE fail if received scopes do not match SCOPE\n");
++ fprintf(stderr, " --expected-uri URI fail if received configuration link does not match URI\n");
++ fprintf(stderr, " --no-hook don't install OAuth hooks (connection will fail)\n");
++ fprintf(stderr, " --token TOKEN use the provided TOKEN value\n");
++}
++
++static bool no_hook = false;
++static const char *expected_uri = NULL;
++static const char *expected_scope = NULL;
++static char *token = NULL;
++
++int
++main(int argc, char *argv[])
++{
++ static const struct option long_options[] = {
++ {"help", no_argument, NULL, 'h'},
++
++ {"expected-scope", required_argument, NULL, 1000},
++ {"expected-uri", required_argument, NULL, 1001},
++ {"no-hook", no_argument, NULL, 1002},
++ {"token", required_argument, NULL, 1003},
++ {0}
++ };
++
++ const char *conninfo;
++ PGconn *conn;
++ int c;
++
++ while ((c = getopt_long(argc, argv, "h", long_options, NULL)) != -1)
++ {
++ switch (c)
++ {
++ case 'h':
++ usage(argv);
++ return 0;
++
++ case 1000: /* --expected-scope */
++ expected_scope = optarg;
++ break;
++
++ case 1001: /* --expected-uri */
++ expected_uri = optarg;
++ break;
++
++ case 1002: /* --no-hook */
++ no_hook = true;
++ break;
++
++ case 1003: /* --token */
++ token = optarg;
++ break;
++
++ default:
++ usage(argv);
++ return 1;
++ }
++ }
++
++ if (argc != optind + 1)
++ {
++ usage(argv);
++ return 1;
++ }
++
++ conninfo = argv[optind];
++
++ /* Set up our OAuth hooks. */
++ PQsetAuthDataHook(handle_auth_data);
++
++ /* Connect. (All the actual work is in the hook.) */
++ conn = PQconnectdb(conninfo);
++ if (PQstatus(conn) != CONNECTION_OK)
++ {
++ fprintf(stderr, "Connection to database failed: %s\n",
++ PQerrorMessage(conn));
++ PQfinish(conn);
++ return 1;
++ }
++
++ printf("connection succeeded\n");
++ PQfinish(conn);
++ return 0;
++}
++
++static int
++handle_auth_data(PGAuthData type, PGconn *conn, void *data)
++{
++ PQoauthBearerRequest *req = data;
++
++ if (no_hook || (type != PQAUTHDATA_OAUTH_BEARER_TOKEN))
++ return 0;
++
++ if (expected_uri)
++ {
++ if (!req->openid_configuration)
++ {
++ fprintf(stderr, "expected URI \"%s\", got NULL\n", expected_uri);
++ return -1;
++ }
++
++ if (strcmp(expected_uri, req->openid_configuration) != 0)
++ {
++ fprintf(stderr, "expected URI \"%s\", got \"%s\"\n", expected_uri, req->openid_configuration);
++ return -1;
++ }
++ }
++
++ if (expected_scope)
++ {
++ if (!req->scope)
++ {
++ fprintf(stderr, "expected scope \"%s\", got NULL\n", expected_scope);
++ return -1;
++ }
++
++ if (strcmp(expected_scope, req->scope) != 0)
++ {
++ fprintf(stderr, "expected scope \"%s\", got \"%s\"\n", expected_scope, req->scope);
++ return -1;
++ }
++ }
++
++ req->token = token;
++ return 1;
+}
## src/test/modules/oauth_validator/t/001_server.pl (new) ##
@@ src/test/modules/oauth_validator/t/001_server.pl (new)
+ 'Potentially unsafe test oauth not enabled in PG_TEST_EXTRA';
+}
+
-+if ($ENV{with_oauth} ne 'curl')
++if ($ENV{with_builtin_oauth} ne 'curl')
+{
+ plan skip_all => 'client-side OAuth not supported by this build';
+}
@@ src/test/modules/oauth_validator/t/001_server.pl (new)
+
+$node->stop;
+
++done_testing();
+
+ ## src/test/modules/oauth_validator/t/002_client.pl (new) ##
+@@
++
++# Copyright (c) 2021-2024, PostgreSQL Global Development Group
++
++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;
++use Test::More;
++
++if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\boauth\b/)
++{
++ plan skip_all =>
++ 'Potentially unsafe test oauth not enabled in PG_TEST_EXTRA';
++}
++
++#
++# Cluster Setup
++#
++
++my $node = PostgreSQL::Test::Cluster->new('primary');
++$node->init;
++$node->append_conf('postgresql.conf', "log_connections = on\n");
++$node->append_conf('postgresql.conf',
++ "oauth_validator_library = 'validator'\n");
++$node->start;
++
++$node->safe_psql('postgres', 'CREATE USER test;');
++
++my $issuer = "https://127.0.0.1:54321";
++my $scope = "openid postgres";
++
++unlink($node->data_dir . '/pg_hba.conf');
++$node->append_conf(
++ 'pg_hba.conf', qq{
++local all test oauth issuer="$issuer" scope="$scope"
++});
++$node->reload;
++
++my ($log_start, $log_end);
++$log_start = $node->wait_for_log(qr/reloading configuration files/);
++
++$ENV{PGOAUTHDEBUG} = "UNSAFE";
++
++#
++# Tests
++#
++
++my $user = "test";
++my $base_connstr = $node->connstr() . " user=$user";
++my $common_connstr = "$base_connstr oauth_client_id=myID";
++
++sub test
++{
++ my ($test_name, %params) = @_;
++
++ my $flags = [];
++ if (defined($params{flags}))
++ {
++ $flags = $params{flags};
++ }
++
++ my @cmd = ("oauth_hook_client", @{$flags}, $common_connstr);
++ diag "running '" . join("' '", @cmd) . "'";
++
++ my ($stdout, $stderr) = run_command(\@cmd);
++
++ if (defined($params{expected_stdout}))
++ {
++ like($stdout, $params{expected_stdout}, "$test_name: stdout matches");
++ }
++
++ if (defined($params{expected_stderr}))
++ {
++ like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
++ }
++ else
++ {
++ is($stderr, "", "$test_name: no stderr");
++ }
++}
++
++test(
++ "basic synchronous hook can provide a token",
++ flags => [
++ "--token", "my-token",
++ "--expected-uri", "$issuer/.well-known/openid-configuration",
++ "--expected-scope", $scope,
++ ],
++ expected_stdout => qr/connection succeeded/);
++
++$node->log_check("validator receives correct token",
++ $log_start,
++ log_like => [ qr/oauth_validator: token="my-token", role="$user"/, ]);
++
++if ($ENV{with_builtin_oauth} ne 'curl')
++{
++ # libpq should help users out if no OAuth support is built in.
++ test(
++ "fails without custom hook installed",
++ flags => ["--no-hook"],
++ expected_stderr =>
++ qr/no custom OAuth flows are available, and libpq was not built using --with-builtin-oauth/
++ );
++}
++
+done_testing();
## src/test/modules/oauth_validator/t/oauth_server.py (new) ##
2: cca5de6726 ! 2: 01df79980b DO NOT MERGE: Add pytest suite for OAuth
@@ .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 libpq_encryption load_balance
-+ PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance python
+- PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance oauth
++ PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance oauth python
# What files to preserve in case tests fail
@@ .cirrus.tasks.yml: task:
configure_32_script: |
su postgres <<-EOF
export CC='ccache gcc -m32'
-@@ .cirrus.tasks.yml: task:
- -Dllvm=disabled \
- --pkg-config-path /usr/lib/i386-linux-gnu/pkgconfig/ \
- -DPERL=perl5.36-i386-linux-gnu \
-- -DPG_TEST_EXTRA="$PG_TEST_EXTRA" \
-+ -DPG_TEST_EXTRA="${PG_TEST_EXTRA//"python"}" \
- build-32
- EOF
-
++ export PG_TEST_EXTRA="${PG_TEST_EXTRA//python}"
+ meson setup \
+ --buildtype=debug \
+ -Dcassert=true -Dinjection_points=true \
## meson.build ##
@@ meson.build: else
@@ src/test/python/client/test_oauth.py (new)
+# 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",
++ os.getenv("with_builtin_oauth") == "none",
++ reason="OAuth client tests require --with-builtin-oauth support",
+)
+
+if platform.system() == "Darwin":
@@ src/test/python/meson.build (new)
+subdir('server')
+
+pytest_env = {
-+ 'with_oauth': oauth_library,
++ 'with_builtin_oauth': oauth_library,
+
+ # Point to the default database; the tests will create their own databases as
+ # needed.