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