1: 6434d90105 = 1: 9c6a340119 common/jsonapi: support FRONTEND clients 2: 13ddf2b6b3 ! 2: 8072d0416e libpq: add OAUTHBEARER SASL mechanism @@ Commit message development headers. Pass `curl` or `iddawc` to --with-oauth/-Doauth during configuration. + Thomas Munro wrote the kqueue() implementation for oauth-curl; thanks! + Several TODOs: - don't retry forever if the server won't accept our token - perform several sanity checks on the OAuth issuer's responses @@ meson.build: if meson.version().version_compare('>=0.57') 'plperl': perl_dep, ## meson_options.txt ## -@@ meson_options.txt: option('lz4', type : 'feature', value: 'auto', +@@ meson_options.txt: option('lz4', type: 'feature', value: 'auto', option('nls', type: 'feature', value: 'auto', - description: 'native language support') + description: 'Native language support') +option('oauth', type : 'combo', choices : ['auto', 'none', 'curl', 'iddawc'], + value: 'auto', + description: 'use LIB for OAuth 2.0 support (curl, iddawc)') + - option('pam', type : 'feature', value: 'auto', - description: 'build with PAM support') + option('pam', type: 'feature', value: 'auto', + description: 'PAM support') ## src/Makefile.global.in ## @@ src/Makefile.global.in: with_ldap = @with_ldap@ ## src/common/meson.build ## @@ src/common/meson.build: common_sources_frontend_static += files( - # For the server build of pgcommon, depend on lwlocknames_h, because at least - # cryptohash_openssl.c, hmac_openssl.c depend on it. That's arguably a + # least cryptohash_openssl.c, hmac_openssl.c depend on it. + # controldata_utils.c depends on wait_event_types_h. That's arguably a # layering violation, but ... +# +# XXX Frontend builds need libpq's pqexpbuffer.h, so adjust the include paths @@ src/common/meson.build: common_sources_frontend_static += files( pgcommon_variants = { '_srv': internal_lib_args + { + 'include_directories': include_directories('.'), - 'sources': common_sources + [lwlocknames_h], + 'sources': common_sources + [lwlocknames_h] + [wait_event_types_h], 'dependencies': [backend_common_code], }, '': default_lib_args + { @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + +#include +#include ++#ifdef HAVE_SYS_EPOLL_H +#include +#include ++#endif ++#ifdef HAVE_SYS_EVENT_H ++#include ++#endif +#include + +#include "common/jsonapi.h" @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) +#include "libpq-int.h" +#include "mb/pg_wchar.h" + ++#ifdef HAVE_SYS_EVENT_H ++/* macOS doesn't define the time unit macros, but uses milliseconds by default. */ ++#ifndef NOTE_MSECONDS ++#define NOTE_MSECONDS 0 ++#endif ++#endif ++ +/* + * Parsed JSON Representations + * @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) +{ + OAuthStep step; /* where are we in the flow? */ + ++#ifdef HAVE_SYS_EPOLL_H + int timerfd; /* a timerfd for signaling async timeouts */ ++#endif + pgsocket mux; /* the multiplexer socket containing all descriptors + tracked by cURL, plus the timerfd */ + CURLM *curlm; /* top-level multi handle for cURL operations */ @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + + if (actx->mux != PGINVALID_SOCKET) + close(actx->mux); ++#ifdef HAVE_SYS_EPOLL_H + if (actx->timerfd >= 0) + close(actx->timerfd); ++#endif + + free(actx); +} @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + * Sets up the actx->mux, which is the altsock that PQconnectPoll clients will + * select() on instead of the Postgres socket during OAuth negotiation. + * -+ * This is just an epoll set abstracting multiple other descriptors. A timerfd -+ * is always part of the set; it's just disabled when we're not using it. ++ * This is just an epoll set or kqueue abstracting multiple other descriptors. ++ * A timerfd is always part of the set when using epoll; it's just disabled ++ * when we're not using it. + */ +static bool +setup_multiplexer(struct async_ctx *actx) +{ ++#ifdef HAVE_SYS_EPOLL_H + struct epoll_event ev = {.events = EPOLLIN}; + + actx->mux = epoll_create1(EPOLL_CLOEXEC); @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + } + + return true; ++#endif ++#ifdef HAVE_SYS_EVENT_H ++ actx->mux = kqueue(); ++ if (actx->mux < 0) ++ { ++ actx_error(actx, "failed to create kqueue: %m"); ++ return false; ++ } ++ ++ return true; ++#endif ++ ++ actx_error(actx, "here's a nickel kid, get yourself a better computer"); ++ return false; +} + +/* -+ * Adds and removes sockets from the multiplexer epoll set, as directed by the ++ * Adds and removes sockets from the multiplexer set, as directed by the + * cURL multi handle. + */ +static int +register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, + void *socketp) +{ ++#ifdef HAVE_SYS_EPOLL_H + struct async_ctx *actx = ctx; + struct epoll_event ev = {0}; + int res; @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + + return -1; + } ++#endif ++#ifdef HAVE_SYS_EVENT_H ++ struct async_ctx *actx = ctx; ++ struct kevent ev[2] = {{0}}; ++ struct kevent ev_out[2]; ++ struct timespec timeout = {0}; ++ int nev = 0; ++ int res; ++ ++ switch (what) ++ { ++ case CURL_POLL_IN: ++ EV_SET(&ev[nev], socket, EVFILT_READ, EV_ADD, 0, 0, 0); ++ nev++; ++ break; ++ ++ case CURL_POLL_OUT: ++ EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_ADD, 0, 0, 0); ++ nev++; ++ break; ++ ++ case CURL_POLL_INOUT: ++ EV_SET(&ev[nev], socket, EVFILT_READ, EV_ADD, 0, 0, 0); ++ nev++; ++ EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_ADD, 0, 0, 0); ++ nev++; ++ break; ++ ++ case CURL_POLL_REMOVE: ++ /* ++ * We don't know which of these is currently registered, perhaps ++ * both, so we try to remove both. This means we need to tolerate ++ * ENOENT below. ++ */ ++ EV_SET(&ev[nev], socket, EVFILT_READ, EV_DELETE, 0, 0, 0); ++ nev++; ++ EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_DELETE, 0, 0, 0); ++ nev++; ++ break; ++ ++ default: ++ actx_error(actx, "unknown cURL socket operation (%d)", what); ++ return -1; ++ } ++ ++ res = kevent(actx->mux, ev, nev, ev_out, lengthof(ev_out), &timeout); ++ if (res < 0) ++ { ++ actx_error(actx, "could not modify kqueue: %m"); ++ return -1; ++ } ++ ++ /* ++ * We can't use the simple errno version of kevent, because we need to skip ++ * over ENOENT while still allowing a second change to be processed. So we ++ * need a longer-form error checking loop. ++ */ ++ for (int i = 0; i < res; ++i) ++ { ++ if (ev_out[i].flags & EV_ERROR && ev_out[i].data != ENOENT) ++ { ++ errno = ev_out[i].data; ++ switch (what) ++ { ++ case CURL_POLL_REMOVE: ++ actx_error(actx, "could not delete from kqueue: %m"); ++ break; ++ default: ++ actx_error(actx, "could not add to kqueue: %m"); ++ } ++ return -1; ++ } ++ } ++#endif + + return 0; +} + +/* -+ * Adds or removes timeouts from the multiplexer epoll set, as directed by the -+ * cURL multi handle. Rather than continually adding and removing the timerfd, -+ * we keep it in the epoll set at all times and just disarm it when it's not ++ * Adds or removes timeouts from the multiplexer set, as directed by the ++ * cURL multi handle. Rather than continually adding and removing the timer, ++ * we keep it in the set at all times and just disarm it when it's not + * needed. + */ +static int +register_timer(CURLM *curlm, long timeout, void *ctx) +{ ++#if HAVE_SYS_EPOLL_H + struct async_ctx *actx = ctx; + struct itimerspec spec = {0}; + @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + actx_error(actx, "setting timerfd to %ld: %m", timeout); + return -1; + } ++#endif ++#ifdef HAVE_SYS_EVENT_H ++ struct async_ctx *actx = ctx; ++ struct kevent ev; ++ ++ EV_SET(&ev, 1, EVFILT_TIMER, timeout < 0 ? EV_DELETE : EV_ADD, ++ NOTE_MSECONDS, timeout, 0); ++ if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0 && errno != ENOENT) ++ { ++ actx_error(actx, "setting kqueue timer to %ld: %m", timeout); ++ return -1; ++ } ++#endif + + return 0; +} @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + } + + actx->mux = PGINVALID_SOCKET; ++#ifdef HAVE_SYS_EPOLL_H + actx->timerfd = -1; ++#endif + + state->async_ctx = actx; + @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + case OAUTH_STEP_TOKEN_REQUEST: + { + const struct token_error *err; ++#ifdef HAVE_SYS_EPOLL_H + struct itimerspec spec = {0}; ++#endif ++#ifdef HAVE_SYS_EVENT_H ++ struct kevent ev = {0}; ++#endif + + if (!finish_token_request(actx, &tok)) + goto error_return; @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + * Wait for the required interval before issuing the next request. + */ + Assert(actx->authz.interval > 0); ++#ifdef HAVE_SYS_EPOLL_H + spec.it_value.tv_sec = actx->authz.interval; + + if (timerfd_settime(actx->timerfd, 0 /* no flags */, &spec, NULL) < 0) @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + } + + *altsock = actx->timerfd; ++#endif ++#ifdef HAVE_SYS_EVENT_H ++ // XXX: I guess this wants to be hidden in a routine ++ EV_SET(&ev, 1, EVFILT_TIMER, EV_ADD, NOTE_MSECONDS, ++ actx->authz.interval * 1000, 0); ++ if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0) ++ { ++ actx_error(actx, "failed to set kqueue timer: %m"); ++ goto error_return; ++ } ++ // XXX: why did we change the altsock in the epoll version? ++#endif + actx->step = OAUTH_STEP_WAIT_INTERVAL; + break; + } 3: 0b0b0f2b33 ! 3: 07be9375aa backend: add OAUTHBEARER SASL mechanism @@ Commit message correctly is delegated entirely to the oauth_validator_command). Several TODOs: - - port to platforms other than "modern Linux" + - port to platforms other than "modern Linux/BSD" - overhaul the communication with oauth_validator_command, which is currently a bad hack on OpenPipeStream() - implement more sanity checks on the OAUTHBEARER message format and @@ src/backend/libpq/auth-oauth.c (new) +static bool validate(Port *port, const char *auth, const char **logdetail); +static bool run_validator_command(Port *port, const char *token); +static bool check_exit(FILE **fh, const char *command); -+static bool unset_cloexec(int fd); ++static bool set_cloexec(int fd); +static bool username_ok_for_shell(const char *username); + +#define KVSEP 0x01 @@ src/backend/libpq/auth-oauth.c (new) + * MUST read all data off of the pipe before writing anything). + * TODO: port to Windows using _pipe(). + */ -+ rc = pipe2(pipefd, O_CLOEXEC); ++ rc = pipe(pipefd); + if (rc < 0) + { + ereport(COMMERROR, @@ src/backend/libpq/auth-oauth.c (new) + rfd = pipefd[0]; + wfd = pipefd[1]; + -+ /* Allow the read pipe be passed to the child. */ -+ if (!unset_cloexec(rfd)) ++ if (!set_cloexec(wfd)) + { + /* error message was already logged */ + goto cleanup; @@ src/backend/libpq/auth-oauth.c (new) + } + + /* Execute the command. */ -+ fh = OpenPipeStream(command.data, "re"); -+ /* TODO: handle failures */ ++ fh = OpenPipeStream(command.data, "r"); ++ if (!fh) ++ { ++ ereport(COMMERROR, ++ (errcode_for_file_access(), ++ errmsg("opening pipe to OAuth validator: %m"))); ++ goto cleanup; ++ } + + /* We don't need the read end of the pipe anymore. */ + close(rfd); @@ src/backend/libpq/auth-oauth.c (new) +} + +static bool -+unset_cloexec(int fd) ++set_cloexec(int fd) +{ + int flags; + int rc; @@ src/backend/libpq/auth-oauth.c (new) + return false; + } + -+ rc = fcntl(fd, F_SETFD, flags & ~FD_CLOEXEC); ++ rc = fcntl(fd, F_SETFD, flags | FD_CLOEXEC); + if (rc < 0) + { + ereport(COMMERROR, + (errcode_for_file_access(), -+ errmsg("could not unset FD_CLOEXEC for child pipe: %m"))); ++ errmsg("could not set FD_CLOEXEC for child pipe: %m"))); + return false; + } + @@ src/include/libpq/auth.h + extern PGDLLIMPORT char *pg_krb_server_keyfile; extern PGDLLIMPORT bool pg_krb_caseins_users; - extern PGDLLIMPORT bool pg_gss_accept_deleg; -@@ src/include/libpq/auth.h: extern PGDLLIMPORT char *pg_krb_realm; + extern PGDLLIMPORT bool pg_gss_accept_delegation; +@@ src/include/libpq/auth.h: extern PGDLLIMPORT bool pg_gss_accept_delegation; extern void ClientAuthentication(Port *port); extern void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, int extralen); 4: 0e8ddadcbf ! 4: 71cedc6ff5 Add pytest suite for OAuth @@ src/test/python/requirements.txt (new) +construct~=2.10.61 +isort~=5.6 +# TODO: update to psycopg[c] 3.1 -+psycopg2~=2.8.6 ++psycopg2~=2.9.6 +pytest~=7.3 +pytest-asyncio~=0.21.0 5: 65c319a6a3 ! 5: 9b02e14829 squash! Add pytest suite for OAuth @@ Commit message TODOs: - The --tap-stream option to pytest-tap is slightly broken during test failures (it suppresses error information), which impedes debugging. - - Unsurprisingly, Windows and Mac builds fail on the Linux-specific - backend changes. + - Unsurprisingly, Windows builds fail on the Linux-/BSD-specific backend + changes. 32-bit builds on Ubuntu fail during testing as well. - pyca/cryptography is pinned at an old version. Since we use it for testing and not security, this isn't a critical problem yet, but it's not ideal. Newer versions require a Rust compiler to build, and while @@ meson.build: foreach test_dir : tests + test_command = [ + pytest, + # Avoid running these tests against an existing database. -+ '--temp-instance', test_output / 'tmp_check', ++ '--temp-instance', test_output / 'data', + + # FIXME pytest-tap's stream feature accidentally suppresses errors that + # are critical for debugging: @@ src/test/python/meson.build (new) @@ +# Copyright (c) 2023, PostgreSQL Global Development Group + ++pytest_env = { ++ 'with_oauth': oauth_library, ++ ++ # Point to the default database; the tests will create their own databases as ++ # needed. ++ 'PGDATABASE': 'postgres', ++ ++ # Avoid the need for a Rust compiler on platforms without prebuilt wheels for ++ # pyca/cryptography. ++ 'CRYPTOGRAPHY_DONT_BUILD_RUST': '1', ++} ++ ++# Some modules (psycopg2) need OpenSSL at compile time; for platforms where we ++# might have multiple implementations installed (macOS+brew), try to use the ++# same one that libpq is using. ++if ssl.found() ++ pytest_incdir = ssl.get_variable(pkgconfig: 'includedir', default_value: '') ++ if pytest_incdir != '' ++ pytest_env += { 'CPPFLAGS': '-I@0@'.format(pytest_incdir) } ++ endif ++ ++ pytest_libdir = ssl.get_variable(pkgconfig: 'libdir', default_value: '') ++ if pytest_libdir != '' ++ pytest_env += { 'LDFLAGS': '-L@0@'.format(pytest_libdir) } ++ endif ++endif ++ +tests += { + 'name': 'python', + 'sd': meson.current_source_dir(), @@ src/test/python/meson.build (new) + './test_internals.py', + './test_pq3.py', + ], -+ 'env': { -+ 'with_oauth': oauth_library, -+ -+ # Point to the default database; the tests will create their own databases -+ # as needed. -+ 'PGDATABASE': 'postgres', -+ -+ # Avoid the need for a Rust compiler on platforms without prebuilt wheels -+ # for pyca/cryptography. -+ 'CRYPTOGRAPHY_DONT_BUILD_RUST': '1', -+ }, ++ 'env': pytest_env, + }, +} @@ src/test/python/server/conftest.py: import pq3 + cleanup_prior_instance(datadir) + subprocess.run(["pg_ctl", "-D", datadir, "init"], check=True) + ++ # The CI looks for *.log files to upload, so the file name here isn't ++ # completely arbitrary. ++ log = os.path.join(datadir, "postmaster.log") + port = unused_tcp_port_factory() -+ log = os.path.join(datadir, "logfile") + + subprocess.run( + [ @@ src/test/python/server/conftest.py: import pq3 + "-l", + log, + "-o", -+ f"-c port={port} -c listen_addresses=localhost", ++ " ".join( ++ [ ++ f"-c port={port}", ++ "-c listen_addresses=localhost", ++ "-c log_connections=on", ++ ] ++ ), + "start", + ], + check=True, -: ---------- > 6: 7d179f7e53 XXX work around psycopg2 build failures