diff options
author | Jacob Champion <jchampion@postgresql.org> | 2025-05-01 09:14:30 -0700 |
---|---|---|
committer | Jacob Champion <jchampion@postgresql.org> | 2025-05-01 09:14:30 -0700 |
commit | b0635bfda0535a7fc36cd11d10eecec4e2a96330 (patch) | |
tree | 13a39de30ba014942dc006a023102a6d9bf2fa51 /src/interfaces/libpq/fe-auth-oauth.c | |
parent | a3ef0b570c56f7bb15e4aa5caf0125fff92a557a (diff) | |
download | postgresql-b0635bfda0535a7fc36cd11d10eecec4e2a96330.tar.gz postgresql-b0635bfda0535a7fc36cd11d10eecec4e2a96330.zip |
oauth: Move the builtin flow into a separate module
The additional packaging footprint of the OAuth Curl dependency, as well
as the existence of libcurl in the address space even if OAuth isn't
ever used by a client, has raised some concerns. Split off this
dependency into a separate loadable module called libpq-oauth.
When configured using --with-libcurl, libpq.so searches for this new
module via dlopen(). End users may choose not to install the libpq-oauth
module, in which case the default flow is disabled.
For static applications using libpq.a, the libpq-oauth staticlib is a
mandatory link-time dependency for --with-libcurl builds. libpq.pc has
been updated accordingly.
The default flow relies on some libpq internals. Some of these can be
safely duplicated (such as the SIGPIPE handlers), but others need to be
shared between libpq and libpq-oauth for thread-safety. To avoid
exporting these internals to all libpq clients forever, these
dependencies are instead injected from the libpq side via an
initialization function. This also lets libpq communicate the offsets of
PGconn struct members to libpq-oauth, so that we can function without
crashing if the module on the search path came from a different build of
Postgres. (A minor-version upgrade could swap the libpq-oauth module out
from under a long-running libpq client before it does its first load of
the OAuth flow.)
This ABI is considered "private". The module has no SONAME or version
symlinks, and it's named libpq-oauth-<major>.so to avoid mixing and
matching across Postgres versions. (Future improvements may promote this
"OAuth flow plugin" to a first-class concept, at which point we would
need a public API to replace this anyway.)
Additionally, NLS support for error messages in b3f0be788a was
incomplete, because the new error macros weren't being scanned by
xgettext. Fix that now.
Per request from Tom Lane and Bruce Momjian. Based on an initial patch
by Daniel Gustafsson, who also contributed docs changes. The "bare"
dlopen() concept came from Thomas Munro. Many people reviewed the design
and implementation; thank you!
Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Christoph Berg <myon@debian.org>
Reviewed-by: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Jelte Fennema-Nio <postgres@jeltef.nl>
Reviewed-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Wolfgang Walther <walther@technowledgy.de>
Discussion: https://postgr.es/m/641687.1742360249%40sss.pgh.pa.us
Diffstat (limited to 'src/interfaces/libpq/fe-auth-oauth.c')
-rw-r--r-- | src/interfaces/libpq/fe-auth-oauth.c | 229 |
1 files changed, 219 insertions, 10 deletions
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 <dlfcn.h> +#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; |