diff --git a/config/programs.m4 b/config/programs.m4 index 0a07feb37cc..0ad1e58b48d 100644 --- a/config/programs.m4 +++ b/config/programs.m4 @@ -286,9 +286,20 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], [ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file is required for --with-libcurl])]) - AC_CHECK_LIB(curl, curl_multi_init, [], + AC_CHECK_LIB(curl, curl_multi_init, [ + AC_DEFINE([HAVE_LIBCURL], [1], [Define to 1 if you have the `curl' library (-lcurl).]) + AC_SUBST(LIBCURL_LDLIBS, -lcurl) + ], [AC_MSG_ERROR([library 'curl' does not provide curl_multi_init])]) + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. AC_CACHE_CHECK([for curl_global_init thread safety], [pgac_cv__libcurl_threadsafe_init], @@ -338,4 +349,8 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], *** lookups. Rebuild libcurl with the AsynchDNS feature enabled in order *** to use it with libpq.]) fi + + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS ])# PGAC_CHECK_LIBCURL diff --git a/configure b/configure index 0936010718d..a4c4bcb40ea 100755 --- a/configure +++ b/configure @@ -655,6 +655,7 @@ UUID_LIBS LDAP_LIBS_BE LDAP_LIBS_FE with_ssl +LIBCURL_LDLIBS PTHREAD_CFLAGS PTHREAD_LIBS PTHREAD_CC @@ -711,6 +712,8 @@ with_libxml LIBNUMA_LIBS LIBNUMA_CFLAGS with_libnuma +LIBCURL_LDFLAGS +LIBCURL_CPPFLAGS LIBCURL_LIBS LIBCURL_CFLAGS with_libcurl @@ -9053,19 +9056,27 @@ $as_echo "yes" >&6; } fi - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + + + # OAuth requires python for testing if test "$with_python" != yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests require --with-python to run" >&5 @@ -12704,9 +12715,6 @@ fi fi -# 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_libcurl" = yes ; then ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" @@ -12754,17 +12762,26 @@ fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_multi_init" >&5 $as_echo "$ac_cv_lib_curl_curl_multi_init" >&6; } if test "x$ac_cv_lib_curl_curl_multi_init" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBCURL 1 -_ACEOF - LIBS="-lcurl $LIBS" + +$as_echo "#define HAVE_LIBCURL 1" >>confdefs.h + + LIBCURL_LDLIBS=-lcurl + else as_fn_error $? "library 'curl' does not provide curl_multi_init" "$LINENO" 5 fi + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init thread safety" >&5 @@ -12868,6 +12885,10 @@ $as_echo "$pgac_cv__libcurl_async_dns" >&6; } *** to use it with libpq." "$LINENO" 5 fi + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS + fi if test "$with_gssapi" = yes ; then @@ -14516,6 +14537,13 @@ done fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + as_fn_error $? "client OAuth is not supported on this platform" "$LINENO" 5 + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/configure.ac b/configure.ac index 2a78cddd825..c0471030e90 100644 --- a/configure.ac +++ b/configure.ac @@ -1033,19 +1033,27 @@ if test "$with_libcurl" = yes ; then # to explicitly set TLS 1.3 ciphersuites). PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.61.0]) - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + AC_SUBST(LIBCURL_CPPFLAGS) + AC_SUBST(LIBCURL_LDFLAGS) + # OAuth requires python for testing if test "$with_python" != yes; then AC_MSG_WARN([*** OAuth support tests require --with-python to run]) @@ -1354,9 +1362,6 @@ failure. It is possible the compiler isn't looking in the proper directory. Use --without-zlib to disable zlib support.])]) fi -# 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_libcurl" = yes ; then PGAC_CHECK_LIBCURL fi @@ -1654,6 +1659,13 @@ if test "$PORTNAME" = "win32" ; then AC_CHECK_HEADERS(crtdefs.h) fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + AC_MSG_ERROR([client-side OAuth is not supported on this platform]) + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/doc/src/sgml/installation.sgml b/doc/src/sgml/installation.sgml index e7ffb942bbd..60419312113 100644 --- a/doc/src/sgml/installation.sgml +++ b/doc/src/sgml/installation.sgml @@ -313,6 +313,14 @@ + + + You need Curl to build an optional module + which implements the OAuth Device + Authorization flow for client applications. + + + You need LZ4, if you want to support diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 8cdd2997d43..695fe958c3e 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -10226,15 +10226,20 @@ void PQinitSSL(int do_ssl); OAuth Support - libpq implements support for the OAuth v2 Device Authorization client flow, + libpq implements support for the OAuth v2 Device Authorization client flow, documented in RFC 8628, - which it will attempt to use by default if the server + as an optional module. See the + installation documentation for information on how to enable support + for Device Authorization as a builtin flow. + + + When support is enabled and the optional module installed, libpq + will use the builtin flow by default if the server requests a bearer token during authentication. This flow can be utilized even if the system running the client application does not have a usable web browser, for example when - running a client via SSH. Client applications may implement their own flows - instead; see . + running a client via SSH. The builtin flow will, by default, print a URL to visit and a user code to @@ -10251,6 +10256,11 @@ Visit https://example.com/device and enter the code: ABCD-EFGH they match expectations, before continuing. Permissions should not be given to untrusted third parties. + + Client applications may implement their own flows to customize interaction + and integration with applications. See + for more information on how add a custom flow to libpq. + For an OAuth client flow to be usable, the connection string must at minimum contain and @@ -10366,7 +10376,9 @@ typedef struct _PGpromptOAuthDevice - The OAuth Device Authorization flow included in libpq + The OAuth Device Authorization flow which + can be included + in libpq requires the end user to visit a URL with a browser, then enter a code which permits libpq to connect to the server on their behalf. The default prompt simply prints the @@ -10378,7 +10390,8 @@ typedef struct _PGpromptOAuthDevice This callback is only invoked during the builtin device authorization flow. If the application installs a custom OAuth - flow, this authdata type will not be used. + flow, or libpq was not built with + support for the builtin flow, this authdata type will not be used. If a non-NULL verification_uri_complete is @@ -10400,8 +10413,9 @@ typedef struct _PGpromptOAuthDevice - Replaces the entire OAuth flow with a custom implementation. The hook - should either directly return a Bearer token for the current + Adds a custom implementation of a flow, replacing the builtin flow if + it is installed. + The hook should either directly return a Bearer token for the current user/issuer/scope combination, if one is available without blocking, or else set up an asynchronous callback to retrieve one. diff --git a/meson.build b/meson.build index a1516e54529..29d46c8ad01 100644 --- a/meson.build +++ b/meson.build @@ -107,6 +107,7 @@ os_deps = [] backend_both_deps = [] backend_deps = [] libpq_deps = [] +libpq_oauth_deps = [] pg_sysroot = '' @@ -860,13 +861,13 @@ endif ############################################################### libcurlopt = get_option('libcurl') +oauth_flow_supported = false + if not libcurlopt.disabled() # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability # to explicitly set TLS 1.3 ciphersuites). libcurl = dependency('libcurl', version: '>= 7.61.0', required: libcurlopt) if libcurl.found() - cdata.set('USE_LIBCURL', 1) - # Check to see whether the current platform supports thread-safe Curl # initialization. libcurl_threadsafe_init = false @@ -938,6 +939,22 @@ if not libcurlopt.disabled() endif endif + # Check that the current platform supports our builtin flow. This requires + # libcurl and one of either epoll or kqueue. + oauth_flow_supported = ( + libcurl.found() + and (cc.check_header('sys/event.h', required: false, + args: test_c_args, include_directories: postgres_inc) + or cc.check_header('sys/epoll.h', required: false, + args: test_c_args, include_directories: postgres_inc)) + ) + + if oauth_flow_supported + cdata.set('USE_LIBCURL', 1) + elif libcurlopt.enabled() + error('client-side OAuth is not supported on this platform') + endif + else libcurl = not_found_dep endif @@ -3272,17 +3289,18 @@ 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, ssl, ] +libpq_oauth_deps += [ + libcurl, +] + subdir('src/interfaces/libpq') -# fe_utils depends on libpq +# fe_utils and libpq-oauth depends on libpq subdir('src/fe_utils') +subdir('src/interfaces/libpq-oauth') # for frontend binaries frontend_code = declare_dependency( diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 6722fbdf365..04952b533de 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -347,6 +347,9 @@ perl_embed_ldflags = @perl_embed_ldflags@ AWK = @AWK@ LN_S = @LN_S@ +LIBCURL_CPPFLAGS = @LIBCURL_CPPFLAGS@ +LIBCURL_LDFLAGS = @LIBCURL_LDFLAGS@ +LIBCURL_LDLIBS = @LIBCURL_LDLIBS@ MSGFMT = @MSGFMT@ MSGFMT_FLAGS = @MSGFMT_FLAGS@ MSGMERGE = @MSGMERGE@ diff --git a/src/interfaces/Makefile b/src/interfaces/Makefile index 7d56b29d28f..e6822caa206 100644 --- a/src/interfaces/Makefile +++ b/src/interfaces/Makefile @@ -14,7 +14,19 @@ include $(top_builddir)/src/Makefile.global SUBDIRS = libpq ecpg +ifeq ($(with_libcurl), yes) +SUBDIRS += libpq-oauth +else +ALWAYS_SUBDIRS += libpq-oauth +endif + $(recurse) +$(recurse_always) all-ecpg-recurse: all-libpq-recurse install-ecpg-recurse: install-libpq-recurse + +ifeq ($(with_libcurl), yes) +all-libpq-oauth-recurse: all-libpq-recurse +install-libpq-oauth-recurse: install-libpq-recurse +endif diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile new file mode 100644 index 00000000000..3e4b34142e0 --- /dev/null +++ b/src/interfaces/libpq-oauth/Makefile @@ -0,0 +1,83 @@ +#------------------------------------------------------------------------- +# +# Makefile for libpq-oauth +# +# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/interfaces/libpq-oauth/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/interfaces/libpq-oauth +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +PGFILEDESC = "libpq-oauth - device authorization OAuth support" + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +NAME = pq-oauth-$(MAJORVERSION) + +# Force the name "libpq-oauth" for both the static and shared libraries. The +# staticlib doesn't need version information in its name. +override shlib := lib$(NAME)$(DLSUFFIX) +override stlib := libpq-oauth.a + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(LIBCURL_CPPFLAGS) $(CPPFLAGS) + +OBJS = \ + $(WIN32RES) + +OBJS_STATIC = oauth-curl.o + +# The shared library needs additional glue symbols. +OBJS_SHLIB = \ + oauth-curl_shlib.o \ + oauth-utils.o \ + +oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH +oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH + +# Add shlib-/stlib-specific objects. +$(shlib): override OBJS += $(OBJS_SHLIB) +$(shlib): $(OBJS_SHLIB) + +$(stlib): override OBJS += $(OBJS_STATIC) +$(stlib): $(OBJS_STATIC) + +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) +SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS) +SHLIB_PREREQS = submake-libpq +SHLIB_EXPORTS = exports.txt + +# Disable -bundle_loader on macOS. +BE_DLLLIBS = + +# By default, a library without an SONAME doesn't get a static library, so we +# add it to the build explicitly. +all: all-lib all-static-lib + +# Shared library stuff +include $(top_srcdir)/src/Makefile.shlib + +# Use src/common/Makefile's trick for tracking dependencies of shlib-specific +# objects. +%_shlib.o: %.c %.o + $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ + +# Ignore the standard rules for SONAME-less installation; we want both the +# static and shared libraries to go into libdir. +install: all installdirs $(stlib) $(shlib) + $(INSTALL_SHLIB) $(shlib) '$(DESTDIR)$(libdir)/$(shlib)' + $(INSTALL_STLIB) $(stlib) '$(DESTDIR)$(libdir)/$(stlib)' + +installdirs: + $(MKDIR_P) '$(DESTDIR)$(libdir)' + +uninstall: + rm -f '$(DESTDIR)$(libdir)/$(stlib)' + rm -f '$(DESTDIR)$(libdir)/$(shlib)' + +clean distclean: clean-lib + rm -f $(OBJS) $(OBJS_STATIC) $(OBJS_SHLIB) diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README new file mode 100644 index 00000000000..553962d644e --- /dev/null +++ b/src/interfaces/libpq-oauth/README @@ -0,0 +1,57 @@ +libpq-oauth is an optional module implementing the Device Authorization flow for +OAuth clients (RFC 8628). It is maintained as its own shared library in order to +isolate its dependency on libcurl. (End users who don't want the Curl dependency +can simply choose not to install this module.) + +If a connection string allows the use of OAuth, and the server asks for it, and +a libpq client has not installed its own custom OAuth flow, libpq will attempt +to delay-load this module using dlopen() and the following ABI. Failure to load +results in a failed connection. + += Load-Time ABI = + +This module ABI is an internal implementation detail, so it's subject to change +across major releases; the name of the module (libpq-oauth-MAJOR) reflects this. +The module exports the following symbols: + +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +- void pg_fe_cleanup_oauth_flow(PGconn *conn); + +pg_fe_run_oauth_flow and pg_fe_cleanup_oauth_flow are implementations of +conn->async_auth and conn->cleanup_async_auth, respectively. + +At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and +libpq_gettext(), which must be injected by libpq using this initialization +function before the flow is run: + +- void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl, + conn_oauth_client_id_func clientid_impl, + conn_oauth_client_secret_func clientsecret_impl, + conn_oauth_discovery_uri_func discoveryuri_impl, + conn_oauth_issuer_id_func issuerid_impl, + conn_oauth_scope_func scope_impl, + conn_sasl_state_func saslstate_impl, + set_conn_altsock_func setaltsock_impl, + set_conn_oauth_token_func settoken_impl); + +It also relies on access to several members of the PGconn struct. Not only can +these change positions across minor versions, but the offsets aren't necessarily +stable within a single minor release (conn->errorMessage, for instance, can +change offsets depending on configure-time options). Therefore the necessary +accessors (named conn_*) and mutators (set_conn_*) are injected here. With this +approach, we can safely search the standard dlopen() paths (e.g. RPATH, +LD_LIBRARY_PATH, the SO cache) for an implementation module to use, even if that +module wasn't compiled at the same time as libpq -- which becomes especially +important during "live upgrade" situations where a running libpq application has +the libpq-oauth module updated out from under it before it's first loaded from +disk. + += Static Build = + +The static library libpq.a does not perform any dynamic loading. If the builtin +flow is enabled, the application is expected to link against libpq-oauth.a +directly to provide the necessary symbols. (libpq.a and libpq-oauth.a must be +part of the same build. Unlike the dynamic module, there are no translation +shims provided.) diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt new file mode 100644 index 00000000000..6891a83dbf9 --- /dev/null +++ b/src/interfaces/libpq-oauth/exports.txt @@ -0,0 +1,4 @@ +# src/interfaces/libpq-oauth/exports.txt +libpq_oauth_init 1 +pg_fe_run_oauth_flow 2 +pg_fe_cleanup_oauth_flow 3 diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build new file mode 100644 index 00000000000..9e7301a7f63 --- /dev/null +++ b/src/interfaces/libpq-oauth/meson.build @@ -0,0 +1,45 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +if not oauth_flow_supported + subdir_done() +endif + +libpq_oauth_sources = files( + 'oauth-curl.c', +) + +# The shared library needs additional glue symbols. +libpq_oauth_so_sources = files( + 'oauth-utils.c', +) +libpq_oauth_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] + +export_file = custom_target('libpq-oauth.exports', + kwargs: gen_export_kwargs, +) + +# port needs to be in include path due to pthread-win32.h +libpq_oauth_inc = include_directories('.', '../libpq', '../../port') + +libpq_oauth_st = static_library('libpq-oauth', + libpq_oauth_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, + dependencies: [frontend_stlib_code, libpq_oauth_deps], + kwargs: default_lib_args, +) + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +libpq_oauth_name = 'libpq-oauth-@0@'.format(pg_version_major) + +libpq_oauth_so = shared_module(libpq_oauth_name, + libpq_oauth_sources + libpq_oauth_so_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_args: libpq_so_c_args, + c_pch: pch_postgres_fe_h, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + link_depends: export_file, + link_args: export_fmt.format(export_file.full_path()), + kwargs: default_lib_args, +) diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c similarity index 94% rename from src/interfaces/libpq/fe-auth-oauth-curl.c rename to src/interfaces/libpq-oauth/oauth-curl.c index c195e00cd28..d13b9cbabb4 100644 --- a/src/interfaces/libpq/fe-auth-oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * fe-auth-oauth-curl.c + * oauth-curl.c * The libcurl implementation of OAuth/OIDC authentication, using the * OAuth Device Authorization Grant (RFC 8628). * @@ -8,7 +8,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * src/interfaces/libpq/fe-auth-oauth-curl.c + * src/interfaces/libpq-oauth/oauth-curl.c * *------------------------------------------------------------------------- */ @@ -17,20 +17,56 @@ #include #include -#ifdef HAVE_SYS_EPOLL_H -#include -#include -#endif -#ifdef HAVE_SYS_EVENT_H -#include -#endif #include +#if defined(HAVE_SYS_EPOLL_H) +#include +#include +#elif defined(HAVE_SYS_EVENT_H) +#include +#else +#error libpq-oauth is not supported on this platform +#endif + #include "common/jsonapi.h" -#include "fe-auth.h" #include "fe-auth-oauth.h" -#include "libpq-int.h" #include "mb/pg_wchar.h" +#include "oauth-curl.h" + +#ifdef USE_DYNAMIC_OAUTH + +/* + * The module build is decoupled from libpq-int.h, to try to avoid inadvertent + * ABI breaks during minor version bumps. Replacements for the missing internals + * are provided by oauth-utils. + */ +#include "oauth-utils.h" + +#else /* !USE_DYNAMIC_OAUTH */ + +/* + * Static builds may rely on PGconn offsets directly. Keep these aligned with + * the bank of callbacks in oauth-utils.h. + */ +#include "libpq-int.h" + +#define conn_errorMessage(CONN) (&CONN->errorMessage) +#define conn_oauth_client_id(CONN) (CONN->oauth_client_id) +#define conn_oauth_client_secret(CONN) (CONN->oauth_client_secret) +#define conn_oauth_discovery_uri(CONN) (CONN->oauth_discovery_uri) +#define conn_oauth_issuer_id(CONN) (CONN->oauth_issuer_id) +#define conn_oauth_scope(CONN) (CONN->oauth_scope) +#define conn_sasl_state(CONN) (CONN->sasl_state) + +#define set_conn_altsock(CONN, VAL) do { CONN->altsock = VAL; } while (0) +#define set_conn_oauth_token(CONN, VAL) do { CONN->oauth_token = VAL; } while (0) + +#endif /* USE_DYNAMIC_OAUTH */ + +/* One final guardrail against accidental inclusion... */ +#if defined(USE_DYNAMIC_OAUTH) && defined(LIBPQ_INT_H) +#error do not rely on libpq-int.h in dynamic builds of libpq-oauth +#endif /* * It's generally prudent to set a maximum response size to buffer in memory, @@ -303,7 +339,7 @@ free_async_ctx(PGconn *conn, struct async_ctx *actx) void pg_fe_cleanup_oauth_flow(PGconn *conn) { - fe_oauth_state *state = conn->sasl_state; + fe_oauth_state *state = conn_sasl_state(conn); if (state->async_ctx) { @@ -311,7 +347,7 @@ pg_fe_cleanup_oauth_flow(PGconn *conn) state->async_ctx = NULL; } - conn->altsock = PGINVALID_SOCKET; + set_conn_altsock(conn, PGINVALID_SOCKET); } /* @@ -1110,7 +1146,7 @@ parse_access_token(struct async_ctx *actx, struct token *tok) static bool setup_multiplexer(struct async_ctx *actx) { -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {.events = EPOLLIN}; actx->mux = epoll_create1(EPOLL_CLOEXEC); @@ -1134,8 +1170,7 @@ setup_multiplexer(struct async_ctx *actx) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) actx->mux = kqueue(); if (actx->mux < 0) { @@ -1158,10 +1193,9 @@ setup_multiplexer(struct async_ctx *actx) } return true; +#else +#error setup_multiplexer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support the Device Authorization flow on this platform"); - return false; } /* @@ -1174,7 +1208,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, { struct async_ctx *actx = ctx; -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {0}; int res; int op = EPOLL_CTL_ADD; @@ -1230,8 +1264,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev[2] = {0}; struct kevent ev_out[2]; struct timespec timeout = {0}; @@ -1312,10 +1345,9 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; +#else +#error register_socket is not implemented on this platform #endif - - actx_error(actx, "libpq does not support multiplexer sockets on this platform"); - return -1; } /* @@ -1334,7 +1366,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, static bool set_timer(struct async_ctx *actx, long timeout) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timeout < 0) @@ -1363,8 +1395,7 @@ set_timer(struct async_ctx *actx, long timeout) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev; #ifdef __NetBSD__ @@ -1419,10 +1450,9 @@ set_timer(struct async_ctx *actx, long timeout) } return true; +#else +#error set_timer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return false; } /* @@ -1433,7 +1463,7 @@ set_timer(struct async_ctx *actx, long timeout) static int timer_expired(struct async_ctx *actx) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timerfd_gettime(actx->timerfd, &spec) < 0) @@ -1453,8 +1483,7 @@ timer_expired(struct async_ctx *actx) /* If the remaining time to expiration is zero, we're done. */ return (spec.it_value.tv_sec == 0 && spec.it_value.tv_nsec == 0); -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) int res; /* Is the timer queue ready? */ @@ -1466,10 +1495,9 @@ timer_expired(struct async_ctx *actx) } return (res > 0); +#else +#error timer_expired is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return -1; } /* @@ -2070,8 +2098,9 @@ static bool check_issuer(struct async_ctx *actx, PGconn *conn) { const struct provider *provider = &actx->provider; + const char *oauth_issuer_id = conn_oauth_issuer_id(conn); - Assert(conn->oauth_issuer_id); /* ensured by setup_oauth_parameters() */ + Assert(oauth_issuer_id); /* ensured by setup_oauth_parameters() */ Assert(provider->issuer); /* ensured by parse_provider() */ /*--- @@ -2091,11 +2120,11 @@ check_issuer(struct async_ctx *actx, PGconn *conn) * sent to. This comparison MUST use simple string comparison as defined * in Section 6.2.1 of [RFC3986]. */ - if (strcmp(conn->oauth_issuer_id, provider->issuer) != 0) + if (strcmp(oauth_issuer_id, provider->issuer) != 0) { actx_error(actx, "the issuer identifier (%s) does not match oauth_issuer (%s)", - provider->issuer, conn->oauth_issuer_id); + provider->issuer, oauth_issuer_id); return false; } @@ -2172,11 +2201,14 @@ check_for_device_flow(struct async_ctx *actx) static bool add_client_identification(struct async_ctx *actx, PQExpBuffer reqbody, PGconn *conn) { + const char *oauth_client_id = conn_oauth_client_id(conn); + const char *oauth_client_secret = conn_oauth_client_secret(conn); + bool success = false; char *username = NULL; char *password = NULL; - if (conn->oauth_client_secret) /* Zero-length secrets are permitted! */ + if (oauth_client_secret) /* Zero-length secrets are permitted! */ { /*---- * Use HTTP Basic auth to send the client_id and secret. Per RFC 6749, @@ -2204,8 +2236,8 @@ add_client_identification(struct async_ctx *actx, PQExpBuffer reqbody, PGconn *c * would it be redundant, but some providers in the wild (e.g. Okta) * refuse to accept it. */ - username = urlencode(conn->oauth_client_id); - password = urlencode(conn->oauth_client_secret); + username = urlencode(oauth_client_id); + password = urlencode(oauth_client_secret); if (!username || !password) { @@ -2225,7 +2257,7 @@ add_client_identification(struct async_ctx *actx, PQExpBuffer reqbody, PGconn *c * If we're not otherwise authenticating, client_id is REQUIRED in the * request body. */ - build_urlencoded(reqbody, "client_id", conn->oauth_client_id); + build_urlencoded(reqbody, "client_id", oauth_client_id); CHECK_SETOPT(actx, CURLOPT_HTTPAUTH, CURLAUTH_NONE, goto cleanup); actx->used_basic_auth = false; @@ -2253,16 +2285,17 @@ cleanup: static bool start_device_authz(struct async_ctx *actx, PGconn *conn) { + const char *oauth_scope = conn_oauth_scope(conn); const char *device_authz_uri = actx->provider.device_authorization_endpoint; PQExpBuffer work_buffer = &actx->work_data; - Assert(conn->oauth_client_id); /* ensured by setup_oauth_parameters() */ + Assert(conn_oauth_client_id(conn)); /* ensured by setup_oauth_parameters() */ Assert(device_authz_uri); /* ensured by check_for_device_flow() */ /* Construct our request body. */ resetPQExpBuffer(work_buffer); - if (conn->oauth_scope && conn->oauth_scope[0]) - build_urlencoded(work_buffer, "scope", conn->oauth_scope); + if (oauth_scope && oauth_scope[0]) + build_urlencoded(work_buffer, "scope", oauth_scope); if (!add_client_identification(actx, work_buffer, conn)) return false; @@ -2344,7 +2377,7 @@ start_token_request(struct async_ctx *actx, PGconn *conn) const char *device_code = actx->authz.device_code; PQExpBuffer work_buffer = &actx->work_data; - Assert(conn->oauth_client_id); /* ensured by setup_oauth_parameters() */ + Assert(conn_oauth_client_id(conn)); /* ensured by setup_oauth_parameters() */ Assert(token_uri); /* ensured by parse_provider() */ Assert(device_code); /* ensured by parse_device_authz() */ @@ -2487,8 +2520,9 @@ prompt_user(struct async_ctx *actx, PGconn *conn) .verification_uri_complete = actx->authz.verification_uri_complete, .expires_in = actx->authz.expires_in, }; + PQauthDataHook_type hook = PQgetAuthDataHook(); - res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); + res = hook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); if (!res) { @@ -2633,8 +2667,10 @@ done: static PostgresPollingStatusType pg_fe_run_oauth_flow_impl(PGconn *conn) { - fe_oauth_state *state = conn->sasl_state; + fe_oauth_state *state = conn_sasl_state(conn); struct async_ctx *actx; + char *oauth_token = NULL; + PQExpBuffer errbuf; if (!initialize_curl(conn)) return PGRES_POLLING_FAILED; @@ -2676,7 +2712,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) do { /* By default, the multiplexer is the altsock. Reassign as desired. */ - conn->altsock = actx->mux; + set_conn_altsock(conn, actx->mux); switch (actx->step) { @@ -2712,7 +2748,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) */ if (!timer_expired(actx)) { - conn->altsock = actx->timerfd; + set_conn_altsock(conn, actx->timerfd); return PGRES_POLLING_READING; } @@ -2732,7 +2768,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) { case OAUTH_STEP_INIT: actx->errctx = "failed to fetch OpenID discovery document"; - if (!start_discovery(actx, conn->oauth_discovery_uri)) + if (!start_discovery(actx, conn_oauth_discovery_uri(conn))) goto error_return; actx->step = OAUTH_STEP_DISCOVERY; @@ -2768,9 +2804,15 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) break; case OAUTH_STEP_TOKEN_REQUEST: - if (!handle_token_response(actx, &conn->oauth_token)) + if (!handle_token_response(actx, &oauth_token)) goto error_return; + /* + * Hook any oauth_token into the PGconn immediately so that + * the allocation isn't lost in case of an error. + */ + set_conn_oauth_token(conn, oauth_token); + if (!actx->user_prompted) { /* @@ -2783,7 +2825,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) actx->user_prompted = true; } - if (conn->oauth_token) + if (oauth_token) break; /* done! */ /* @@ -2798,7 +2840,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) * the client wait directly on the timerfd rather than the * multiplexer. */ - conn->altsock = actx->timerfd; + set_conn_altsock(conn, actx->timerfd); actx->step = OAUTH_STEP_WAIT_INTERVAL; actx->running = 1; @@ -2818,48 +2860,40 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) * point, actx->running will be set. But there are some corner cases * where we can immediately loop back around; see start_request(). */ - } while (!conn->oauth_token && !actx->running); + } while (!oauth_token && !actx->running); /* If we've stored a token, we're done. Otherwise come back later. */ - return conn->oauth_token ? PGRES_POLLING_OK : PGRES_POLLING_READING; + return oauth_token ? PGRES_POLLING_OK : PGRES_POLLING_READING; error_return: + errbuf = conn_errorMessage(conn); /* * Assemble the three parts of our error: context, body, and detail. See * also the documentation for struct async_ctx. */ if (actx->errctx) - { - appendPQExpBufferStr(&conn->errorMessage, - libpq_gettext(actx->errctx)); - appendPQExpBufferStr(&conn->errorMessage, ": "); - } + appendPQExpBuffer(errbuf, "%s: ", libpq_gettext(actx->errctx)); if (PQExpBufferDataBroken(actx->errbuf)) - appendPQExpBufferStr(&conn->errorMessage, - libpq_gettext("out of memory")); + appendPQExpBufferStr(errbuf, libpq_gettext("out of memory")); else - appendPQExpBufferStr(&conn->errorMessage, actx->errbuf.data); + appendPQExpBufferStr(errbuf, actx->errbuf.data); if (actx->curl_err[0]) { - size_t len; - - appendPQExpBuffer(&conn->errorMessage, - " (libcurl: %s)", actx->curl_err); + appendPQExpBuffer(errbuf, " (libcurl: %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') + if (errbuf->len >= 2 && errbuf->data[errbuf->len - 2] == '\n') { - conn->errorMessage.data[len - 2] = ')'; - conn->errorMessage.data[len - 1] = '\0'; - conn->errorMessage.len--; + errbuf->data[errbuf->len - 2] = ')'; + errbuf->data[errbuf->len - 1] = '\0'; + errbuf->len--; } } - appendPQExpBufferChar(&conn->errorMessage, '\n'); + appendPQExpBufferChar(errbuf, '\n'); return PGRES_POLLING_FAILED; } diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h new file mode 100644 index 00000000000..248d0424ad0 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-curl.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * oauth-curl.h + * + * Definitions for OAuth Device Authorization module + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-curl.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_CURL_H +#define OAUTH_CURL_H + +#include "libpq-fe.h" + +/* Exported async-auth callbacks. */ +extern PGDLLEXPORT PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +extern PGDLLEXPORT void pg_fe_cleanup_oauth_flow(PGconn *conn); + +#endif /* OAUTH_CURL_H */ diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c new file mode 100644 index 00000000000..45fdc7579f2 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.c @@ -0,0 +1,233 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.c + * + * "Glue" helpers providing a copy of some internal APIs from libpq. At + * some point in the future, we might be able to deduplicate. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq-oauth/oauth-utils.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include + +#include "oauth-utils.h" + +#ifndef USE_DYNAMIC_OAUTH +#error oauth-utils.c is not supported in static builds +#endif + +#ifdef LIBPQ_INT_H +#error do not rely on libpq-int.h in dynamic builds of libpq-oauth +#endif + +/* + * Function pointers set by libpq_oauth_init(). + */ + +pgthreadlock_t pg_g_threadlock; +static libpq_gettext_func libpq_gettext_impl; + +conn_errorMessage_func conn_errorMessage; +conn_oauth_client_id_func conn_oauth_client_id; +conn_oauth_client_secret_func conn_oauth_client_secret; +conn_oauth_discovery_uri_func conn_oauth_discovery_uri; +conn_oauth_issuer_id_func conn_oauth_issuer_id; +conn_oauth_scope_func conn_oauth_scope; +conn_sasl_state_func conn_sasl_state; + +set_conn_altsock_func set_conn_altsock; +set_conn_oauth_token_func set_conn_oauth_token; + +/*- + * Initializes libpq-oauth by setting necessary callbacks. + * + * The current implementation relies on the following private implementation + * details of libpq: + * + * - pg_g_threadlock: protects libcurl initialization if the underlying Curl + * installation is not threadsafe + * + * - libpq_gettext: translates error messages using libpq's message domain + * + * The implementation also needs access to several members of the PGconn struct, + * which are not guaranteed to stay in place across minor versions. Accessors + * (named conn_*) and mutators (named set_conn_*) are injected here. + */ +void +libpq_oauth_init(pgthreadlock_t threadlock_impl, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl, + conn_oauth_client_id_func clientid_impl, + conn_oauth_client_secret_func clientsecret_impl, + conn_oauth_discovery_uri_func discoveryuri_impl, + conn_oauth_issuer_id_func issuerid_impl, + conn_oauth_scope_func scope_impl, + conn_sasl_state_func saslstate_impl, + set_conn_altsock_func setaltsock_impl, + set_conn_oauth_token_func settoken_impl) +{ + pg_g_threadlock = threadlock_impl; + libpq_gettext_impl = gettext_impl; + conn_errorMessage = errmsg_impl; + conn_oauth_client_id = clientid_impl; + conn_oauth_client_secret = clientsecret_impl; + conn_oauth_discovery_uri = discoveryuri_impl; + conn_oauth_issuer_id = issuerid_impl; + conn_oauth_scope = scope_impl; + conn_sasl_state = saslstate_impl; + set_conn_altsock = setaltsock_impl; + set_conn_oauth_token = settoken_impl; +} + +/* + * Append a formatted string to the error message buffer of the given + * connection, after translating it. This is a copy of libpq's internal API. + */ +void +libpq_append_conn_error(PGconn *conn, const char *fmt,...) +{ + int save_errno = errno; + bool done; + va_list args; + PQExpBuffer errorMessage = conn_errorMessage(conn); + + Assert(fmt[strlen(fmt) - 1] != '\n'); + + if (PQExpBufferBroken(errorMessage)) + return; /* already failed */ + + /* Loop in case we have to retry after enlarging the buffer. */ + do + { + errno = save_errno; + va_start(args, fmt); + done = appendPQExpBufferVA(errorMessage, libpq_gettext(fmt), args); + va_end(args); + } while (!done); + + appendPQExpBufferChar(errorMessage, '\n'); +} + +#ifdef ENABLE_NLS + +/* + * A shim that defers to the actual libpq_gettext(). + */ +char * +libpq_gettext(const char *msgid) +{ + if (!libpq_gettext_impl) + { + /* + * Possible if the libpq build didn't enable NLS but the libpq-oauth + * build did. That's an odd mismatch, but we can handle it. + * + * Note that callers of libpq_gettext() have to treat the return value + * as if it were const, because builds without NLS simply pass through + * their argument. + */ + return unconstify(char *, msgid); + } + + return libpq_gettext_impl(msgid); +} + +#endif /* ENABLE_NLS */ + +/* + * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. + */ +bool +oauth_unsafe_debugging_enabled(void) +{ + const char *env = getenv("PGOAUTHDEBUG"); + + return (env && strcmp(env, "UNSAFE") == 0); +} + +/* + * Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by + * pq_block/reset_sigpipe(). + */ +#ifdef WIN32 +#define SOCK_ERRNO (WSAGetLastError()) +#define SOCK_ERRNO_SET(e) WSASetLastError(e) +#else +#define SOCK_ERRNO errno +#define SOCK_ERRNO_SET(e) (errno = (e)) +#endif + +/* + * Block SIGPIPE for this thread. This is a copy of libpq's internal API. + */ +int +pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending) +{ + sigset_t sigpipe_sigset; + sigset_t sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + /* Block SIGPIPE and save previous mask for later reset */ + SOCK_ERRNO_SET(pthread_sigmask(SIG_BLOCK, &sigpipe_sigset, osigset)); + if (SOCK_ERRNO) + return -1; + + /* We can have a pending SIGPIPE only if it was blocked before */ + if (sigismember(osigset, SIGPIPE)) + { + /* Is there a pending SIGPIPE? */ + if (sigpending(&sigset) != 0) + return -1; + + if (sigismember(&sigset, SIGPIPE)) + *sigpipe_pending = true; + else + *sigpipe_pending = false; + } + else + *sigpipe_pending = false; + + return 0; +} + +/* + * Discard any pending SIGPIPE and reset the signal mask. This is a copy of + * libpq's internal API. + */ +void +pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe) +{ + int save_errno = SOCK_ERRNO; + int signo; + sigset_t sigset; + + /* Clear SIGPIPE only if none was pending */ + if (got_epipe && !sigpipe_pending) + { + if (sigpending(&sigset) == 0 && + sigismember(&sigset, SIGPIPE)) + { + sigset_t sigpipe_sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + sigwait(&sigpipe_sigset, &signo); + } + } + + /* Restore saved block mask */ + pthread_sigmask(SIG_SETMASK, osigset, NULL); + + SOCK_ERRNO_SET(save_errno); +} diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h new file mode 100644 index 00000000000..f4ffefef208 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.h @@ -0,0 +1,94 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.h + * + * Definitions providing missing libpq internal APIs + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-utils.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_UTILS_H +#define OAUTH_UTILS_H + +#include "fe-auth-oauth.h" +#include "libpq-fe.h" +#include "pqexpbuffer.h" + +/* + * A bank of callbacks to safely access members of PGconn, which are all passed + * to libpq_oauth_init() by libpq. + * + * Keep these aligned with the definitions in fe-auth-oauth.c as well as the + * static declarations in oauth-curl.c. + */ +#define DECLARE_GETTER(TYPE, MEMBER) \ + typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \ + extern conn_ ## MEMBER ## _func conn_ ## MEMBER; + +#define DECLARE_SETTER(TYPE, MEMBER) \ + typedef void (*set_conn_ ## MEMBER ## _func) (PGconn *conn, TYPE val); \ + extern set_conn_ ## MEMBER ## _func set_conn_ ## MEMBER; + +DECLARE_GETTER(PQExpBuffer, errorMessage); +DECLARE_GETTER(char *, oauth_client_id); +DECLARE_GETTER(char *, oauth_client_secret); +DECLARE_GETTER(char *, oauth_discovery_uri); +DECLARE_GETTER(char *, oauth_issuer_id); +DECLARE_GETTER(char *, oauth_scope); +DECLARE_GETTER(fe_oauth_state *, sasl_state); + +DECLARE_SETTER(pgsocket, altsock); +DECLARE_SETTER(char *, oauth_token); + +#undef DECLARE_GETTER +#undef DECLARE_SETTER + +typedef char *(*libpq_gettext_func) (const char *msgid); + +/* Initializes libpq-oauth. */ +extern PGDLLEXPORT void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl, + conn_oauth_client_id_func clientid_impl, + conn_oauth_client_secret_func clientsecret_impl, + conn_oauth_discovery_uri_func discoveryuri_impl, + conn_oauth_issuer_id_func issuerid_impl, + conn_oauth_scope_func scope_impl, + conn_sasl_state_func saslstate_impl, + set_conn_altsock_func setaltsock_impl, + set_conn_oauth_token_func settoken_impl); + +/* + * Duplicated APIs, copied from libpq (primarily libpq-int.h, which we cannot + * depend on here). + */ + +typedef enum +{ + PG_BOOL_UNKNOWN = 0, /* Currently unknown */ + PG_BOOL_YES, /* Yes (true) */ + PG_BOOL_NO /* No (false) */ +} PGTernaryBool; + +extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern bool oauth_unsafe_debugging_enabled(void); +extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending); +extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe); + +#ifdef ENABLE_NLS +extern char *libpq_gettext(const char *msgid) pg_attribute_format_arg(1); +#else +#define libpq_gettext(x) (x) +#endif + +extern pgthreadlock_t pg_g_threadlock; + +#define pglock_thread() pg_g_threadlock(true) +#define pgunlock_thread() pg_g_threadlock(false) + +#endif /* OAUTH_UTILS_H */ diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 90b0b65db6f..c6fe5fec7f6 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -31,7 +31,6 @@ endif OBJS = \ $(WIN32RES) \ - fe-auth-oauth.o \ fe-auth-scram.o \ fe-cancel.o \ fe-connect.o \ @@ -64,9 +63,11 @@ OBJS += \ fe-secure-gssapi.o endif -ifeq ($(with_libcurl),yes) -OBJS += fe-auth-oauth-curl.o -endif +# The OAuth implementation differs depending on the type of library being built. +OBJS_STATIC = fe-auth-oauth.o + +fe-auth-oauth_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH +OBJS_SHLIB = fe-auth-oauth_shlib.o ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) @@ -86,7 +87,7 @@ endif # that are built correctly for use in a shlib. SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib ifneq ($(PORTNAME), win32) -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lcurl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) +SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) 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 @@ -101,12 +102,26 @@ ifeq ($(with_ssl),openssl) PKG_CONFIG_REQUIRES_PRIVATE = libssl, libcrypto endif +ifeq ($(with_libcurl),yes) +# libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and +# libpq-oauth needs libcurl. Put both into *.private. +PKG_CONFIG_REQUIRES_PRIVATE += libcurl +%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth +endif + all: all-lib libpq-refs-stamp # Shared library stuff include $(top_srcdir)/src/Makefile.shlib backend_src = $(top_srcdir)/src/backend +# Add shlib-/stlib-specific objects. +$(shlib): override OBJS += $(OBJS_SHLIB) +$(shlib): $(OBJS_SHLIB) + +$(stlib): override OBJS += $(OBJS_STATIC) +$(stlib): $(OBJS_STATIC) + # Check for functions that libpq must not call, currently just exit(). # (Ideally we'd reject abort() too, but there are various scenarios where # build toolchains insert abort() calls, e.g. to implement assert().) @@ -115,8 +130,6 @@ 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 @@ -124,7 +137,7 @@ 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 -e _atexit | grep exit; then \ + @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \ echo 'libpq must not be calling any function which invokes exit'; exit 1; \ fi endif @@ -138,6 +151,11 @@ fe-misc.o: fe-misc.c $(top_builddir)/src/port/pg_config_paths.h $(top_builddir)/src/port/pg_config_paths.h: $(MAKE) -C $(top_builddir)/src/port pg_config_paths.h +# Use src/common/Makefile's trick for tracking dependencies of shlib-specific +# objects. +%_shlib.o: %.c %.o + $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ + install: all installdirs install-lib $(INSTALL_DATA) $(srcdir)/libpq-fe.h '$(DESTDIR)$(includedir)' $(INSTALL_DATA) $(srcdir)/libpq-events.h '$(DESTDIR)$(includedir)' @@ -171,6 +189,6 @@ uninstall: uninstall-lib clean distclean: clean-lib $(MAKE) -C test $@ rm -rf tmp_check - rm -f $(OBJS) pthread.h libpq-refs-stamp + rm -f $(OBJS) $(OBJS_SHLIB) $(OBJS_STATIC) pthread.h libpq-refs-stamp # Might be left over from a Win32 client-only build rm -f pg_config_paths.h diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index d5143766858..0625cf39e9a 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -210,3 +210,4 @@ PQsetAuthDataHook 207 PQgetAuthDataHook 208 PQdefaultAuthDataHook 209 PQfullProtocolVersion 210 +appendPQExpBufferVA 211 diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index ab6a45e2aba..9fbff89a21d 100644 --- a/src/interfaces/libpq/fe-auth-oauth.c +++ b/src/interfaces/libpq/fe-auth-oauth.c @@ -15,6 +15,10 @@ #include "postgres_fe.h" +#ifdef USE_DYNAMIC_OAUTH +#include +#endif + #include "common/base64.h" #include "common/hmac.h" #include "common/jsonapi.h" @@ -22,6 +26,7 @@ #include "fe-auth.h" #include "fe-auth-oauth.h" #include "mb/pg_wchar.h" +#include "pg_config_paths.h" /* The exported OAuth callback mechanism. */ static void *oauth_init(PGconn *conn, const char *password, @@ -721,6 +726,218 @@ cleanup_user_oauth_flow(PGconn *conn) state->async_ctx = NULL; } +/*------------- + * Builtin Flow + * + * There are three potential implementations of use_builtin_flow: + * + * 1) If the OAuth client is disabled at configuration time, return false. + * Dependent clients must provide their own flow. + * 2) If the OAuth client is enabled and USE_DYNAMIC_OAUTH is defined, dlopen() + * the libpq-oauth plugin and use its implementation. + * 3) Otherwise, use flow callbacks that are statically linked into the + * executable. + */ + +#if !defined(USE_LIBCURL) + +/* + * This configuration doesn't support the builtin flow. + */ + +bool +use_builtin_flow(PGconn *conn, fe_oauth_state *state) +{ + return false; +} + +#elif defined(USE_DYNAMIC_OAUTH) + +/* + * Use the builtin flow in the libpq-oauth plugin, which is loaded at runtime. + */ + +typedef char *(*libpq_gettext_func) (const char *msgid); + +/* + * Define accessor/mutator shims to inject into libpq-oauth, so that it doesn't + * depend on the offsets within PGconn. (These have changed during minor version + * updates in the past.) + */ + +#define DEFINE_GETTER(TYPE, MEMBER) \ + typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \ + static TYPE conn_ ## MEMBER(PGconn *conn) { return conn->MEMBER; } + +/* Like DEFINE_GETTER, but returns a pointer to the member. */ +#define DEFINE_GETTER_P(TYPE, MEMBER) \ + typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \ + static TYPE conn_ ## MEMBER(PGconn *conn) { return &conn->MEMBER; } + +#define DEFINE_SETTER(TYPE, MEMBER) \ + typedef void (*set_conn_ ## MEMBER ## _func) (PGconn *conn, TYPE val); \ + static void set_conn_ ## MEMBER(PGconn *conn, TYPE val) { conn->MEMBER = val; } + +DEFINE_GETTER_P(PQExpBuffer, errorMessage); +DEFINE_GETTER(char *, oauth_client_id); +DEFINE_GETTER(char *, oauth_client_secret); +DEFINE_GETTER(char *, oauth_discovery_uri); +DEFINE_GETTER(char *, oauth_issuer_id); +DEFINE_GETTER(char *, oauth_scope); +DEFINE_GETTER(fe_oauth_state *, sasl_state); + +DEFINE_SETTER(pgsocket, altsock); +DEFINE_SETTER(char *, oauth_token); + +/* + * Loads the libpq-oauth plugin via dlopen(), initializes it, and plugs its + * callbacks into the connection's async auth handlers. + * + * Failure to load here results in a relatively quiet connection error, to + * handle the use case where the build supports loading a flow but a user does + * not want to install it. Troubleshooting of linker/loader failures can be done + * via PGOAUTHDEBUG. + */ +bool +use_builtin_flow(PGconn *conn, fe_oauth_state *state) +{ + static bool initialized = false; + static pthread_mutex_t init_mutex = PTHREAD_MUTEX_INITIALIZER; + int lockerr; + + void (*init) (pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl, + conn_oauth_client_id_func clientid_impl, + conn_oauth_client_secret_func clientsecret_impl, + conn_oauth_discovery_uri_func discoveryuri_impl, + conn_oauth_issuer_id_func issuerid_impl, + conn_oauth_scope_func scope_impl, + conn_sasl_state_func saslstate_impl, + set_conn_altsock_func setaltsock_impl, + set_conn_oauth_token_func settoken_impl); + PostgresPollingStatusType (*flow) (PGconn *conn); + void (*cleanup) (PGconn *conn); + + /* + * On macOS only, load the module using its absolute install path; the + * standard search behavior is not very helpful for this use case. Unlike + * on other platforms, DYLD_LIBRARY_PATH is used as a fallback even with + * absolute paths (modulo SIP effects), so tests can continue to work. + * + * On the other platforms, load the module using only the basename, to + * rely on the runtime linker's standard search behavior. + */ + const char *const module_name = +#if defined(__darwin__) + LIBDIR "/libpq-oauth-" PG_MAJORVERSION DLSUFFIX; +#else + "libpq-oauth-" PG_MAJORVERSION DLSUFFIX; +#endif + + state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL); + if (!state->builtin_flow) + { + /* + * For end users, this probably isn't an error condition, it just + * means the flow isn't installed. Developers and package maintainers + * may want to debug this via the PGOAUTHDEBUG envvar, though. + * + * Note that POSIX dlerror() isn't guaranteed to be threadsafe. + */ + if (oauth_unsafe_debugging_enabled()) + fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror()); + + return false; + } + + if ((init = dlsym(state->builtin_flow, "libpq_oauth_init")) == NULL + || (flow = dlsym(state->builtin_flow, "pg_fe_run_oauth_flow")) == NULL + || (cleanup = dlsym(state->builtin_flow, "pg_fe_cleanup_oauth_flow")) == NULL) + { + /* + * This is more of an error condition than the one above, but due to + * the dlerror() threadsafety issue, lock it behind PGOAUTHDEBUG too. + */ + if (oauth_unsafe_debugging_enabled()) + fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror()); + + dlclose(state->builtin_flow); + return false; + } + + /* + * Past this point, we do not unload the module. It stays in the process + * permanently. + */ + + /* + * We need to inject necessary function pointers into the module. This + * only needs to be done once -- even if the pointers are constant, + * assigning them while another thread is executing the flows feels like + * tempting fate. + */ + if ((lockerr = pthread_mutex_lock(&init_mutex)) != 0) + { + /* Should not happen... but don't continue if it does. */ + Assert(false); + + libpq_append_conn_error(conn, "failed to lock mutex (%d)", lockerr); + return false; + } + + if (!initialized) + { + init(pg_g_threadlock, +#ifdef ENABLE_NLS + libpq_gettext, +#else + NULL, +#endif + conn_errorMessage, + conn_oauth_client_id, + conn_oauth_client_secret, + conn_oauth_discovery_uri, + conn_oauth_issuer_id, + conn_oauth_scope, + conn_sasl_state, + set_conn_altsock, + set_conn_oauth_token); + + initialized = true; + } + + pthread_mutex_unlock(&init_mutex); + + /* Set our asynchronous callbacks. */ + conn->async_auth = flow; + conn->cleanup_async_auth = cleanup; + + return true; +} + +#else + +/* + * Use the builtin flow in libpq-oauth.a (see libpq-oauth/oauth-curl.h). + */ + +extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +extern void pg_fe_cleanup_oauth_flow(PGconn *conn); + +bool +use_builtin_flow(PGconn *conn, fe_oauth_state *state) +{ + /* Set our asynchronous callbacks. */ + conn->async_auth = pg_fe_run_oauth_flow; + conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow; + + return true; +} + +#endif /* USE_LIBCURL */ + + /* * Chooses an OAuth client flow for the connection, which will retrieve a Bearer * token for presentation to the server. @@ -792,18 +1009,10 @@ setup_token_request(PGconn *conn, fe_oauth_state *state) libpq_append_conn_error(conn, "user-defined OAuth flow failed"); goto fail; } - else + else if (!use_builtin_flow(conn, state)) { -#if USE_LIBCURL - /* Hand off to our built-in OAuth flow. */ - conn->async_auth = pg_fe_run_oauth_flow; - conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow; - -#else - libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built with libcurl support"); + libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)"); goto fail; - -#endif } return true; diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h index 3f1a7503a01..0d59e91605b 100644 --- a/src/interfaces/libpq/fe-auth-oauth.h +++ b/src/interfaces/libpq/fe-auth-oauth.h @@ -15,8 +15,8 @@ #ifndef FE_AUTH_OAUTH_H #define FE_AUTH_OAUTH_H +#include "fe-auth-sasl.h" #include "libpq-fe.h" -#include "libpq-int.h" enum fe_oauth_step @@ -27,18 +27,24 @@ enum fe_oauth_step FE_OAUTH_SERVER_ERROR, }; +/* + * This struct is exported to the libpq-oauth module. If changes are needed + * during backports to stable branches, please keep ABI compatibility (no + * changes to existing members, add new members at the end, etc.). + */ typedef struct { enum fe_oauth_step step; PGconn *conn; void *async_ctx; + + void *builtin_flow; } fe_oauth_state; -extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -extern void pg_fe_cleanup_oauth_flow(PGconn *conn); extern void pqClearOAuthToken(PGconn *conn); extern bool oauth_unsafe_debugging_enabled(void); +extern bool use_builtin_flow(PGconn *conn, fe_oauth_state *state); /* Mechanisms in fe-auth-oauth.c */ extern const pg_fe_sasl_mech pg_oauth_mech; diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index 292fecf3320..a74e885b169 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -38,10 +38,6 @@ if gssapi.found() ) endif -if libcurl.found() - libpq_sources += files('fe-auth-oauth-curl.c') -endif - export_file = custom_target('libpq.exports', kwargs: gen_export_kwargs, ) @@ -50,6 +46,9 @@ export_file = custom_target('libpq.exports', libpq_inc = include_directories('.', '../../port') libpq_c_args = ['-DSO_MAJOR_VERSION=5'] +# The OAuth implementation differs depending on the type of library being built. +libpq_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] + # Not using both_libraries() here as # 1) resource files should only be in the shared library # 2) we want the .pc file to include a dependency to {pgport,common}_static for @@ -70,7 +69,7 @@ libpq_st = static_library('libpq', libpq_so = shared_library('libpq', libpq_sources + libpq_so_sources, include_directories: [libpq_inc, postgres_inc], - c_args: libpq_c_args, + c_args: libpq_c_args + libpq_so_c_args, c_pch: pch_postgres_fe_h, version: '5.' + pg_version_major.to_string(), soversion: host_system != 'windows' ? '5' : '', @@ -86,12 +85,26 @@ libpq = declare_dependency( include_directories: [include_directories('.')] ) +private_deps = [ + frontend_stlib_code, + libpq_deps, +] + +if oauth_flow_supported + # libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and + # libpq-oauth needs libcurl. Put both into *.private. + private_deps += [ + libpq_oauth_deps, + '-lpq-oauth', + ] +endif + pkgconfig.generate( name: 'libpq', description: 'PostgreSQL libpq library', url: pg_url, libraries: libpq, - libraries_private: [frontend_stlib_code, libpq_deps], + libraries_private: private_deps, ) install_headers( diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk index ae761265852..b87df277d93 100644 --- a/src/interfaces/libpq/nls.mk +++ b/src/interfaces/libpq/nls.mk @@ -13,15 +13,21 @@ GETTEXT_FILES = fe-auth.c \ fe-secure-common.c \ fe-secure-gssapi.c \ fe-secure-openssl.c \ - win32.c -GETTEXT_TRIGGERS = libpq_append_conn_error:2 \ + win32.c \ + ../libpq-oauth/oauth-curl.c \ + ../libpq-oauth/oauth-utils.c +GETTEXT_TRIGGERS = actx_error:2 \ + libpq_append_conn_error:2 \ libpq_append_error:2 \ libpq_gettext \ libpq_ngettext:1,2 \ + oauth_parse_set_error:2 \ pqInternalNotice:2 -GETTEXT_FLAGS = libpq_append_conn_error:2:c-format \ +GETTEXT_FLAGS = actx_error:2:c-format \ + libpq_append_conn_error:2:c-format \ libpq_append_error:2:c-format \ libpq_gettext:1:pass-c-format \ libpq_ngettext:1:pass-c-format \ libpq_ngettext:2:pass-c-format \ + oauth_parse_set_error:2:c-format \ pqInternalNotice:2:c-format diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build index 55da678ec27..91a8de1ee9b 100644 --- a/src/makefiles/meson.build +++ b/src/makefiles/meson.build @@ -203,6 +203,8 @@ pgxs_empty = [ 'LIBNUMA_CFLAGS', 'LIBNUMA_LIBS', 'LIBURING_CFLAGS', 'LIBURING_LIBS', + + 'LIBCURL_CPPFLAGS', 'LIBCURL_LDFLAGS', 'LIBCURL_LDLIBS', ] if host_system == 'windows' and cc.get_argument_syntax() != 'msvc' diff --git a/src/test/modules/oauth_validator/meson.build b/src/test/modules/oauth_validator/meson.build index 36d1b26369f..e190f9cf15a 100644 --- a/src/test/modules/oauth_validator/meson.build +++ b/src/test/modules/oauth_validator/meson.build @@ -78,7 +78,7 @@ tests += { ], 'env': { 'PYTHON': python.path(), - 'with_libcurl': libcurl.found() ? 'yes' : 'no', + 'with_libcurl': oauth_flow_supported ? 'yes' : 'no', 'with_python': 'yes', }, }, diff --git a/src/test/modules/oauth_validator/t/002_client.pl b/src/test/modules/oauth_validator/t/002_client.pl index 8dd502f41e1..21d4acc1926 100644 --- a/src/test/modules/oauth_validator/t/002_client.pl +++ b/src/test/modules/oauth_validator/t/002_client.pl @@ -110,7 +110,7 @@ if ($ENV{with_libcurl} ne 'yes') "fails without custom hook installed", flags => ["--no-hook"], expected_stderr => - qr/no custom OAuth flows are available, and libpq was not built with libcurl support/ + qr/no OAuth flows are available \(try installing the libpq-oauth package\)/ ); }