1: 16f1b8fc02 ! 1: 785add8015 Add OAUTHBEARER SASL mechanism @@ .cirrus.tasks.yml: task: --buildtype=debug \ -Dcassert=true -Dinjection_points=true \ -Duuid=bsd -Dtcl_version=tcl86 -Ddtrace=auto \ -+ -Dbuiltin_oauth=curl \ ++ -Dlibcurl=enabled \ -Dextra_lib_dirs=/usr/local/lib -Dextra_include_dirs=/usr/local/include/ \ build EOF @@ .cirrus.tasks.yml: LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >- + --with-gssapi + --with-icu + --with-ldap ++ --with-libcurl + --with-libxml --with-libxslt --with-llvm - --with-lz4 -+ --with-builtin-oauth=curl - --with-pam - --with-perl - --with-python @@ .cirrus.tasks.yml: LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >- + --with-zstd LINUX_MESON_FEATURES: &LINUX_MESON_FEATURES >- ++ -Dlibcurl=enabled -Dllvm=enabled -+ -Dbuiltin_oauth=curl -Duuid=e2fs - @@ .cirrus.tasks.yml: task: EOF @@ config/programs.m4: if test "$pgac_cv_ldap_safe" != yes; then *** also uses LDAP will crash on exit.]) fi]) -+# PGAC_CHECK_LIBCURL -+# ------------------ -+# Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability to -+# explicitly set TLS 1.3 ciphersuites). - -+AC_DEFUN([PGAC_CHECK_LIBCURL], -+[AC_CACHE_CHECK([for compatible libcurl], [pgac_cv_check_libcurl], -+[AC_COMPILE_IFELSE([AC_LANG_PROGRAM( -+[#include -+#if LIBCURL_VERSION_MAJOR < 7 || (LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR < 61) -+choke me -+#endif], [])], -+[pgac_cv_check_libcurl=yes], -+[pgac_cv_check_libcurl=no])]) -+ -+if test "$pgac_cv_check_libcurl" != yes; then -+ AC_MSG_ERROR([ -+*** The installed version of libcurl is too old to use with PostgreSQL. -+*** libcurl version 7.61.0 or later is required.]) -+fi]) - +- +- # PGAC_CHECK_READLINE # ------------------- + # Check for the readline library and dependent libraries, either ## configure ## -@@ configure: with_uuid +@@ configure: XML2_LIBS + XML2_CFLAGS + XML2_CONFIG + with_libxml ++LIBCURL_LIBS ++LIBCURL_CFLAGS ++with_libcurl + with_uuid with_readline with_systemd - with_selinux -+with_builtin_oauth - with_ldap - with_krb_srvnam - krb_srvtab -@@ configure: with_krb_srvnam - with_pam - with_bsd_auth - with_ldap -+with_builtin_oauth - with_bonjour - with_selinux - with_systemd +@@ configure: with_readline + with_libedit_preferred + with_uuid + with_ossp_uuid ++with_libcurl + with_libxml + with_libxslt + with_system_tzdata +@@ configure: PKG_CONFIG_PATH + PKG_CONFIG_LIBDIR + ICU_CFLAGS + ICU_LIBS ++LIBCURL_CFLAGS ++LIBCURL_LIBS + XML2_CONFIG + XML2_CFLAGS + XML2_LIBS @@ configure: Optional Packages: - --with-pam build with PAM support - --with-bsd-auth build with BSD Authentication support - --with-ldap build with LDAP support -+ --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; } + prefer BSD Libedit over GNU Readline + --with-uuid=LIB build contrib/uuid-ossp using LIB (bsd,e2fs,ossp) + --with-ossp-uuid obsolete spelling of --with-uuid=ossp ++ --with-libcurl build with libcurl support for OAuth client flows + --with-libxml build with XML support + --with-libxslt use XSLT support when building contrib/xml2 + --with-system-tzdata=DIR +@@ configure: Some influential environment variables: + path overriding pkg-config's built-in search path + ICU_CFLAGS C compiler flags for ICU, overriding pkg-config + ICU_LIBS linker flags for ICU, overriding pkg-config ++ LIBCURL_CFLAGS ++ C compiler flags for LIBCURL, overriding pkg-config ++ LIBCURL_LIBS ++ linker flags for LIBCURL, overriding pkg-config + XML2_CONFIG path to xml2-config utility + XML2_CFLAGS C compiler flags for XML2, overriding pkg-config + XML2_LIBS linker flags for XML2, overriding pkg-config +@@ configure: fi +# -+# OAuth 2.0 ++# libcurl +# -+{ $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; } ++{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with libcurl support for OAuth client flows" >&5 ++$as_echo_n "checking whether to build with libcurl support for OAuth client flows... " >&6; } + + + -+# Check whether --with-builtin-oauth was given. -+if test "${with_builtin_oauth+set}" = set; then : -+ withval=$with_builtin_oauth; ++# Check whether --with-libcurl was given. ++if test "${with_libcurl+set}" = set; then : ++ withval=$with_libcurl; + case $withval in + yes) -+ as_fn_error $? "argument required for --with-builtin-oauth option" "$LINENO" 5 ++ ++$as_echo "#define USE_LIBCURL 1" >>confdefs.h ++ + ;; + no) -+ as_fn_error $? "argument required for --with-builtin-oauth option" "$LINENO" 5 ++ : + ;; + *) -+ ++ as_fn_error $? "no argument expected for --with-libcurl option" "$LINENO" 5 + ;; + esac + ++else ++ with_libcurl=no ++ +fi + + -+if test x"$with_builtin_oauth" = x"" ; then -+ with_builtin_oauth=no ++{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_libcurl" >&5 ++$as_echo "$with_libcurl" >&6; } ++ ++ ++if test "$with_libcurl" = yes ; then ++ # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability ++ # to explicitly set TLS 1.3 ciphersuites). ++ ++pkg_failed=no ++{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for libcurl >= 7.61.0" >&5 ++$as_echo_n "checking for libcurl >= 7.61.0... " >&6; } ++ ++if test -n "$LIBCURL_CFLAGS"; then ++ pkg_cv_LIBCURL_CFLAGS="$LIBCURL_CFLAGS" ++ elif test -n "$PKG_CONFIG"; then ++ if test -n "$PKG_CONFIG" && \ ++ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libcurl >= 7.61.0\""; } >&5 ++ ($PKG_CONFIG --exists --print-errors "libcurl >= 7.61.0") 2>&5 ++ ac_status=$? ++ $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 ++ test $ac_status = 0; }; then ++ pkg_cv_LIBCURL_CFLAGS=`$PKG_CONFIG --cflags "libcurl >= 7.61.0" 2>/dev/null` ++ test "x$?" != "x0" && pkg_failed=yes ++else ++ pkg_failed=yes ++fi ++ else ++ pkg_failed=untried ++fi ++if test -n "$LIBCURL_LIBS"; then ++ pkg_cv_LIBCURL_LIBS="$LIBCURL_LIBS" ++ elif test -n "$PKG_CONFIG"; then ++ if test -n "$PKG_CONFIG" && \ ++ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libcurl >= 7.61.0\""; } >&5 ++ ($PKG_CONFIG --exists --print-errors "libcurl >= 7.61.0") 2>&5 ++ ac_status=$? ++ $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 ++ test $ac_status = 0; }; then ++ pkg_cv_LIBCURL_LIBS=`$PKG_CONFIG --libs "libcurl >= 7.61.0" 2>/dev/null` ++ test "x$?" != "x0" && pkg_failed=yes ++else ++ pkg_failed=yes +fi ++ else ++ pkg_failed=untried ++fi ++ + -+if test x"$with_builtin_oauth" = x"curl"; then + -+$as_echo "#define USE_BUILTIN_OAUTH 1" >>confdefs.h ++if test $pkg_failed = yes; then ++ { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 ++$as_echo "no" >&6; } + ++if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then ++ _pkg_short_errors_supported=yes ++else ++ _pkg_short_errors_supported=no ++fi ++ if test $_pkg_short_errors_supported = yes; then ++ LIBCURL_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libcurl >= 7.61.0" 2>&1` ++ else ++ LIBCURL_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libcurl >= 7.61.0" 2>&1` ++ fi ++ # Put the nasty error message in config.log where it belongs ++ echo "$LIBCURL_PKG_ERRORS" >&5 ++ ++ as_fn_error $? "Package requirements (libcurl >= 7.61.0) were not met: ++ ++$LIBCURL_PKG_ERRORS ++ ++Consider adjusting the PKG_CONFIG_PATH environment variable if you ++installed software in a non-standard prefix. ++ ++Alternatively, you may set the environment variables LIBCURL_CFLAGS ++and LIBCURL_LIBS to avoid the need to call pkg-config. ++See the pkg-config man page for more details." "$LINENO" 5 ++elif test $pkg_failed = untried; then ++ { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 ++$as_echo "no" >&6; } ++ { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 ++$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} ++as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it ++is in your PATH or set the PKG_CONFIG environment variable to the full ++path to pkg-config. ++ ++Alternatively, you may set the environment variables LIBCURL_CFLAGS ++and LIBCURL_LIBS to avoid the need to call pkg-config. ++See the pkg-config man page for more details. ++ ++To get pkg-config, see . ++See \`config.log' for more details" "$LINENO" 5; } ++else ++ LIBCURL_CFLAGS=$pkg_cv_LIBCURL_CFLAGS ++ LIBCURL_LIBS=$pkg_cv_LIBCURL_LIBS ++ { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 ++$as_echo "yes" >&6; } + -+$as_echo "#define USE_OAUTH_CURL 1" >>confdefs.h ++fi + + # OAuth requires python for testing + if test "$with_python" != yes; then -+ { $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;} ++ { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests require --with-python to run" >&5 ++$as_echo "$as_me: WARNING: *** OAuth support tests require --with-python to run" >&2;} + fi -+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_builtin_oauth" >&5 -+$as_echo "$with_builtin_oauth" >&6; } -+ -+ + # - # Bonjour + # XML # @@ configure: fi @@ configure: 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_builtin_oauth" = curl ; then ++if test "$with_libcurl" = yes ; 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-builtin-oauth=curl" "$LINENO" 5 -+fi -+ -+ { $as_echo "$as_me:${as_lineno-$LINENO}: checking for compatible libcurl" >&5 -+$as_echo_n "checking for compatible libcurl... " >&6; } -+if ${pgac_cv_check_libcurl+:} false; then : -+ $as_echo_n "(cached) " >&6 -+else -+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext -+/* end confdefs.h. */ -+#include -+#if LIBCURL_VERSION_MAJOR < 7 || (LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR < 61) -+choke me -+#endif -+int -+main () -+{ -+ -+ ; -+ return 0; -+} -+_ACEOF -+if ac_fn_c_try_compile "$LINENO"; then : -+ pgac_cv_check_libcurl=yes -+else -+ pgac_cv_check_libcurl=no -+fi -+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext ++ as_fn_error $? "library 'curl' is required for --with-libcurl" "$LINENO" 5 +fi -+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_check_libcurl" >&5 -+$as_echo "$pgac_cv_check_libcurl" >&6; } + -+if test "$pgac_cv_check_libcurl" != yes; then -+ as_fn_error $? " -+*** The installed version of libcurl is too old to use with PostgreSQL. -+*** libcurl version 7.61.0 or later is required." "$LINENO" 5 -+fi +fi + if test "$with_gssapi" = yes ; then @@ configure: fi +fi + -+if test "$with_builtin_oauth" = curl; then ++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" +if test "x$ac_cv_header_curl_curl_h" = xyes; then : + +else -+ as_fn_error $? "header file is required for --with-builtin-oauth=curl" "$LINENO" 5 ++ as_fn_error $? "header file is required for --with-libcurl" "$LINENO" 5 +fi + + @@ configure: fi if test "$PORTNAME" = "win32" ; then ## configure.ac ## -@@ configure.ac: AC_MSG_RESULT([$with_ldap]) - AC_SUBST(with_ldap) +@@ configure.ac: fi + AC_SUBST(with_uuid) +# -+# OAuth 2.0 ++# libcurl +# -+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 ++AC_MSG_CHECKING([whether to build with libcurl support for OAuth client flows]) ++PGAC_ARG_BOOL(with, libcurl, no, [build with libcurl support for OAuth client flows], ++ [AC_DEFINE([USE_LIBCURL], 1, [Define to 1 to build with libcurl support for OAuth client flows. (--with-libcurl)])]) ++AC_MSG_RESULT([$with_libcurl]) ++AC_SUBST(with_libcurl) ++ ++if test "$with_libcurl" = yes ; then ++ # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability ++ # to explicitly set TLS 1.3 ciphersuites). ++ PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.61.0]) + -+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]) ++ AC_MSG_WARN([*** OAuth support tests require --with-python to run]) + fi -+elif test x"$with_builtin_oauth" != x"no"; then -+ AC_MSG_ERROR([--with-builtin-oauth must specify curl]) +fi + -+AC_MSG_RESULT([$with_builtin_oauth]) -+AC_SUBST(with_builtin_oauth) -+ + # - # Bonjour + # XML # @@ configure.ac: failure. It is possible the compiler isn't looking in the proper directory. Use --without-zlib to disable zlib support.])]) @@ configure.ac: failure. It is possible the compiler isn't looking in the proper +# 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 ++if test "$with_libcurl" = yes ; then ++ AC_CHECK_LIB(curl, curl_multi_init, [], [AC_MSG_ERROR([library 'curl' is required for --with-libcurl])]) +fi + if test "$with_gssapi" = yes ; then @@ configure.ac: elif test "$with_uuid" = ossp ; then [AC_MSG_ERROR([header file or is required for OSSP UUID])])]) fi -+if test "$with_builtin_oauth" = curl; then -+ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file is required for --with-builtin-oauth=curl])]) ++if test "$with_libcurl" = yes; then ++ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file is required for --with-libcurl])]) +fi + if test "$PORTNAME" = "win32" ; then @@ doc/src/sgml/installation.sgml: build-postgresql: -+ -+ ++ ++ + + -+ Build with support for OAuth 2.0 client flows. The only -+ LIBRARY supported is . ++ Build with libcurl support for OAuth 2.0 client flows. + This requires the curl package to be + installed. Building with this will check for the required header files + and libraries to make sure that your curl @@ doc/src/sgml/installation.sgml: build-postgresql: + + + - - + + @@ doc/src/sgml/installation.sgml: ninja install -+ -+ ++ ++ + + -+ Build with support for OAuth 2.0 client flows. The only -+ LIBRARY supported is . ++ Build with libcurl support for OAuth 2.0 client flows. + This requires the curl package to be + installed. Building with this will check for the required header files + and libraries to make sure that your curl @@ doc/src/sgml/installation.sgml: ninja install + + + - - + + ## doc/src/sgml/libpq.sgml ## @@ meson.build: endif +############################################################### -+# Library: OAuth (libcurl) ++# Library: libcurl +############################################################### + -+libcurl = not_found_dep -+oauth_library = 'none' -+oauthopt = get_option('builtin_oauth') -+ -+if oauthopt == 'auto' and auto_features.disabled() -+ oauthopt = 'none' -+endif -+ -+if oauthopt in ['auto', 'curl'] ++libcurlopt = get_option('libcurl') ++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: (oauthopt == 'curl')) ++ libcurl = dependency('libcurl', version: '>= 7.61.0', required: libcurlopt) + if libcurl.found() -+ oauth_library = 'curl' -+ cdata.set('USE_BUILTIN_OAUTH', 1) -+ cdata.set('USE_OAUTH_CURL', 1) ++ cdata.set('USE_LIBCURL', 1) + endif ++else ++ libcurl = not_found_dep +endif + -+if oauthopt == 'auto' and auto_features.enabled() and not libcurl.found() -+ error('no OAuth implementation library found') -+endif + + ############################################################### - # Library: Tcl (for pltcl) - # + # Library: libxml + ############################################################### @@ meson.build: libpq_deps += [ gssapi, @@ meson.build: if meson.version().version_compare('>=0.57') 'llvm': llvm, ## meson_options.txt ## -@@ meson_options.txt: option('bonjour', type: 'feature', value: 'auto', - option('bsd_auth', type: 'feature', value: 'auto', - description: 'BSD Authentication support') +@@ meson_options.txt: option('icu', type: 'feature', value: 'auto', + option('ldap', type: 'feature', value: 'auto', + description: 'LDAP support') -+option('builtin_oauth', type : 'combo', choices : ['auto', 'none', 'curl'], -+ value: 'auto', -+ description: 'use LIB for built-in OAuth 2.0 client flows (curl)') ++option('libcurl', type : 'feature', value: 'auto', ++ description: 'libcurl support for OAuth client flows') + - option('docs', type: 'feature', value: 'auto', - description: 'Documentation in HTML and man page format') + option('libedit_preferred', type: 'boolean', value: false, + description: 'Prefer BSD Libedit over GNU Readline') ## src/Makefile.global.in ## -@@ src/Makefile.global.in: with_ldap = @with_ldap@ +@@ src/Makefile.global.in: with_systemd = @with_systemd@ + with_gssapi = @with_gssapi@ + with_krb_srvnam = @with_krb_srvnam@ + with_ldap = @with_ldap@ ++with_libcurl = @with_libcurl@ with_libxml = @with_libxml@ with_libxslt = @with_libxslt@ with_llvm = @with_llvm@ -+with_builtin_oauth = @with_builtin_oauth@ - with_system_tzdata = @with_system_tzdata@ - with_uuid = @with_uuid@ - with_zlib = @with_zlib@ ## src/backend/libpq/Makefile ## @@ src/backend/libpq/Makefile: include $(top_builddir)/src/Makefile.global @@ src/backend/libpq/auth-oauth.c (new) + +/* Mechanism declaration */ +const pg_be_sasl_mech pg_be_oauth_mech = { -+ oauth_get_mechanisms, -+ oauth_init, -+ oauth_exchange, ++ .get_mechanisms = oauth_get_mechanisms, ++ .init = oauth_init, ++ .exchange = oauth_exchange, + -+ PG_MAX_AUTH_TOKEN_LENGTH, ++ .max_message_length = PG_MAX_AUTH_TOKEN_LENGTH, +}; + + @@ src/backend/libpq/auth-oauth.c (new) + value = sep + 1; + validate_kvpair(key, value); + -+ if (!strcmp(key, AUTH_KEY)) ++ if (strcmp(key, AUTH_KEY) == 0) + { + if (auth) + ereport(ERROR, @@ src/backend/libpq/auth-oauth.c (new) + if (!(token = validate_token_format(auth))) + return false; + ++ /* ++ * Ensure that we have a validation library loaded, this should always be ++ * the case and an error here is indicative of a bug. ++ */ ++ if (!ValidatorCallbacks || !ValidatorCallbacks->validate_cb) ++ ereport(FATAL, ++ errcode(ERRCODE_INTERNAL_ERROR), ++ errmsg("validation of OAuth token requested without a validator loaded")); ++ + /* Call the validation function from the validator module */ + ret = ValidatorCallbacks->validate_cb(validator_module_state, + token, port->user_name); @@ src/backend/libpq/auth-oauth.c (new) + "OAuth validator", libname, "_PG_oauth_validator_module_init")); + + ValidatorCallbacks = (*validator_init) (); ++ Assert(ValidatorCallbacks); + + /* Allocate memory for validator library private state data */ + validator_module_state = (ValidatorModuleState *) palloc0(sizeof(ValidatorModuleState)); @@ src/backend/libpq/auth-oauth.c (new) + char *file_name = hbaline->sourcefile; + char *rawstring; + List *elemlist = NIL; -+ ListCell *l; + + *err_msg = NULL; + @@ src/backend/libpq/auth-oauth.c (new) + goto done; + } + -+ foreach(l, elemlist) ++ foreach_ptr(char, allowed, elemlist) + { -+ char *allowed = lfirst(l); -+ + if (strcmp(allowed, hbaline->oauth_validator) == 0) + goto done; + } @@ src/include/pg_config.h.in #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 LDAP support. (--with-ldap) */ + #undef USE_LDAP -+/* Define to 1 to build with OAuth 2.0 client flows. (--with-builtin-oauth) */ -+#undef USE_BUILTIN_OAUTH ++/* Define to 1 to build with libcurl support for OAuth client flows. ++ (--with-libcurl) */ ++#undef USE_LIBCURL + - /* 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 use libcurl for OAuth client flows. */ -+#undef USE_OAUTH_CURL -+ - /* Define to 1 to build with OpenSSL support. (--with-ssl=openssl) */ - #undef USE_OPENSSL + /* Define to 1 to build with XML support. (--with-libxml) */ + #undef USE_LIBXML ## src/interfaces/libpq/Makefile ## @@ src/interfaces/libpq/Makefile: OBJS += \ fe-secure-gssapi.o endif -+ifeq ($(with_builtin_oauth),curl) ++ifeq ($(with_libcurl),yes) +OBJS += fe-auth-oauth-curl.o +endif + @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + { + char **scalar; /* for all scalar types */ + struct curl_slist **array; /* for type == JSON_TOKEN_ARRAY_START */ -+ }; ++ } target; + + bool required; /* REQUIRED field, or just OPTIONAL? */ +}; @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + } + } + -+ free(name); + return JSON_SUCCESS; +} + @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + struct oauth_parse *ctx = state; + + --ctx->nested; ++ if (!ctx->nested) ++ Assert(!ctx->active); /* all fields should be fully processed */ ++ + return JSON_SUCCESS; +} + @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) +oauth_json_scalar(void *state, char *token, JsonTokenType type) +{ + struct oauth_parse *ctx = state; -+ JsonParseErrorType result = JSON_SUCCESS; + + if (!ctx->nested) + { + oauth_parse_set_error(ctx, "top-level element must be an object"); -+ result = JSON_SEM_ACTION_FAILED; -+ goto cleanup; ++ return JSON_SEM_ACTION_FAILED; + } + + if (ctx->active) + { -+ JsonTokenType expected; ++ const struct json_field *field = ctx->active; ++ JsonTokenType expected = field->type; + -+ /* -+ * Make sure this matches what the active field expects. Arrays must -+ * contain only strings with the current implementation. -+ */ -+ if (ctx->active->type == JSON_TOKEN_ARRAY_START) ++ /* Make sure this matches what the active field expects. */ ++ if (expected == JSON_TOKEN_ARRAY_START) ++ { ++ /* Are we actually inside an array? */ ++ if (ctx->nested < 2) ++ { ++ report_type_mismatch(ctx); ++ return JSON_SEM_ACTION_FAILED; ++ } ++ ++ /* Currently, arrays can only contain strings. */ + expected = JSON_TOKEN_STRING; -+ else -+ expected = ctx->active->type; ++ } + + if (type != expected) + { + report_type_mismatch(ctx); -+ result = JSON_SEM_ACTION_FAILED; -+ goto cleanup; ++ return JSON_SEM_ACTION_FAILED; + } + + /* -+ * FIXME if the JSON field is duplicated, we'll leak the prior value. -+ * Error out in that case instead. ++ * We don't allow duplicate field names; error out if the target has ++ * already been set. + */ -+ if (ctx->active->type != JSON_TOKEN_ARRAY_START) ++ if ((field->type == JSON_TOKEN_ARRAY_START && *field->target.array) ++ || (field->type != JSON_TOKEN_ARRAY_START && *field->target.scalar)) ++ { ++ oauth_parse_set_error(ctx, "field \"%s\" is duplicated", ++ field->name); ++ return JSON_SEM_ACTION_FAILED; ++ } ++ ++ if (field->type != JSON_TOKEN_ARRAY_START) + { + Assert(ctx->nested == 1); + -+ *ctx->active->scalar = token; ++ *field->target.scalar = strdup(token); ++ if (!*field->target.scalar) ++ return JSON_OUT_OF_MEMORY; ++ + ctx->active = NULL; + -+ return JSON_SUCCESS; /* don't free the token */ ++ return JSON_SUCCESS; + } -+ else /* ctx->target_array */ ++ else + { + struct curl_slist *temp; + + Assert(ctx->nested == 2); + -+ temp = curl_slist_append(*ctx->active->array, token); ++ /* Note that curl_slist_append() makes a copy of the token. */ ++ temp = curl_slist_append(*field->target.array, token); + if (!temp) -+ { -+ oauth_parse_set_error(ctx, "out of memory"); -+ result = JSON_SEM_ACTION_FAILED; -+ goto cleanup; -+ } ++ return JSON_OUT_OF_MEMORY; + -+ *ctx->active->array = temp; -+ -+ /* -+ * Note that curl_slist_append() makes a copy of the token, so we -+ * can free it below. -+ */ ++ *field->target.array = temp; + } + } + else @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + /* otherwise we just ignore it */ + } + -+cleanup: -+ free(token); -+ return result; ++ return JSON_SUCCESS; +} + +/* @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + return false; + } + ++ /* ++ * pg_parse_json doesn't validate the incoming UTF-8, so we have to check ++ * that up front. ++ */ ++ if (pg_encoding_verifymbstr(PG_UTF8, resp->data, resp->len) != resp->len) ++ { ++ actx_error(actx, "response is not valid UTF-8"); ++ return false; ++ } ++ + makeJsonLexContextCstringLen(&lex, resp->data, resp->len, PG_UTF8, true); ++ setJsonLexContextOwnsTokens(&lex, true); /* must not leak on error */ + + ctx.errbuf = &actx->errbuf; + ctx.fields = fields; @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + /* Check all required fields. */ + while (fields->name) + { -+ if (fields->required && !*fields->scalar && !*fields->array) ++ if (fields->required ++ && !*fields->target.scalar ++ && !*fields->target.array) + { + actx_error(actx, "field \"%s\" is missing", fields->name); + goto cleanup; @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + escaped = curl_easy_escape(NULL, s, 0); + if (!escaped) + { -+ markPQExpBufferBroken(buf); ++ termPQExpBuffer(buf); /* mark the buffer broken */ + return; + } + @@ src/interfaces/libpq/fe-auth-oauth.c (new) + } + } + -+ free(name); + return JSON_SUCCESS; +} + @@ src/interfaces/libpq/fe-auth-oauth.c (new) + if (!ctx->nested) + { + ctx->errmsg = libpq_gettext("top-level element must be an object"); ++ return JSON_SEM_ACTION_FAILED; + } -+ else if (ctx->target_field) ++ ++ if (ctx->target_field) + { + Assert(ctx->nested == 1); + -+ if (type == JSON_TOKEN_STRING) ++ /* ++ * We don't allow duplicate field names; error out if the target has ++ * already been set. ++ */ ++ if (*ctx->target_field) + { -+ *ctx->target_field = token; -+ -+ ctx->target_field = NULL; -+ ctx->target_field_name = NULL; ++ oauth_json_set_error(ctx, ++ libpq_gettext("field \"%s\" is duplicated"), ++ ctx->target_field_name); ++ return JSON_SEM_ACTION_FAILED; ++ } + -+ return JSON_SUCCESS; /* don't free the token we're using */ ++ /* The only fields we support are strings. */ ++ if (type != JSON_TOKEN_STRING) ++ { ++ oauth_json_set_error(ctx, ++ libpq_gettext("field \"%s\" must be a string"), ++ ctx->target_field_name); ++ return JSON_SEM_ACTION_FAILED; + } + -+ oauth_json_set_error(ctx, -+ libpq_gettext("field \"%s\" must be a string"), -+ ctx->target_field_name); ++ *ctx->target_field = strdup(token); ++ if (!*ctx->target_field) ++ return JSON_OUT_OF_MEMORY; ++ ++ ctx->target_field = NULL; ++ ctx->target_field_name = NULL; ++ } ++ else ++ { ++ /* otherwise we just ignore it */ + } + -+ free(token); -+ return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS; ++ return JSON_SUCCESS; +} + +#define HTTPS_SCHEME "https://" @@ src/interfaces/libpq/fe-auth-oauth.c (new) +} + +static bool -+handle_oauth_sasl_error(PGconn *conn, char *msg, int msglen) ++handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen) +{ + JsonLexContext lex = {0}; + JsonSemAction sem = {0}; + JsonParseErrorType err; + struct json_ctx ctx = {0}; + char *errmsg = NULL; ++ bool success = false; ++ ++ Assert(conn->oauth_issuer_id); /* ensured by setup_oauth_parameters() */ + + /* Sanity check. */ + if (strlen(msg) != msglen) @@ src/interfaces/libpq/fe-auth-oauth.c (new) + return false; + } + ++ /* ++ * pg_parse_json doesn't validate the incoming UTF-8, so we have to check ++ * that up front. ++ */ ++ if (pg_encoding_verifymbstr(PG_UTF8, msg, msglen) != msglen) ++ { ++ libpq_append_conn_error(conn, ++ "server's error response is not valid UTF-8"); ++ return false; ++ } ++ + makeJsonLexContextCstringLen(&lex, msg, msglen, PG_UTF8, true); ++ setJsonLexContextOwnsTokens(&lex, true); /* must not leak on error */ + + initPQExpBuffer(&ctx.errbuf); + sem.semstate = &ctx; @@ src/interfaces/libpq/fe-auth-oauth.c (new) + freeJsonLexContext(&lex); + + if (errmsg) -+ return false; ++ goto cleanup; + + /* TODO: what if these override what the user already specified? */ + /* TODO: what if there's no discovery URI? */ @@ src/interfaces/libpq/fe-auth-oauth.c (new) + /* The URI must correspond to our existing issuer, to avoid mix-ups. */ + discovery_issuer = issuer_from_well_known_uri(conn, ctx.discovery_uri); + if (!discovery_issuer) -+ return false; /* error message already set */ ++ goto cleanup; /* error message already set */ + + if (strcmp(conn->oauth_issuer_id, discovery_issuer) != 0) + { @@ src/interfaces/libpq/fe-auth-oauth.c (new) + conn->oauth_issuer_id); + + free(discovery_issuer); -+ return false; ++ goto cleanup; + } + + free(discovery_issuer); @@ src/interfaces/libpq/fe-auth-oauth.c (new) + free(conn->oauth_discovery_uri); + + conn->oauth_discovery_uri = ctx.discovery_uri; ++ ctx.discovery_uri = NULL; + } + + if (ctx.scope) @@ src/interfaces/libpq/fe-auth-oauth.c (new) + free(conn->oauth_scope); + + conn->oauth_scope = ctx.scope; ++ ctx.scope = NULL; + } + /* TODO: missing error scope should clear any existing connection scope */ + @@ src/interfaces/libpq/fe-auth-oauth.c (new) + { + libpq_append_conn_error(conn, + "server sent error response without a status"); -+ return false; ++ goto cleanup; + } + + if (strcmp(ctx.status, "invalid_token") == 0) @@ src/interfaces/libpq/fe-auth-oauth.c (new) + } + /* TODO: include status in hard failure message */ + -+ return true; ++ success = true; ++ ++cleanup: ++ free(ctx.status); ++ free(ctx.scope); ++ free(ctx.discovery_uri); ++ ++ return success; +} + +static void @@ src/interfaces/libpq/fe-auth-oauth.c (new) + } + else + { -+#if USE_BUILTIN_OAUTH ++#if USE_LIBCURL + /* + * Hand off to our built-in OAuth flow. + * @@ src/interfaces/libpq/fe-auth-oauth.c (new) + 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"); ++ libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built using --with-libcurl"); + goto fail; + +#endif @@ src/interfaces/libpq/fe-auth-oauth.c (new) + * Respond with the required dummy message (RFC 7628, sec. 3.2.3). + */ + *output = strdup(kvsep); ++ if (unlikely(!*output)) ++ { ++ libpq_append_conn_error(conn, "out of memory"); ++ return SASL_FAILED; ++ } + *outputlen = strlen(*output); /* == 1 */ + + state->state = FE_OAUTH_SERVER_ERROR; @@ src/interfaces/libpq/meson.build: if gssapi.found() ) endif -+if oauth_library == 'curl' ++if libcurl.found() + libpq_sources += files('fe-auth-oauth-curl.c') +endif + @@ src/interfaces/libpq/meson.build: if gssapi.found() kwargs: gen_export_kwargs, ) - ## src/interfaces/libpq/pqexpbuffer.c ## -@@ src/interfaces/libpq/pqexpbuffer.c: static const char *const oom_buffer_ptr = oom_buffer; - * - * Put a PQExpBuffer in "broken" state if it isn't already. - */ --static void -+void - markPQExpBufferBroken(PQExpBuffer str) - { - if (str->data != oom_buffer) - - ## src/interfaces/libpq/pqexpbuffer.h ## -@@ src/interfaces/libpq/pqexpbuffer.h: extern void initPQExpBuffer(PQExpBuffer str); - extern void destroyPQExpBuffer(PQExpBuffer str); - extern void termPQExpBuffer(PQExpBuffer str); - -+/*------------------------ -+ * markPQExpBufferBroken -+ * Put a PQExpBuffer in "broken" state if it isn't already. -+ */ -+extern void markPQExpBufferBroken(PQExpBuffer str); -+ - /*------------------------ - * resetPQExpBuffer - * 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, @@ src/test/modules/oauth_validator/Makefile (new) +include $(top_srcdir)/contrib/contrib-global.mk + +export PYTHON -+export with_builtin_oauth ++export with_libcurl +export with_python + +endif @@ src/test/modules/oauth_validator/fail_validator.c (new) + +#include "fmgr.h" +#include "libpq/oauth.h" -+#include "miscadmin.h" -+#include "utils/guc.h" -+#include "utils/memutils.h" + +PG_MODULE_MAGIC; + @@ src/test/modules/oauth_validator/meson.build (new) + ], + 'env': { + 'PYTHON': python.path(), -+ 'with_builtin_oauth': oauth_library, ++ 'with_libcurl': libcurl.found() ? 'yes' : 'no', + 'with_python': 'yes', + }, + }, @@ src/test/modules/oauth_validator/t/001_server.pl (new) + 'Potentially unsafe test oauth not enabled in PG_TEST_EXTRA'; +} + -+if ($ENV{with_builtin_oauth} ne 'curl') ++if ($ENV{with_libcurl} ne 'yes') +{ + plan skip_all => 'client-side OAuth not supported by this build'; +} @@ src/test/modules/oauth_validator/t/002_client.pl (new) + $log_start, + log_like => [ qr/oauth_validator: token="my-token", role="$user"/, ]); + -+if ($ENV{with_builtin_oauth} ne 'curl') ++if ($ENV{with_libcurl} ne 'yes') +{ + # 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/ ++ qr/no custom OAuth flows are available, and libpq was not built using --with-libcurl/ + ); +} + @@ src/tools/pgindent/typedefs.list: explain_get_index_name_hook_type fe_scram_state fe_scram_state_enum fetch_range_request -@@ src/tools/pgindent/typedefs.list: normal_rand_fctx - nsphash_hash +@@ src/tools/pgindent/typedefs.list: nsphash_hash ntile_context + nullingrel_info numeric +oauth_state object_access_hook_type 2: 3d169848db ! 2: 28cc3463aa DO NOT MERGE: Add pytest suite for OAuth @@ 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_builtin_oauth") == "none", -+ reason="OAuth client tests require --with-builtin-oauth support", ++ os.getenv("with_libcurl") != "yes", ++ reason="OAuth client tests require --with-libcurl support", +) + +if platform.system() == "Darwin": @@ src/test/python/client/test_oauth.py (new) +def fail_oauth_handshake(conn, sasl_resp, *, errmsg="doesn't matter"): + """ + Sends a failure response via the OAUTHBEARER mechanism, consumes the -+ client's dummy reponse, and issues a FATAL error to end the exchange. ++ client's dummy response, and issues a FATAL error to end the exchange. + + sasl_resp is a dictionary which will be serialized as the OAUTHBEARER JSON + response. If provided, errmsg is used in the FATAL ErrorResponse. @@ src/test/python/client/test_oauth.py (new) + pass + + ++class RawBytes(bytes): ++ """ ++ Like RawResponse, but bypasses the UTF-8 encoding step as well, allowing ++ implementations to issue invalid encodings. ++ """ ++ ++ pass ++ ++ +class OpenIDProvider(threading.Thread): + """ + A thread that runs a mock OpenID provider server on an SSL-enabled socket. @@ src/test/python/client/test_oauth.py (new) + self.end_headers() + + if resp is not None: -+ if not isinstance(resp, RawResponse): -+ resp = json.dumps(resp) -+ resp = resp.encode("utf-8") ++ if not isinstance(resp, RawBytes): ++ if not isinstance(resp, RawResponse): ++ resp = json.dumps(resp) ++ resp = resp.encode("utf-8") + self.wfile.write(resp) + + self.close_connection = True @@ src/test/python/client/test_oauth.py (new) + id="bad JSON: invalid syntax", + ), + pytest.param( ++ b"\xFF\xFF\xFF\xFF", ++ "server's error response is not valid UTF-8", ++ id="bad JSON: invalid encoding", ++ ), ++ pytest.param( + '"abcde"', + "top-level element must be an object", + id="bad JSON: top-level element is a string", @@ src/test/python/client/test_oauth.py (new) + id="bad JSON: int openid-configuration member", + ), + pytest.param( ++ '{ "status": "invalid_token", "openid-configuration": "", "openid-configuration": "" }', ++ 'field "openid-configuration" is duplicated', ++ id="bad JSON: duplicated field", ++ ), ++ pytest.param( + '{ "status": "invalid_token", "scope": 1 }', + 'field "scope" must be a string', + id="bad JSON: int scope member", @@ src/test/python/client/test_oauth.py (new) + with pq3.wrap(sock, debug_stream=sys.stdout) as conn: + initial = start_oauth_handshake(conn) + ++ if isinstance(response, str): ++ response = response.encode("utf-8") ++ + # Fail the SASL exchange with an invalid JSON response. + pq3.send( + conn, + pq3.types.AuthnRequest, + type=pq3.authn.SASLContinue, -+ body=response.encode("utf-8"), ++ body=response, + ) + + # The client should disconnect, so the socket is closed here. (If @@ src/test/python/client/test_oauth.py (new) + id="NULL bytes in document", + ), + pytest.param( ++ (200, RawBytes(b"blah\xFFblah")), ++ r"failed to parse OpenID discovery document: response is not valid UTF-8", ++ id="document is not UTF-8", ++ ), ++ pytest.param( + (200, 123), + r"failed to parse OpenID discovery document: top-level element must be an object", + id="scalar at top level", @@ src/test/python/client/test_oauth.py (new) + pytest.param( + (200, {"grant_types_supported": 123}), + r'failed to parse OpenID discovery document: field "grant_types_supported" must be an array of strings', -+ id="scalar grant types field", ++ id="numeric grant types field", ++ ), ++ pytest.param( ++ ( ++ 200, ++ { ++ "grant_types_supported": "urn:ietf:params:oauth:grant-type:device_code" ++ }, ++ ), ++ r'failed to parse OpenID discovery document: field "grant_types_supported" must be an array of strings', ++ id="string grant types field", + ), + pytest.param( + (200, {"grant_types_supported": {}}), @@ src/test/python/client/test_oauth.py (new) + r"failed to parse OpenID discovery document: the issuer identifier \(https://.*/path\) does not match oauth_issuer \(https://.*\)", + id="mismatched issuer identifier", + ), ++ pytest.param( ++ ( ++ 200, ++ RawResponse( ++ """{ ++ "issuer": "https://256.256.256.256/path", ++ "token_endpoint": "https://256.256.256.256/token", ++ "grant_types_supported": [ ++ "urn:ietf:params:oauth:grant-type:device_code" ++ ], ++ "device_authorization_endpoint": "https://256.256.256.256/dev", ++ "device_authorization_endpoint": "https://256.256.256.256/dev" ++ }""" ++ ), ++ ), ++ r'failed to parse OpenID discovery document: field "device_authorization_endpoint" is duplicated', ++ id="duplicated field", ++ ), + # + # Exercise HTTP-level failures by breaking the protocol. Note that the + # error messages here are implementation-dependent. @@ src/test/python/meson.build (new) +subdir('server') + +pytest_env = { -+ 'with_builtin_oauth': oauth_library, ++ 'with_libcurl': libcurl.found() ? 'yes' : 'no', + + # Point to the default database; the tests will create their own databases as + # needed. @@ src/test/python/server/oauthtest.c (new) + } + else + { -+ if (*expected_bearer && !strcmp(token, expected_bearer)) ++ if (*expected_bearer && strcmp(token, expected_bearer) == 0) + res->authorized = true; + if (set_authn_id) + res->authn_id = authn_id;